blob: 6940aaf8dc24ff226ca71f9838066551636249c2 [file] [log] [blame]
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +03001#!/usr/bin/env python3
2
3import re
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +03004import os
5import sys
6import copy
7import os.path
8import argparse
9import itertools
koder aka kdanilov70227062016-11-26 23:23:21 +020010from typing import Optional, Iterator, Union, Dict, Iterable, List, TypeVar, Callable, Tuple, NamedTuple, Any
11from collections import OrderedDict
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030012
13
koder aka kdanilov7f59d562016-12-26 01:34:23 +020014from ...result_classes import IStorable
koder aka kdanilovf2865172016-12-30 03:35:11 +020015from ...result_classes import TestJobConfig
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030016from ...utils import sec_to_str, ssize2b
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030017
18
19SECTION = 0
20SETTING = 1
21INCLUDE = 2
22
23
koder aka kdanilov70227062016-11-26 23:23:21 +020024Var = NamedTuple('Var', [('name', str)])
25CfgLine = NamedTuple('CfgLine',
26 [('fname', str),
27 ('lineno', int),
28 ('oline', str),
29 ('tp', int),
30 ('name', str),
31 ('val', Any)])
32
33TestSumm = NamedTuple("TestSumm",
34 [("oper", str),
35 ("mode", str),
36 ("bsize", int),
37 ("iodepth", int),
38 ("vm_count", int)])
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030039
40
koder aka kdanilovf2865172016-12-30 03:35:11 +020041class FioJobConfig(TestJobConfig):
koder aka kdanilov70227062016-11-26 23:23:21 +020042 def __init__(self, name: str) -> None:
koder aka kdanilovf2865172016-12-30 03:35:11 +020043 TestJobConfig.__init__(self)
koder aka kdanilov70227062016-11-26 23:23:21 +020044 self.vals = OrderedDict() # type: Dict[str, Any]
koder aka kdanilovf2865172016-12-30 03:35:11 +020045 self.name = name
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030046
koder aka kdanilovf2865172016-12-30 03:35:11 +020047 def __eq__(self, other: 'FioJobConfig') -> bool:
48 return self.vals == other.vals
49
50 def copy(self) -> 'FioJobConfig':
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030051 return copy.deepcopy(self)
52
koder aka kdanilov22d134e2016-11-08 11:33:19 +020053 def required_vars(self) -> Iterator[Tuple[str, Var]]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030054 for name, val in self.vals.items():
55 if isinstance(val, Var):
56 yield name, val
57
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030058 def is_free(self) -> bool:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030059 return len(list(self.required_vars())) == 0
60
koder aka kdanilov70227062016-11-26 23:23:21 +020061 def __str__(self) -> str:
koder aka kdanilovf2865172016-12-30 03:35:11 +020062 res = "[{0}]\n".format(self.summary)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030063
64 for name, val in self.vals.items():
65 if name.startswith('_') or name == name.upper():
66 continue
67 if isinstance(val, Var):
68 res += "{0}={{{1}}}\n".format(name, val.name)
69 else:
70 res += "{0}={1}\n".format(name, val)
71
72 return res
73
koder aka kdanilovf2865172016-12-30 03:35:11 +020074 def __repr__(self) -> str:
75 return str(self)
76
koder aka kdanilov7f59d562016-12-26 01:34:23 +020077 def raw(self) -> Dict[str, Any]:
78 return {
koder aka kdanilovf2865172016-12-30 03:35:11 +020079 'vals': [[key, val] for key, val in self.vals.items()],
80 'summary': self.summary,
81 'name': self.name
koder aka kdanilov7f59d562016-12-26 01:34:23 +020082 }
83
84 @classmethod
koder aka kdanilovf2865172016-12-30 03:35:11 +020085 def fromraw(cls, data: Dict[str, Any]) -> 'FioJobConfig':
koder aka kdanilov7f59d562016-12-26 01:34:23 +020086 obj = cls(data['name'])
87 obj.summary = data['summary']
88 obj.vals.update(data['vals'])
89 return obj
90
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030091
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030092class ParseError(ValueError):
koder aka kdanilov70227062016-11-26 23:23:21 +020093 def __init__(self, msg: str, fname: str, lineno: int, line_cont:Optional[str] = "") -> None:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030094 ValueError.__init__(self, msg)
95 self.file_name = fname
96 self.lineno = lineno
97 self.line_cont = line_cont
98
koder aka kdanilov70227062016-11-26 23:23:21 +020099 def __str__(self) -> str:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300100 msg = "In {0}:{1} ({2}) : {3}"
101 return msg.format(self.file_name,
102 self.lineno,
103 self.line_cont,
104 super(ParseError, self).__str__())
105
106
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300107def is_name(name: str) -> bool:
koder aka kdanilov70227062016-11-26 23:23:21 +0200108 return re.match("[a-zA-Z_][a-zA-Z_0-9]*", name) is not None
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300109
110
koder aka kdanilov70227062016-11-26 23:23:21 +0200111def parse_value(val: str) -> Union[int, str, float, List, Var]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300112 try:
113 return int(val)
114 except ValueError:
115 pass
116
117 try:
118 return float(val)
119 except ValueError:
120 pass
121
122 if val.startswith('{%'):
123 assert val.endswith("%}")
124 content = val[2:-2]
125 vals = list(i.strip() for i in content.split(','))
koder aka kdanilov70227062016-11-26 23:23:21 +0200126 return list(map(parse_value, vals))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300127
128 if val.startswith('{'):
129 assert val.endswith("}")
130 assert is_name(val[1:-1])
131 return Var(val[1:-1])
koder aka kdanilov70227062016-11-26 23:23:21 +0200132
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300133 return val
134
135
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200136def fio_config_lexer(fio_cfg: str, fname: str) -> Iterator[CfgLine]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300137 for lineno, oline in enumerate(fio_cfg.split("\n")):
138 try:
139 line = oline.strip()
140
141 if line.startswith("#") or line.startswith(";"):
142 continue
143
144 if line == "":
145 continue
146
147 if '#' in line:
148 raise ParseError("# isn't allowed inside line",
149 fname, lineno, oline)
150
151 if line.startswith('['):
152 yield CfgLine(fname, lineno, oline, SECTION,
153 line[1:-1].strip(), None)
154 elif '=' in line:
155 opt_name, opt_val = line.split('=', 1)
156 yield CfgLine(fname, lineno, oline, SETTING,
157 opt_name.strip(),
158 parse_value(opt_val.strip()))
159 elif line.startswith("include "):
160 yield CfgLine(fname, lineno, oline, INCLUDE,
161 line.split(" ", 1)[1], None)
162 else:
163 yield CfgLine(fname, lineno, oline, SETTING, line, '1')
164
165 except Exception as exc:
166 raise ParseError(str(exc), fname, lineno, oline)
167
168
koder aka kdanilovf2865172016-12-30 03:35:11 +0200169def fio_config_parse(lexer_iter: Iterable[CfgLine]) -> Iterator[FioJobConfig]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300170 in_globals = False
171 curr_section = None
koder aka kdanilov70227062016-11-26 23:23:21 +0200172 glob_vals = OrderedDict() # type: Dict[str, Any]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300173 sections_count = 0
174
koder aka kdanilov70227062016-11-26 23:23:21 +0200175 lexed_lines = list(lexer_iter) # type: List[CfgLine]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300176 one_more = True
177 includes = {}
178
179 while one_more:
koder aka kdanilov70227062016-11-26 23:23:21 +0200180 new_lines = [] # type: List[CfgLine]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300181 one_more = False
182 for line in lexed_lines:
183 fname, lineno, oline, tp, name, val = line
184
185 if INCLUDE == tp:
186 if not os.path.exists(fname):
187 dirname = '.'
188 else:
189 dirname = os.path.dirname(fname)
190
191 new_fname = os.path.join(dirname, name)
192 includes[new_fname] = (fname, lineno)
193
194 try:
195 cont = open(new_fname).read()
196 except IOError as err:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200197 raise ParseError("Error while including file {}: {}".format(new_fname, err), fname, lineno, oline)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300198
199 new_lines.extend(fio_config_lexer(cont, new_fname))
200 one_more = True
201 else:
202 new_lines.append(line)
203
204 lexed_lines = new_lines
205
206 for fname, lineno, oline, tp, name, val in lexed_lines:
207 if tp == SECTION:
208 if curr_section is not None:
209 yield curr_section
210 curr_section = None
211
212 if name == 'global':
213 if sections_count != 0:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200214 raise ParseError("[global] section should be only one and first", fname, lineno, oline)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300215 in_globals = True
216 else:
217 in_globals = False
koder aka kdanilovf2865172016-12-30 03:35:11 +0200218 curr_section = FioJobConfig(name)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300219 curr_section.vals = glob_vals.copy()
220 sections_count += 1
221 else:
222 assert tp == SETTING
223 if in_globals:
224 glob_vals[name] = val
225 elif name == name.upper():
koder aka kdanilovf2865172016-12-30 03:35:11 +0200226 raise ParseError("Param {!r} not in [global] section".format(name), fname, lineno, oline)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300227 elif curr_section is None:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200228 raise ParseError("Data outside section", fname, lineno, oline)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300229 else:
230 curr_section.vals[name] = val
231
232 if curr_section is not None:
233 yield curr_section
234
235
koder aka kdanilovf2865172016-12-30 03:35:11 +0200236def process_cycles(sec: FioJobConfig) -> Iterator[FioJobConfig]:
koder aka kdanilov70227062016-11-26 23:23:21 +0200237 cycles = OrderedDict() # type: Dict[str, Any]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300238
239 for name, val in sec.vals.items():
240 if isinstance(val, list) and name.upper() != name:
241 cycles[name] = val
242
243 if len(cycles) == 0:
244 yield sec
245 else:
koder aka kdanilov70227062016-11-26 23:23:21 +0200246 # iodepth should changes faster
247 numjobs = cycles.pop('iodepth', None)
248 items = list(cycles.items())
koder aka kdanilov170936a2015-06-27 22:51:17 +0300249
koder aka kdanilov70227062016-11-26 23:23:21 +0200250 if items:
koder aka kdanilov170936a2015-06-27 22:51:17 +0300251 keys, vals = zip(*items)
252 keys = list(keys)
253 vals = list(vals)
254 else:
255 keys = []
256 vals = []
257
258 if numjobs is not None:
259 vals.append(numjobs)
koder aka kdanilov70227062016-11-26 23:23:21 +0200260 keys.append('iodepth')
koder aka kdanilov170936a2015-06-27 22:51:17 +0300261
262 for combination in itertools.product(*vals):
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300263 new_sec = sec.copy()
koder aka kdanilov170936a2015-06-27 22:51:17 +0300264 new_sec.vals.update(zip(keys, combination))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300265 yield new_sec
266
267
koder aka kdanilov70227062016-11-26 23:23:21 +0200268FioParamsVal = Union[str, Var]
269FioParams = Dict[str, FioParamsVal]
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300270
271
koder aka kdanilovf2865172016-12-30 03:35:11 +0200272def apply_params(sec: FioJobConfig, params: FioParams) -> FioJobConfig:
koder aka kdanilov70227062016-11-26 23:23:21 +0200273 processed_vals = OrderedDict() # type: Dict[str, Any]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300274 processed_vals.update(params)
275 for name, val in sec.vals.items():
276 if name in params:
277 continue
278
279 if isinstance(val, Var):
280 if val.name in params:
281 val = params[val.name]
282 elif val.name in processed_vals:
283 val = processed_vals[val.name]
284 processed_vals[name] = val
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300285
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300286 sec = sec.copy()
287 sec.vals = processed_vals
288 return sec
289
290
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300291def abbv_name_to_full(name: str) -> str:
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300292 assert len(name) == 3
293
294 smode = {
295 'a': 'async',
296 's': 'sync',
297 'd': 'direct',
298 'x': 'sync direct'
299 }
300 off_mode = {'s': 'sequential', 'r': 'random'}
koder aka kdanilov7248c7b2015-05-31 22:53:03 +0300301 oper = {'r': 'read', 'w': 'write', 'm': 'mixed'}
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300302 return smode[name[2]] + " " + \
303 off_mode[name[0]] + " " + oper[name[1]]
304
305
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300306MAGIC_OFFSET = 0.1885
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300307
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300308
koder aka kdanilovf2865172016-12-30 03:35:11 +0200309def final_process(sec: FioJobConfig, counter: List[int] = [0]) -> FioJobConfig:
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300310 sec = sec.copy()
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300311
312 sec.vals['unified_rw_reporting'] = '1'
313
koder aka kdanilovbb6d6cd2015-06-20 02:55:07 +0300314 if isinstance(sec.vals['size'], Var):
315 raise ValueError("Variable {0} isn't provided".format(
316 sec.vals['size'].name))
317
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300318 sz = ssize2b(sec.vals['size'])
319 offset = sz * ((MAGIC_OFFSET * counter[0]) % 1.0)
320 offset = int(offset) // 1024 ** 2
321 new_vars = {'UNIQ_OFFSET': str(offset) + "m"}
322
323 for name, val in sec.vals.items():
324 if isinstance(val, Var):
325 if val.name in new_vars:
326 sec.vals[name] = new_vars[val.name]
327
koder aka kdanilovbb6d6cd2015-06-20 02:55:07 +0300328 for vl in sec.vals.values():
329 if isinstance(vl, Var):
330 raise ValueError("Variable {0} isn't provided".format(vl.name))
331
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300332 params = sec.vals.copy()
333 params['UNIQ'] = 'UN{0}'.format(counter[0])
334 params['COUNTER'] = str(counter[0])
335 params['TEST_SUMM'] = get_test_summary(sec)
336 sec.name = sec.name.format(**params)
337 counter[0] += 1
338
339 return sec
340
341
koder aka kdanilovf2865172016-12-30 03:35:11 +0200342def get_test_sync_mode(sec: FioJobConfig) -> str:
koder aka kdanilovbc2c8982015-06-13 02:50:43 +0300343 if isinstance(sec, dict):
344 vals = sec
345 else:
346 vals = sec.vals
347
348 is_sync = str(vals.get("sync", "0")) == "1"
349 is_direct = str(vals.get("direct", "0")) == "1"
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300350
351 if is_sync and is_direct:
352 return 'x'
353 elif is_sync:
354 return 's'
355 elif is_direct:
356 return 'd'
357 else:
358 return 'a'
359
360
koder aka kdanilovf2865172016-12-30 03:35:11 +0200361def get_test_summary_tuple(sec: FioJobConfig, vm_count: int = None) -> TestSumm:
koder aka kdanilovbc2c8982015-06-13 02:50:43 +0300362 if isinstance(sec, dict):
363 vals = sec
364 else:
365 vals = sec.vals
366
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300367 rw = {"randread": "rr",
368 "randwrite": "rw",
369 "read": "sr",
koder aka kdanilov7248c7b2015-05-31 22:53:03 +0300370 "write": "sw",
371 "randrw": "rm",
372 "rw": "sm",
koder aka kdanilovbc2c8982015-06-13 02:50:43 +0300373 "readwrite": "sm"}[vals["rw"]]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300374
375 sync_mode = get_test_sync_mode(sec)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300376
koder aka kdanilov170936a2015-06-27 22:51:17 +0300377 return TestSumm(rw,
378 sync_mode,
379 vals['blocksize'],
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +0200380 vals.get('iodepth', '1'),
koder aka kdanilov170936a2015-06-27 22:51:17 +0300381 vm_count)
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300382
383
koder aka kdanilovf2865172016-12-30 03:35:11 +0200384def get_test_summary(sec: FioJobConfig, vm_count: int = None, noiodepth: bool = False) -> str:
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300385 tpl = get_test_summary_tuple(sec, vm_count)
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300386
387 res = "{0.oper}{0.mode}{0.bsize}".format(tpl)
koder aka kdanilov70227062016-11-26 23:23:21 +0200388 if not noiodepth:
389 res += "qd{}".format(tpl.iodepth)
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300390
391 if tpl.vm_count is not None:
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300392 res += "vm{}".format(tpl.vm_count)
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300393
394 return res
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300395
396
koder aka kdanilovf2865172016-12-30 03:35:11 +0200397def execution_time(sec: FioJobConfig) -> int:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300398 return sec.vals.get('ramp_time', 0) + sec.vals.get('runtime', 0)
399
400
koder aka kdanilovf2865172016-12-30 03:35:11 +0200401def parse_all_in_1(source:str, fname: str = None) -> Iterator[FioJobConfig]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300402 return fio_config_parse(fio_config_lexer(source, fname))
403
404
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300405FM_FUNC_INPUT = TypeVar("FM_FUNC_INPUT")
406FM_FUNC_RES = TypeVar("FM_FUNC_RES")
407
408
409def flatmap(func: Callable[[FM_FUNC_INPUT], Iterable[FM_FUNC_RES]],
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200410 inp_iter: Iterable[FM_FUNC_INPUT]) -> Iterator[FM_FUNC_RES]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300411 for val in inp_iter:
412 for res in func(val):
413 yield res
414
415
koder aka kdanilovf2865172016-12-30 03:35:11 +0200416def get_log_files(sec: FioJobConfig) -> List[Tuple[str, str]]:
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200417 res = [] # type: List[Tuple[str, str]]
418 for key, name in (('write_iops_log', 'iops'), ('write_bw_log', 'bw'), ('write_hist_log', 'lat')):
419 log = sec.vals.get(key)
420 if log is not None:
421 res.append((name, log))
422 return res
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +0200423
424
koder aka kdanilovf2865172016-12-30 03:35:11 +0200425def fio_cfg_compile(source: str, fname: str, test_params: FioParams) -> Iterator[FioJobConfig]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300426 it = parse_all_in_1(source, fname)
427 it = (apply_params(sec, test_params) for sec in it)
428 it = flatmap(process_cycles, it)
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200429 for sec in map(final_process, it):
430 sec.summary = get_test_summary(sec)
431 yield sec
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300432
433
434def parse_args(argv):
435 parser = argparse.ArgumentParser(
436 description="Run fio' and return result")
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300437 parser.add_argument("-p", "--params", nargs="*", metavar="PARAM=VAL",
438 default=[],
439 help="Provide set of pairs PARAM=VAL to" +
440 "format into job description")
441 parser.add_argument("action", choices=['estimate', 'compile', 'num_tests'])
442 parser.add_argument("jobfile")
443 return parser.parse_args(argv)
444
445
446def main(argv):
447 argv_obj = parse_args(argv)
448
449 if argv_obj.jobfile == '-':
450 job_cfg = sys.stdin.read()
451 else:
452 job_cfg = open(argv_obj.jobfile).read()
453
454 params = {}
455 for param_val in argv_obj.params:
456 assert '=' in param_val
457 name, val = param_val.split("=", 1)
458 params[name] = parse_value(val)
459
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300460 sec_it = fio_cfg_compile(job_cfg, argv_obj.jobfile, params)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300461
462 if argv_obj.action == 'estimate':
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300463 print(sec_to_str(sum(map(execution_time, sec_it))))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300464 elif argv_obj.action == 'num_tests':
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300465 print(sum(map(len, map(list, sec_it))))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300466 elif argv_obj.action == 'compile':
467 splitter = "\n#" + "-" * 70 + "\n\n"
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300468 print(splitter.join(map(str, sec_it)))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300469
470 return 0
471
472
473if __name__ == '__main__':
474 exit(main(sys.argv[1:]))