blob: 3d3ede344543abf42ee6979f88c83f0ff0a305c4 [file] [log] [blame]
savex4448e132018-04-25 15:51:14 +02001import abc
2import os
Alex1839bbf2019-08-22 17:17:21 -05003import re
Alex41485522019-04-12 17:26:18 -05004import time
savex4448e132018-04-25 15:51:14 +02005
Alex Savatieiev5118de02019-02-20 15:50:42 -06006from cfg_checker.common import const
Alex3ebc5632019-04-18 16:47:18 -05007from cfg_checker.common import logger_cli
Alex1839bbf2019-08-22 17:17:21 -05008from cfg_checker.common.file_utils import read_file_as_lines
Alex836fac82019-08-22 13:36:16 -05009from cfg_checker.nodes import salt_master
Alex3ebc5632019-04-18 16:47:18 -050010
11import jinja2
12
13import six
savex4448e132018-04-25 15:51:14 +020014
15pkg_dir = os.path.dirname(__file__)
Alex Savatieiev6d010be2019-03-11 10:36:59 -050016pkg_dir = os.path.join(pkg_dir, os.pardir, os.pardir)
savex4448e132018-04-25 15:51:14 +020017pkg_dir = os.path.normpath(pkg_dir)
18
Alex836fac82019-08-22 13:36:16 -050019# % threshhold values
20_disk_warn = 80
21_disk_critical = 90
22_ram_warn = 5
23_ram_critical = 3
Alex1839bbf2019-08-22 17:17:21 -050024_softnet_interval = 5
25
26UP = const.NODE_UP
27DOWN = const.NODE_DOWN
Alex836fac82019-08-22 13:36:16 -050028
savex4448e132018-04-25 15:51:14 +020029
savex4448e132018-04-25 15:51:14 +020030def line_breaks(text):
31 # replace python linebreaks with html breaks
32 return text.replace("\n", "<br />")
33
34
Alex41485522019-04-12 17:26:18 -050035def get_sorted_keys(td):
36 # detect if we can sort by desc
37 # Yes, this is slow, but bullet-proof from empty desc
38 _desc = all([bool(td[k]['desc']) for k in td.keys()])
39 # Get sorted list
40 if not _desc:
41 return sorted(td.keys())
42 else:
43 return sorted(
44 td.keys(),
45 key=lambda k: (
Alexd0391d42019-05-21 18:48:55 -050046 td[k]['desc']['section'],
Alex41485522019-04-12 17:26:18 -050047 td[k]['desc']['app'],
48 k
49 )
50 )
51
52
53def get_max(_list):
54 return sorted(_list)[-1]
55
56
Alex836fac82019-08-22 13:36:16 -050057def make_pkg_action_label(act):
Alex41485522019-04-12 17:26:18 -050058 _act_labels = {
59 const.ACT_UPGRADE: "Upgrade possible",
60 const.ACT_NEED_UP: "Needs upgrade",
61 const.ACT_NEED_DOWN: "Needs downgrade",
Alex9e4bfaf2019-06-11 15:21:59 -050062 const.ACT_REPO: "Repo update",
Alex41485522019-04-12 17:26:18 -050063 const.ACT_NA: ""
64 }
65 return _act_labels[act]
66
67
Alex836fac82019-08-22 13:36:16 -050068def make_pkg_action_class(act):
Alex41485522019-04-12 17:26:18 -050069 _act_classes = {
70 const.ACT_UPGRADE: "possible",
71 const.ACT_NEED_UP: "needs_up",
72 const.ACT_NEED_DOWN: "needs_down",
73 const.ACT_REPO: "needs_repo",
74 const.ACT_NA: ""
75 }
76 return _act_classes[act]
77
78
Alex836fac82019-08-22 13:36:16 -050079def make_pkg_status_label(sts):
Alex41485522019-04-12 17:26:18 -050080 _status_labels = {
81 const.VERSION_OK: "OK",
82 const.VERSION_UP: "Upgraded",
83 const.VERSION_DOWN: "Downgraded",
Alex26b8a8c2019-10-09 17:09:07 -050084 const.VERSION_WARN: "WARNING",
Alex41485522019-04-12 17:26:18 -050085 const.VERSION_ERR: "ERROR",
86 const.VERSION_NA: "N/A"
87 }
88 return _status_labels[sts]
89
90
Alex836fac82019-08-22 13:36:16 -050091def make_pkg_status_class(sts):
92 return const.all_pkg_statuses[sts]
93
94
95def make_node_status(sts):
96 return const.node_status[sts]
Alex41485522019-04-12 17:26:18 -050097
98
Alexd0391d42019-05-21 18:48:55 -050099def make_repo_info(repos):
100 _text = ""
101 for r in repos:
102 # tag
103 _text += r['tag'] + ": "
104 # repo header
105 _text += " ".join([
106 r['subset'],
107 r['release'],
108 r['ubuntu-release'],
109 r['type'],
110 r['arch']
111 ]) + ", "
112 # maintainer w/o email
Alex3bc95f62020-03-05 17:00:04 -0600113 _text += ascii(r['maintainer'][:r['maintainer'].find('<')-1])
Alexd0391d42019-05-21 18:48:55 -0500114 # newline
115 _text += "<br />"
116 return _text
117
118
savex4448e132018-04-25 15:51:14 +0200119@six.add_metaclass(abc.ABCMeta)
120class _Base(object):
121 def __init__(self):
122 self.jinja2_env = self.init_jinja2_env()
123
124 @abc.abstractmethod
125 def __call__(self, payload):
126 pass
127
128 @staticmethod
129 def init_jinja2_env():
130 return jinja2.Environment(
131 loader=jinja2.FileSystemLoader(os.path.join(pkg_dir, 'templates')),
132 trim_blocks=True,
133 lstrip_blocks=True)
134
135
136class _TMPLBase(_Base):
137 @abc.abstractproperty
138 def tmpl(self):
139 pass
140
141 @staticmethod
142 def _count_totals(data):
143 data['counters']['total_nodes'] = len(data['nodes'])
144
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100145 def __call__(self, payload):
savex4448e132018-04-25 15:51:14 +0200146 # init data structures
147 data = self.common_data()
Alex41485522019-04-12 17:26:18 -0500148 # payload should have pre-sorted structure according to report called
149 # nodes, openstack_release, mcp_release, etc...
150 data.update(payload)
savex4448e132018-04-25 15:51:14 +0200151
152 # add template specific data
153 self._extend_data(data)
154
155 # do counts global
156 self._count_totals(data)
157
158 # specific filters
savex4448e132018-04-25 15:51:14 +0200159 self.jinja2_env.filters['linebreaks'] = line_breaks
Alex41485522019-04-12 17:26:18 -0500160 self.jinja2_env.filters['get_max'] = get_max
161
162 self.jinja2_env.filters['get_sorted_keys'] = get_sorted_keys
Alex836fac82019-08-22 13:36:16 -0500163 self.jinja2_env.filters['pkg_status_label'] = make_pkg_status_label
164 self.jinja2_env.filters['pkg_status_class'] = make_pkg_status_class
165 self.jinja2_env.filters['pkg_action_label'] = make_pkg_action_label
166 self.jinja2_env.filters['pkg_action_class'] = make_pkg_action_class
167 self.jinja2_env.filters['node_status_class'] = make_node_status
Alexc6566d82019-09-23 16:07:17 -0500168 self.jinja2_env.filters['pkg_repo_info'] = make_repo_info
savex4448e132018-04-25 15:51:14 +0200169
170 # render!
Alex41485522019-04-12 17:26:18 -0500171 logger_cli.info("-> Using template: {}".format(self.tmpl))
savex4448e132018-04-25 15:51:14 +0200172 tmpl = self.jinja2_env.get_template(self.tmpl)
Alex41485522019-04-12 17:26:18 -0500173 logger_cli.info("-> Rendering")
savex4448e132018-04-25 15:51:14 +0200174 return tmpl.render(data)
175
176 def common_data(self):
177 return {
178 'counters': {},
Alex41485522019-04-12 17:26:18 -0500179 'salt_info': {},
180 'gen_date': time.strftime("%m/%d/%Y %H:%M:%S")
savex4448e132018-04-25 15:51:14 +0200181 }
182
183 def _extend_data(self, data):
184 pass
185
186
Alex41485522019-04-12 17:26:18 -0500187# HTML Package versions report
188class CSVAllPackages(_TMPLBase):
189 tmpl = "pkg_versions_csv.j2"
190
191
192# HTML Package versions report
savexce010ba2018-04-27 09:49:23 +0200193class HTMLPackageCandidates(_TMPLBase):
Alex41485522019-04-12 17:26:18 -0500194 tmpl = "pkg_versions_html.j2"
savex4448e132018-04-25 15:51:14 +0200195
196
Alex Savatieievd48994d2018-12-13 12:13:00 +0100197# Package versions report
198class HTMLModelCompare(_TMPLBase):
199 tmpl = "model_tree_cmp_tmpl.j2"
200
201 def _extend_data(self, data):
Alex Savatieiev36b938d2019-01-21 11:01:18 +0100202 # move names into separate place
Alexb8af13a2019-04-16 18:38:12 -0500203 data["names"] = data["diffs"].pop("diff_names")
204 data["tabs"] = data.pop("diffs")
Alex3ebc5632019-04-18 16:47:18 -0500205
Alex Savatieievd48994d2018-12-13 12:13:00 +0100206 # counters - mdl_diff
Alex Savatieiev4f149d02019-02-28 17:15:29 -0600207 for _tab in data["tabs"].keys():
208 data['counters'][_tab] = len(data["tabs"][_tab]["diffs"].keys())
Alex Savatieievd48994d2018-12-13 12:13:00 +0100209
210
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600211class HTMLNetworkReport(_TMPLBase):
212 tmpl = "network_check_tmpl.j2"
213
Alex836fac82019-08-22 13:36:16 -0500214 def _extend_data(self, data):
215 def get_bytes(value):
Alex3bc95f62020-03-05 17:00:04 -0600216 _char = value[-1]
217 _ord = ord(_char)
218 if _ord > 47 and _ord < 58:
219 # bytes comes with no Char
Alex836fac82019-08-22 13:36:16 -0500220 return int(value)
Alex3bc95f62020-03-05 17:00:04 -0600221 else:
222 _sizes = ["*", "K", "M", "G", "T"]
223 _flo = float(value[:-1])
224 _pwr = 1
225 if _char in _sizes:
226 _pwr = _sizes.index(_char)
227 return int(_flo**_pwr)
Alex836fac82019-08-22 13:36:16 -0500228
Alex2e213b22019-12-05 10:40:29 -0600229 def _dmidecode(_dict, type=0):
Alex3bc95f62020-03-05 17:00:04 -0600230 # _key = "dmi"
Alex2e213b22019-12-05 10:40:29 -0600231 _key_r = "dmi_r"
232 _f_cmd = salt_master.get_cmd_for_nodes
233 _cmd = "dmidecode -t {}".format(type)
234 _f_cmd(_cmd, _key_r, target_dict=_dict)
235 # TODO: parse BIOS output or given type
236 pass
237
238 def _lsblk(_dict):
Alex3bc95f62020-03-05 17:00:04 -0600239 # _key = "lsblk"
Alex2e213b22019-12-05 10:40:29 -0600240 _key_r = "lsblk_raw"
241 _f_cmd = salt_master.get_cmd_for_nodes
242 _columns = [
243 "NAME",
244 "HCTL",
245 "TYPE",
246 "SIZE",
247 "VENDOR",
248 "MODEL",
249 "SERIAL",
250 "REV",
251 "TRAN"
252 ]
253 _cmd = "lsblk -S --output {}".format(",".join(_columns))
254 _f_cmd(_cmd, _key_r, target_dict=_dict)
255 # TODO: parse lsblk output
256 pass
257
Alex1839bbf2019-08-22 17:17:21 -0500258 def _lscpu(_dict):
259 _key = "lscpu"
260 _key_r = "lscpu_raw"
261 # get all of the values
Alex836fac82019-08-22 13:36:16 -0500262 _f_cmd = salt_master.get_cmd_for_nodes
Alex1839bbf2019-08-22 17:17:21 -0500263 _cmd = "lscpu | sed -n '/\\:/s/ \\+/ /gp'"
264 _f_cmd(_cmd, _key_r, target_dict=_dict)
265 # parse them and put into dict
Alex3bc95f62020-03-05 17:00:04 -0600266 for node, dt in _dict.items():
Alex1839bbf2019-08-22 17:17:21 -0500267 dt[_key] = {}
268 if dt['status'] == DOWN:
Alexe65ff4e2019-10-11 14:45:09 -0500269 continue
270 if not dt[_key_r]:
271 # no stats collected, put negatives
272 dt.pop(_key_r)
Alex1839bbf2019-08-22 17:17:21 -0500273 continue
274 lines = dt[_key_r].splitlines()
275 for line in lines:
276 li = line.split(':')
277 _var_name = li[0].lower()
278 _var_name = re.sub(' ', '_', _var_name)
279 _var_name = re.sub('|'.join(['\\(', '\\)']), '', _var_name)
280 _var_value = li[1].strip()
281 dt[_key][_var_name] = _var_value
282 dt.pop(_key_r)
283 # detect virtual nodes
284 if "hypervisor_vendor" in dt[_key]:
285 dt['node_type'] = "virtual"
286 else:
287 dt['node_type'] = "physical"
Alex836fac82019-08-22 13:36:16 -0500288
Alex1839bbf2019-08-22 17:17:21 -0500289 def _free(_dict):
290 _key = "ram"
291 _key_r = "ram_raw"
Alex836fac82019-08-22 13:36:16 -0500292 _f_cmd = salt_master.get_cmd_for_nodes
Alex1839bbf2019-08-22 17:17:21 -0500293 _cmd = "free -h | sed -n '/Mem/s/ \\+/ /gp'"
294 _f_cmd(_cmd, _key_r, target_dict=_dict)
295 # parse them and put into dict
Alex3bc95f62020-03-05 17:00:04 -0600296 for node, dt in _dict.items():
Alex1839bbf2019-08-22 17:17:21 -0500297 dt[_key] = {}
298 if dt['status'] == DOWN:
Alexe65ff4e2019-10-11 14:45:09 -0500299 continue
300 if not dt[_key_r]:
301 # no stats collected, put negatives
302 dt.pop(_key_r)
Alex1839bbf2019-08-22 17:17:21 -0500303 continue
304 li = dt[_key_r].split()
305 dt[_key]['total'] = li[1]
306 dt[_key]['used'] = li[2]
307 dt[_key]['free'] = li[3]
308 dt[_key]['shared'] = li[4]
309 dt[_key]['cache'] = li[5]
310 dt[_key]['available'] = li[6]
311
312 _total = get_bytes(li[1])
313 _avail = get_bytes(li[6])
314 _m = _avail * 100.0 / _total
315 if _m < _ram_critical:
316 dt[_key]["status"] = "fail"
317 elif _m < _ram_warn:
318 dt[_key]["status"] = "warn"
319 else:
320 dt[_key]["status"] = ""
Alex836fac82019-08-22 13:36:16 -0500321
322 def _services(_dict):
323 _key = "services"
324 _key_r = "services_raw"
325 _f_cmd = salt_master.get_cmd_for_nodes
326 _cmd = "service --status-all"
327 _f_cmd(_cmd, _key_r, target_dict=_dict)
Alex3bc95f62020-03-05 17:00:04 -0600328 for node, dt in _dict.items():
Alex836fac82019-08-22 13:36:16 -0500329 dt[_key] = {}
Alex1839bbf2019-08-22 17:17:21 -0500330 if dt['status'] == DOWN:
Alexe65ff4e2019-10-11 14:45:09 -0500331 continue
332 if not dt[_key_r]:
333 # no stats collected, put negatives
334 dt.pop(_key_r)
Alex1839bbf2019-08-22 17:17:21 -0500335 continue
Alex836fac82019-08-22 13:36:16 -0500336 lines = dt[_key_r].splitlines()
337 for line in lines:
338 li = line.split()
339 _status = li[1]
340 _name = li[3]
341 if _status == '-':
342 dt[_key][_name] = False
343 elif _status == '+':
344 dt[_key][_name] = True
345 else:
346 dt[_key][_name] = None
347 dt.pop(_key_r)
348
Alex1839bbf2019-08-22 17:17:21 -0500349 def _vcp_status(_dict):
350 _key = "virsh"
351 _key_r = "virsh_raw"
352 salt_master.get_cmd_for_nodes(
353 "virsh list --all | sed -n -e '/[0-9]/s/ \\+/ /gp'",
354 _key_r,
355 target_dict=_dict,
356 nodes="kvm*"
357 )
358 _kvm = filter(lambda x: x.find("kvm") >= 0, _dict.keys())
359 for node in _kvm:
360 dt = _dict[node]
361 dt[_key] = {}
362 if dt['status'] == DOWN:
Alexe65ff4e2019-10-11 14:45:09 -0500363 continue
364 if not dt[_key_r]:
365 # no stats collected, put negatives
366 dt.pop(_key_r)
Alex1839bbf2019-08-22 17:17:21 -0500367 continue
368 lines = dt[_key_r].splitlines()
369 for line in lines:
370 li = line.split()
371 _id = li[0]
372 _name = li[1]
373 _status = li[2]
374 dt[_key][_name] = {
375 'id': _id,
376 'status': _status
377 }
378 dt.pop(_key_r)
379
380 # query per-cpu and count totals
381 # total (0), dropped(1), squeezed (2), collision (7)
382 def _soft_net_stats(_dict):
383 _key = "net_stats"
384 _key_r = "net_stats_raw"
385 _f_cmd = salt_master.get_cmd_for_nodes
386 _cmd = "cat /proc/net/softnet_stat; echo \\#; " \
387 "sleep {}; cat /proc/net/softnet_stat".format(
388 _softnet_interval
389 )
390 _f_cmd(_cmd, _key_r, target_dict=_dict)
Alex3bc95f62020-03-05 17:00:04 -0600391 for node, dt in _dict.items():
Alex1839bbf2019-08-22 17:17:21 -0500392 _cpuindex = 1
393 _add_mode = True
Alex1839bbf2019-08-22 17:17:21 -0500394 # totals for start mark
395 _ts = [0, 0, 0, 0]
396 # skip if node is down
397 if dt['status'] == DOWN:
Alexc96fdd32019-10-15 12:48:59 -0500398 dt[_key] = {
399 "total": [-1, -1, -1, -1]
400 }
Alex1839bbf2019-08-22 17:17:21 -0500401 continue
Alexf3dbe862019-10-07 15:17:04 -0500402 if not dt[_key_r]:
403 # no stats collected, put negatives
404 dt.pop(_key_r)
405 dt[_key] = {
406 "total": [-1, -1, -1, -1]
407 }
408 continue
409 # final totals
410 dt[_key] = {
411 "total": [0, 0, 0, 0]
412 }
Alex1839bbf2019-08-22 17:17:21 -0500413 lines = dt[_key_r].splitlines()
414 for line in lines:
415 if line.startswith("#"):
416 _add_mode = False
417 _cpuindex = 1
418 continue
419 li = line.split()
420 _c = [
421 int(li[0], 16),
422 int(li[1], 16),
423 int(li[2], 16),
424 int(li[7], 16)
425 ]
426 _id = "cpu{:02}".format(_cpuindex)
427 if _id not in dt[_key]:
428 dt[_key][_id] = []
429 _dc = dt[_key][_id]
430 if _add_mode:
431 # saving values and adding totals
432 dt[_key][_id] = _c
433 # save start totals
434 _ts = [_ts[i]+_c[i] for i in range(0, len(_c))]
435 else:
436 # this is second measurement
437 # subtract all values
438 for i in range(len(_c)):
439 dt[_key][_id][i] = _c[i] - _dc[i]
440 dt[_key]["total"][i] += _c[i]
441 _cpuindex += 1
442 # finally, subtract initial totals
Alex3bc95f62020-03-05 17:00:04 -0600443 for k, v in dt[_key].items():
Alex1839bbf2019-08-22 17:17:21 -0500444 if k != "total":
445 dt[_key][k] = [v[i] / 5. for i in range(len(v))]
446 else:
447 dt[_key][k] = [(v[i]-_ts[i])/5. for i in range(len(v))]
448 dt.pop(_key_r)
449
450 # prepare yellow and red marker values
Alex836fac82019-08-22 13:36:16 -0500451 data["const"] = {
Alex1839bbf2019-08-22 17:17:21 -0500452 "net_interval": _softnet_interval,
Alex836fac82019-08-22 13:36:16 -0500453 "ram_warn": _ram_warn,
454 "ram_critical": _ram_critical,
455 "disk_warn": _disk_warn,
Alex1839bbf2019-08-22 17:17:21 -0500456 "disk_critical": _disk_critical,
457 "services": read_file_as_lines(
458 os.path.join(
459 pkg_dir,
460 'etc',
461 'services.list'
462 )
463 )
Alex836fac82019-08-22 13:36:16 -0500464 }
465
466 # get kernel version
467 salt_master.get_cmd_for_nodes(
468 "uname -r",
469 "kernel",
470 target_dict=data["nodes"]
471 )
Alex1839bbf2019-08-22 17:17:21 -0500472 # process lscpu data
473 _lscpu(data["nodes"])
Alex836fac82019-08-22 13:36:16 -0500474
475 # free ram
476 # sample: 16425392 14883144 220196
Alex1839bbf2019-08-22 17:17:21 -0500477 _free(data["nodes"])
Alex836fac82019-08-22 13:36:16 -0500478
479 # disk space
480 # sample: /dev/vda1 78G 33G 45G 43%
Alexe65ff4e2019-10-11 14:45:09 -0500481 _key = "disk"
482 _key_r = "disk_raw"
Alex836fac82019-08-22 13:36:16 -0500483 salt_master.get_cmd_for_nodes(
484 "df -h | sed -n '/^\\/dev/s/ \\+/ /gp' | cut -d\" \" -f 1-5",
485 "disk_raw",
486 target_dict=data["nodes"]
487 )
Alex3bc95f62020-03-05 17:00:04 -0600488 for dt in data["nodes"].values():
Alexc96fdd32019-10-15 12:48:59 -0500489 dt["disk"] = {}
490 dt["disk_max_dev"] = None
Alexe65ff4e2019-10-11 14:45:09 -0500491 if dt['status'] == DOWN:
Alexc96fdd32019-10-15 12:48:59 -0500492 dt["disk"]["unknown"] = {}
493 dt["disk_max_dev"] = "unknown"
Alexe65ff4e2019-10-11 14:45:09 -0500494 continue
495 if not dt[_key_r]:
496 # no stats collected, put negatives
497 dt.pop(_key_r)
498 dt[_key] = {}
499 continue
Alex836fac82019-08-22 13:36:16 -0500500 # show first device row by default
Alexe65ff4e2019-10-11 14:45:09 -0500501 _d = dt["disk"]
502 _r = dt["disk_raw"]
Alex836fac82019-08-22 13:36:16 -0500503 _r = _r.splitlines()
504 _max = -1
505 for idx in range(0, len(_r)):
506 _t = _r[idx].split()
507 _d[_t[0]] = {}
508 _d[_t[0]]['v'] = _t[1:]
509 _chk = int(_t[-1].split('%')[0])
510 if _chk > _max:
Alexe65ff4e2019-10-11 14:45:09 -0500511 dt["disk_max_dev"] = _t[0]
Alex836fac82019-08-22 13:36:16 -0500512 _max = _chk
513 if _chk > _disk_critical:
514 _d[_t[0]]['f'] = "fail"
515 elif _chk > _disk_warn:
516 _d[_t[0]]['f'] = "warn"
517 else:
518 _d[_t[0]]['f'] = ""
519
520 # prepare networks data for report
Alex3bc95f62020-03-05 17:00:04 -0600521 for net, net_v in data['map'].items():
522 for node, ifs in net_v.items():
Alex836fac82019-08-22 13:36:16 -0500523 for d in ifs:
524 _err = "fail"
525 d['interface_error'] = _err if d['interface_error'] else ""
526 d['mtu_error'] = _err if d['mtu_error'] else ""
527 d['status_error'] = _err if d['status_error'] else ""
528 d['subnet_gateway_error'] = \
529 _err if d['subnet_gateway_error'] else ""
530
531 _services(data["nodes"])
Alex1839bbf2019-08-22 17:17:21 -0500532 # vcp status
533 # query virsh and prepare for report
534 _vcp_status(data["nodes"])
535
536 # soft net stats
537 _soft_net_stats(data["nodes"])
Alex836fac82019-08-22 13:36:16 -0500538
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600539
savex4448e132018-04-25 15:51:14 +0200540class ReportToFile(object):
541 def __init__(self, report, target):
542 self.report = report
543 self.target = target
544
545 def __call__(self, payload):
546 payload = self.report(payload)
547
548 if isinstance(self.target, six.string_types):
549 self._wrapped_dump(payload)
550 else:
551 self._dump(payload, self.target)
552
553 def _wrapped_dump(self, payload):
554 with open(self.target, 'wt') as target:
555 self._dump(payload, target)
556
557 @staticmethod
558 def _dump(payload, target):
559 target.write(payload)