glusterfs
859 строк · 25.8 Кб
1#!/usr/bin/python3
2
3from __future__ import absolute_import
4from __future__ import division
5from __future__ import unicode_literals
6from __future__ import print_function
7import re
8import sys
9import fcntl
10import base64
11import threading
12import socket
13import os
14import shlex
15import argparse
16import subprocess
17import time
18import SimpleXMLRPCServer
19import xmlrpclib
20import md5
21import httplib
22import uuid
23
24DEFAULT_PORT = 9999
25TEST_TIMEOUT_S = 15 * 60
26CLIENT_CONNECT_TIMEOUT_S = 10
27CLIENT_TIMEOUT_S = 60
28PATCH_FILE_UID = str(uuid.uuid4())
29SSH_TIMEOUT_S = 10
30MAX_ATTEMPTS = 3
31ADDRESS_FAMILY = 'IPv4'
32
33
34def socket_instance(address_family):
35if address_family.upper() == 'ipv4'.upper():
36return socket.socket(socket.AF_INET, socket.SOCK_STREAM)
37elif address_family.upper() == 'ipv6'.upper():
38return socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
39else:
40Log.error("Invalid IP address family")
41sys.exit(1)
42
43
44def patch_file():
45return "/tmp/%s-patch.tar.gz" % PATCH_FILE_UID
46
47# ..............................................................................
48# SimpleXMLRPCServer IPvX Wrapper
49# ..............................................................................
50
51
52class GeneralXMLRPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer):
53def __init__(self, addr):
54SimpleXMLRPCServer.SimpleXMLRPCServer.__init__(self, addr)
55
56def server_bind(self):
57if self.socket:
58self.socket.close()
59self.socket = socket_instance(args.address_family)
60self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
61SimpleXMLRPCServer.SimpleXMLRPCServer.server_bind(self)
62
63
64class HTTPConnection(httplib.HTTPConnection):
65def __init__(self, host):
66self.host = host
67httplib.HTTPConnection.__init__(self, host)
68
69def connect(self):
70old_timeout = socket.getdefaulttimeout()
71self.sock = socket.create_connection((self.host, self.port),
72timeout=CLIENT_CONNECT_TIMEOUT_S)
73self.sock.settimeout(old_timeout)
74
75
76class IPTransport(xmlrpclib.Transport):
77def __init__(self, *args, **kwargs):
78xmlrpclib.Transport.__init__(self, *args, **kwargs)
79
80def make_connection(self, host):
81return HTTPConnection(host)
82
83
84# ..............................................................................
85# Common
86# ..............................................................................
87
88
89class Timer:
90def __init__(self):
91self.start = time.time()
92
93def elapsed_s(self):
94return int(time.time() - self.start)
95
96def reset(self):
97ret = self.elapsed_s()
98self.start = time.time()
99return ret
100
101
102def encode(buf):
103return base64.b16encode(buf)
104
105
106def decode(buf):
107return base64.b16decode(buf)
108
109
110def get_file_content(path):
111with open(path, "r") as f:
112return f.read()
113
114
115def write_to_file(path, data):
116with open(path, "w") as f:
117f.write(data)
118
119
120def failsafe(fn, args=()):
121try:
122return (True, fn(*args))
123except (xmlrpclib.Fault, xmlrpclib.ProtocolError, xmlrpclib.ResponseError,
124Exception) as err:
125Log.debug(str(err))
126return (False, None)
127
128
129class LogLevel:
130DEBUG = 2
131ERROR = 1
132CLI = 0
133
134
135class Log:
136LOGLEVEL = LogLevel.ERROR
137
138@staticmethod
139def _normalize(msg):
140return msg[:100]
141
142@staticmethod
143def debug(msg):
144if Log.LOGLEVEL >= LogLevel.DEBUG:
145sys.stdout.write("<debug> %s\n" % Log._normalize(msg))
146sys.stdout.flush()
147
148@staticmethod
149def error(msg):
150sys.stderr.write("<error> %s\n" % Log._normalize(msg))
151
152@staticmethod
153def header(msg):
154sys.stderr.write("* %s *\n" % Log._normalize(msg))
155
156@staticmethod
157def cli(msg):
158sys.stderr.write("%s\n" % msg)
159
160
161class Shell:
162def __init__(self, cwd=None, logpath=None):
163self.cwd = cwd
164self.shell = True
165self.redirect = open(os.devnull if not logpath else logpath, "wr+")
166
167def __del__(self):
168self.redirect.close()
169
170def cd(self, cwd):
171Log.debug("cd %s" % cwd)
172self.cwd = cwd
173
174def truncate(self):
175self.redirect.truncate(0)
176
177def read_logs(self):
178self.redirect.seek(0)
179return self.redirect.read()
180
181def check_call(self, cmd):
182status = self.call(cmd)
183if status:
184raise Exception("Error running command %s. status=%s"
185% (cmd, status))
186
187def call(self, cmd):
188if isinstance(cmd, list):
189return self._calls(cmd)
190
191return self._call(cmd)
192
193def ssh(self, hostname, cmd, id_rsa=None):
194flags = "" if not id_rsa else "-i " + id_rsa
195return self.call("timeout %s ssh %s root@%s \"%s\"" %
196(SSH_TIMEOUT_S, flags, hostname, cmd))
197
198def scp(self, hostname, src, dest, id_rsa=None):
199flags = "" if not id_rsa else "-i " + id_rsa
200return self.call("timeout %s scp %s %s root@%s:%s" %
201(SSH_TIMEOUT_S, flags, src, hostname, dest))
202
203def output(self, cmd, cwd=None):
204Log.debug("%s> %s" % (cwd, cmd))
205return subprocess.check_output(shlex.split(cmd), cwd=self.cwd)
206
207def _calls(self, cmds):
208Log.debug("Running commands. %s" % cmds)
209for c in cmds:
210status = self.call(c)
211if status:
212Log.error("Commands failed with %s" % status)
213return status
214return 0
215
216def _call(self, cmd):
217if not self.shell:
218cmd = shlex.split(cmd)
219
220Log.debug("%s> %s" % (self.cwd, cmd))
221
222status = subprocess.call(cmd, cwd=self.cwd, shell=self.shell,
223stdout=self.redirect, stderr=self.redirect)
224
225Log.debug("return %s" % status)
226return status
227
228
229# ..............................................................................
230# Server role
231# ..............................................................................
232
233class TestServer:
234def __init__(self, port, scratchdir):
235self.port = port
236self.scratchdir = scratchdir
237self.shell = Shell()
238self.rpc = None
239self.pidf = None
240
241self.shell.check_call("mkdir -p %s" % self.scratchdir)
242self._process_lock()
243
244def __del__(self):
245if self.pidf:
246self.pidf.close()
247
248def init(self):
249Log.debug("Starting xmlrpc server on port %s" % self.port)
250self.rpc = GeneralXMLRPCServer(("", self.port))
251self.rpc.register_instance(Handlers(self.scratchdir))
252
253def serve(self):
254(status, _) = failsafe(self.rpc.serve_forever)
255Log.cli("== End ==")
256
257def _process_lock(self):
258pid_filename = os.path.basename(__file__).replace("/", "-")
259pid_filepath = "%s/%s.pid" % (self.scratchdir, pid_filename)
260self.pidf = open(pid_filepath, "w")
261try:
262fcntl.lockf(self.pidf, fcntl.LOCK_EX | fcntl.LOCK_NB)
263# We have the lock, kick anybody listening on this port
264self.shell.call("kill $(lsof -t -i:%s)" % self.port)
265except IOError:
266Log.error("Another process instance is running")
267sys.exit(0)
268
269#
270# Server Handler
271#
272
273
274handler_lock = threading.Lock()
275handler_serving_since = Timer()
276
277
278def synchronized(func):
279def decorator(*args, **kws):
280handler_lock.acquire()
281h = args[0]
282try:
283h.shell.truncate()
284ret = func(*args, **kws)
285return ret
286except Exception() as err:
287Log.error(str(err))
288Log.error(decode(h._log_content()))
289raise
290finally:
291handler_lock.release()
292handler_serving_since.reset()
293
294return decorator
295
296
297class Handlers:
298def __init__(self, scratchdir):
299self.client_id = None
300self.scratchdir = scratchdir
301self.gluster_root = "%s/glusterfs" % self.scratchdir
302self.shell = Shell(logpath="%s/test-handlers.log" % self.scratchdir)
303
304def hello(self, id):
305if not handler_lock.acquire(False):
306return False
307try:
308return self._hello_locked(id)
309finally:
310handler_lock.release()
311
312def _hello_locked(self, id):
313if handler_serving_since.elapsed_s() > CLIENT_TIMEOUT_S:
314Log.debug("Disconnected client %s" % self.client_id)
315self.client_id = None
316
317if not self.client_id:
318self.client_id = id
319handler_serving_since.reset()
320return True
321
322return (id == self.client_id)
323
324@synchronized
325def ping(self, id=None):
326if id:
327return id == self.client_id
328return True
329
330@synchronized
331def bye(self, id):
332assert id == self.client_id
333self.client_id = None
334handler_serving_since.reset()
335return True
336
337@synchronized
338def cleanup(self, id):
339assert id == self.client_id
340self.shell.cd(self.gluster_root)
341self.shell.check_call("PATH=.:$PATH; sudo ./clean_gfs_devserver.sh")
342return True
343
344@synchronized
345def copy(self, id, name, content):
346with open("%s/%s" % (self.scratchdir, name), "w+") as f:
347f.write(decode(content))
348return True
349
350@synchronized
351def copygzip(self, id, content):
352assert id == self.client_id
353gzipfile = "%s/tmp.tar.gz" % self.scratchdir
354tarfile = "%s/tmp.tar" % self.scratchdir
355self.shell.check_call("rm -f %s" % gzipfile)
356self.shell.check_call("rm -f %s" % tarfile)
357write_to_file(gzipfile, decode(content))
358
359self.shell.cd(self.scratchdir)
360self.shell.check_call("rm -r -f %s" % self.gluster_root)
361self.shell.check_call("mkdir -p %s" % self.gluster_root)
362
363self.shell.cd(self.gluster_root)
364cmds = [
365"gunzip -f -q %s" % gzipfile,
366"tar -xvf %s" % tarfile
367]
368return self.shell.call(cmds) == 0
369
370@synchronized
371def build(self, id, asan=False):
372assert id == self.client_id
373self.shell.cd(self.gluster_root)
374self.shell.call("make clean")
375env = "ASAN_ENABLED=1" if asan else ""
376return self.shell.call(
377"%s ./extras/distributed-testing/distributed-test-build.sh" % env) == 0
378
379@synchronized
380def install(self, id):
381assert id == self.client_id
382self.shell.cd(self.gluster_root)
383return self.shell.call("make install") == 0
384
385@synchronized
386def prove(self, id, test, timeout, valgrind="no", asan_noleaks=True):
387assert id == self.client_id
388self.shell.cd(self.gluster_root)
389env = "DEBUG=1 "
390if valgrind == "memcheck" or valgrind == "yes":
391cmd = "valgrind"
392cmd += " --tool=memcheck --leak-check=full --track-origins=yes"
393cmd += " --show-leak-kinds=all -v prove -v"
394elif valgrind == "drd":
395cmd = "valgrind"
396cmd += " --tool=drd -v prove -v"
397elif asan_noleaks:
398cmd = "prove -v"
399env += "ASAN_OPTIONS=detect_leaks=0 "
400else:
401cmd = "prove -v"
402
403status = self.shell.call(
404"%s timeout %s %s %s" % (env, timeout, cmd, test))
405
406if status != 0:
407return (False, self._log_content())
408return (True, "")
409
410def _log_content(self):
411return encode(self.shell.read_logs())
412
413# ..............................................................................
414# Cli role
415# ..............................................................................
416
417
418class RPCConnection((threading.Thread)):
419def __init__(self, host, port, path, cb):
420threading.Thread.__init__(self)
421self.host = host
422self.port = port
423self.path = path
424self.shell = Shell()
425self.cb = cb
426self.stop = False
427self.proxy = None
428self.logid = "%s:%s" % (self.host, self.port)
429
430def connect(self):
431(status, ret) = failsafe(self._connect)
432return (status and ret)
433
434def _connect(self):
435url = "http://%s:%s" % (self.host, self.port)
436self.proxy = xmlrpclib.ServerProxy(url, transport=IPTransport())
437return self.proxy.hello(self.cb.id)
438
439def disconnect(self):
440self.stop = True
441
442def ping(self):
443return self.proxy.ping()
444
445def init(self):
446return self._copy() and self._compile_and_install()
447
448def run(self):
449(status, ret) = failsafe(self.init)
450if not status:
451self.cb.note_lost_connection(self)
452return
453elif not ret:
454self.cb.note_setup_failed(self)
455return
456
457while not self.stop:
458(status, ret) = failsafe(self._run)
459if not status or not ret:
460self.cb.note_lost_connection(self)
461break
462time.sleep(0)
463
464failsafe(self.proxy.bye, (self.cb.id,))
465Log.debug("%s connection thread stopped" % self.host)
466
467def _run(self):
468test = self.cb.next_test()
469(status, _) = failsafe(self._execute_next, (test,))
470if not status:
471self.cb.note_retry(test)
472return False
473return True
474
475def _execute_next(self, test):
476if not test:
477time.sleep(1)
478return
479
480(status, error) = self.proxy.prove(self.cb.id, test,
481self.cb.test_timeout,
482self.cb.valgrind,
483self.cb.asan_noleaks)
484if status:
485self.cb.note_done(test)
486else:
487self.cb.note_error(test, error)
488
489def _compile_and_install(self):
490Log.debug("<%s> Build " % self.logid)
491asan = self.cb.asan or self.cb.asan_noleaks
492return (self.proxy.build(self.cb.id, asan) and
493self.proxy.install(self.cb.id))
494
495def _copy(self):
496return self._copy_gzip()
497
498def _copy_gzip(self):
499Log.cli("<%s> copying and compiling %s to remote" %
500(self.logid, self.path))
501data = encode(get_file_content(patch_file()))
502Log.debug("GZIP size = %s B" % len(data))
503return self.proxy.copygzip(self.cb.id, data)
504
505
506class RPCConnectionPool:
507def __init__(self, gluster_path, hosts, n, id_rsa):
508self.gluster_path = gluster_path
509self.hosts = hosts
510self.conns = []
511self.faulty = []
512self.n = int(len(hosts) / 2) + 1 if not n else n
513self.id_rsa = id_rsa
514self.stop = False
515self.scanner = threading.Thread(target=self._scan_hosts_loop)
516self.kicker = threading.Thread(target=self._kick_hosts_loop)
517self.shell = Shell()
518self.since_start = Timer()
519
520self.shell.check_call("rm -f %s" % patch_file())
521self.shell.check_call("tar -zcvf %s ." % patch_file())
522self.id = md5.new(get_file_content(patch_file())).hexdigest()
523Log.cli("client UID %s" % self.id)
524Log.cli("patch UID %s" % PATCH_FILE_UID)
525
526def __del__(self):
527self.shell.check_call("rm -f %s" % patch_file())
528
529def pool_status(self):
530elapsed_m = int(self.since_start.elapsed_s() / 60)
531return "%s/%s connected, %smin elapsed" % (len(self.conns), self.n,
532elapsed_m)
533
534def connect(self):
535Log.debug("Starting scanner")
536self.scanner.start()
537self.kicker.start()
538
539def disconnect(self):
540self.stop = True
541for conn in self.conns:
542conn.disconnect()
543
544def note_lost_connection(self, conn):
545Log.cli("lost connection to %s" % conn.host)
546self.conns.remove(conn)
547self.hosts.append((conn.host, conn.port))
548
549def note_setup_failed(self, conn):
550Log.error("Setup failed on %s:%s" % (conn.host, conn.port))
551self.conns.remove(conn)
552self.faulty.append((conn.host, conn.port))
553
554def _scan_hosts_loop(self):
555Log.debug("Scanner thread started")
556while not self.stop:
557failsafe(self._scan_hosts)
558time.sleep(5)
559
560def _scan_hosts(self):
561if len(self.hosts) == 0 and len(self.conns) == 0:
562Log.error("no more hosts available to loadbalance")
563sys.exit(1)
564
565for (host, port) in self.hosts:
566if (len(self.conns) >= self.n) or self.stop:
567break
568self._scan_host(host, port)
569
570def _scan_host(self, host, port):
571Log.debug("scanning %s:%s" % (host, port))
572c = RPCConnection(host, port, self.gluster_path, self)
573(status, result) = failsafe(c.connect)
574if status and result:
575self.hosts.remove((host, port))
576Log.debug("Connected to %s:%s" % (host, port))
577self.conns.append(c)
578c.start()
579Log.debug("%s / %s connected" % (len(self.conns), self.n))
580else:
581Log.debug("Failed to connect to %s:%s" % (host, port))
582
583def _kick_hosts_loop(self):
584Log.debug("Kick thread started")
585while not self.stop:
586time.sleep(10)
587failsafe(self._kick_hosts)
588
589Log.debug("Kick thread stopped")
590
591def _is_pingable(self, host, port):
592c = RPCConnection(host, port, self.gluster_path, self)
593failsafe(c.connect)
594(status, result) = failsafe(c.ping)
595return status and result
596
597def _kick_hosts(self):
598# Do not kick hosts if we have the optimal number of connections
599if (len(self.conns) >= self.n) or self.stop:
600Log.debug("Skip kicking hosts")
601return
602
603# Check and if dead kick all hosts
604for (host, port) in self.hosts:
605if self.stop:
606Log.debug("Break kicking hosts")
607break
608
609if self._is_pingable(host, port):
610Log.debug("Host=%s is alive. Won't kick" % host)
611continue
612
613Log.debug("Kicking %s" % host)
614mypath = sys.argv[0]
615myname = os.path.basename(mypath)
616destpath = "/tmp/%s" % myname
617sh = Shell()
618sh.scp(host, mypath, destpath, self.id_rsa)
619sh.ssh(host, "nohup %s --server &>> %s.log &" %
620(destpath, destpath), self.id_rsa)
621
622def join(self):
623self.scanner.join()
624self.kicker.join()
625for c in self.conns:
626c.join()
627
628
629# ..............................................................................
630# test role
631# ..............................................................................
632
633class TestRunner(RPCConnectionPool):
634def __init__(self, gluster_path, hosts, n, tests, flaky_tests, valgrind,
635asan, asan_noleaks, id_rsa, test_timeout):
636RPCConnectionPool.__init__(self, gluster_path, self._parse_hosts(hosts),
637n, id_rsa)
638self.flaky_tests = flaky_tests.split(" ")
639self.pending = []
640self.done = []
641self.error = []
642self.retry = {}
643self.error_logs = []
644self.stats_timer = Timer()
645self.valgrind = valgrind
646self.asan = asan
647self.asan_noleaks = asan_noleaks
648self.test_timeout = test_timeout
649
650self.tests = self._get_tests(tests)
651
652Log.debug("tests: %s" % self.tests)
653
654def _get_tests(self, tests):
655if not tests or tests == "all":
656return self._not_flaky(self._all())
657elif tests == "flaky":
658return self.flaky_tests
659else:
660return self._not_flaky(tests.strip().split(" "))
661
662def run(self):
663self.connect()
664self.join()
665return len(self.error)
666
667def _pretty_print(self, data):
668if isinstance(data, list):
669str = ""
670for i in data:
671str = "%s %s" % (str, i)
672return str
673return "%s" % data
674
675def print_result(self):
676Log.cli("== RESULTS ==")
677Log.cli("SUCCESS : %s" % len(self.done))
678Log.cli("ERRORS : %s" % len(self.error))
679Log.cli("== ERRORS ==")
680Log.cli(self._pretty_print(self.error))
681Log.cli("== LOGS ==")
682Log.cli(self._pretty_print(self.error_logs))
683Log.cli("== END ==")
684
685def next_test(self):
686if len(self.tests):
687test = self.tests.pop()
688self.pending.append(test)
689return test
690
691if not len(self.pending):
692self.disconnect()
693
694return None
695
696def _pct_completed(self):
697total = len(self.tests) + len(self.pending) + len(self.done)
698total += len(self.error)
699completed = len(self.done) + len(self.error)
700return 0 if not total else int(completed / total * 100)
701
702def note_done(self, test):
703Log.cli("%s PASS (%s%% done) (%s)" % (test, self._pct_completed(),
704self.pool_status()))
705self.pending.remove(test)
706self.done.append(test)
707if test in self.retry:
708del self.retry[test]
709
710def note_error(self, test, errstr):
711Log.cli("%s FAIL" % test)
712self.pending.remove(test)
713if test not in self.retry:
714self.retry[test] = 1
715
716if errstr:
717path = "%s/%s-%s.log" % ("/tmp", test.replace("/", "-"),
718self.retry[test])
719failsafe(write_to_file, (path, decode(errstr),))
720self.error_logs.append(path)
721
722if self.retry[test] < MAX_ATTEMPTS:
723self.retry[test] += 1
724Log.debug("retry test %s attempt %s" % (test, self.retry[test]))
725self.tests.append(test)
726else:
727Log.debug("giveup attempt test %s" % test)
728del self.retry[test]
729self.error.append(test)
730
731def note_retry(self, test):
732Log.cli("retry %s on another host" % test)
733self.pending.remove(test)
734self.tests.append(test)
735
736#
737# test classifications
738#
739def _all(self):
740return self._list_tests(["tests"], recursive=True)
741
742def _not_flaky(self, tests):
743for t in self.flaky_tests:
744if t in tests:
745tests.remove(t)
746return tests
747
748def _list_tests(self, prefixes, recursive=False, ignore_ifnotexist=False):
749tests = []
750for prefix in prefixes:
751real_path = "%s/%s" % (self.gluster_path, prefix)
752if not os.path.exists(real_path) and ignore_ifnotexist:
753continue
754for f in os.listdir(real_path):
755if os.path.isdir(real_path + "/" + f):
756if recursive:
757tests += self._list_tests([prefix + "/" + f], recursive)
758else:
759if re.match(r".*\.t$", f):
760tests += [prefix + "/" + f]
761return tests
762
763def _parse_hosts(self, hosts):
764ret = []
765for h in args.hosts.split(" "):
766ret.append((h, DEFAULT_PORT))
767Log.debug(ret)
768return ret
769
770# ..............................................................................
771# Roles entry point
772# ..............................................................................
773
774
775def run_as_server(args):
776if not args.server_path:
777Log.error("please provide server path")
778return 1
779
780server = TestServer(args.port, args.server_path)
781server.init()
782server.serve()
783return 0
784
785
786def run_as_tester(args):
787Log.header("GLUSTER TEST CLI")
788
789Log.debug("args = %s" % args)
790
791tests = TestRunner(args.gluster_path, args.hosts, args.n, args.tests,
792args.flaky_tests, valgrind=args.valgrind,
793asan=args.asan, asan_noleaks=args.asan_noleaks,
794id_rsa=args.id_rsa, test_timeout=args.test_timeout)
795result = tests.run()
796tests.print_result()
797return result
798
799# ..............................................................................
800# main
801# ..............................................................................
802
803
804def main(args):
805if args.v:
806Log.LOGLEVEL = LogLevel.DEBUG
807
808if args.server and args.tester:
809Log.error("Invalid arguments. More than one role specified")
810sys.exit(1)
811
812if args.server:
813sys.exit(run_as_server(args))
814elif args.tester:
815sys.exit(run_as_tester(args))
816else:
817Log.error("please specify a mode for CI")
818parser.print_help()
819sys.exit(1)
820
821
822parser = argparse.ArgumentParser(description="Gluster CI")
823
824# server role
825parser.add_argument("--server", help="start server", action="store_true")
826parser.add_argument("--server_path", help="server scratch space",
827default="/tmp/gluster-test")
828parser.add_argument("--host", help="server address to listen", default="")
829parser.add_argument("--port", help="server port to listen",
830type=int, default=DEFAULT_PORT)
831# test role
832parser.add_argument("--tester", help="start tester", action="store_true")
833parser.add_argument("--valgrind[=memcheck,drd]",
834help="run tests with valgrind tool 'memcheck' or 'drd'",
835default="no")
836parser.add_argument("--asan", help="test with asan enabled",
837action="store_true")
838parser.add_argument("--asan-noleaks", help="test with asan but no mem leaks",
839action="store_true")
840parser.add_argument("--tests", help="all/flaky/list of tests", default=None)
841parser.add_argument("--flaky_tests", help="list of flaky tests", default=None)
842parser.add_argument("--n", help="max number of machines to use", type=int,
843default=0)
844parser.add_argument("--hosts", help="list of worker machines")
845parser.add_argument("--gluster_path", help="gluster path to test",
846default=os.getcwd())
847parser.add_argument("--id-rsa", help="private key to use for ssh",
848default=None)
849parser.add_argument("--test-timeout",
850help="test timeout in sec (default 15min)",
851default=TEST_TIMEOUT_S)
852# general
853parser.add_argument("-v", help="verbose", action="store_true")
854parser.add_argument("--address_family", help="IPv6 or IPv4 to use",
855default=ADDRESS_FAMILY)
856
857args = parser.parse_args()
858
859main(args)
860