From f689b63f689a0ee2faec575ac3b5fd47d8f1b19d Mon Sep 17 00:00:00 2001 From: Alain Spineux Date: Fri, 4 Sep 2020 13:32:53 +0200 Subject: [PATCH] BEE Backport regress/scripts/blab.py This commit is the result of the squash of the following main commits: Author: Alain Spineux Date: Wed Oct 16 19:28:20 2019 +0200 regress: add a Dedup resource and make async.sh rebuild the SD plugins Author: Alain Spineux Date: Wed Feb 28 15:50:40 2018 +0100 regress: tweak blab.py Author: Alain Spineux Date: Thu Feb 1 16:53:37 2018 +0100 regress: add lab.CheckConfig() to blab.py to check FORCE_XXXX - check if FORCE_XXXX are set and skip (exit 0) if not appropriate. - add lab.Die("your message") - add lab.Exit(exit_code, ["your message"]) Author: Alain Spineux Date: Mon Aug 14 18:55:49 2017 +0200 regress: dedup-checkoptions-test & blab.py update for new vacuum logging Author: Alain Spineux Date: Mon Aug 14 13:49:58 2017 +0200 regress: tweak dedup - arun.ini update to include all dedup tests - blab.py handle new scrub/vacuum logging Author: Alain Spineux Date: Tue Aug 8 16:34:57 2017 +0200 regress: improve tests/dedup-scrub-test.py, blap.py and querry_dde.py - dedup-scrub-test.py handle fake index entries - query_dde.py handle forged addresses - blap.py more definitions Author: Alain Spineux Date: Mon Jul 10 13:41:30 2017 +0200 regress: blab.py add minimal support for scrubber - add EZThread.AsyncStop0 - add Dedupengine.GetContainerPath() - add minimal support for scrubber Author: Alain Spineux Date: Tue May 30 16:32:21 2017 +0200 regress: export QUERY_DDE_ADVANCED variable in blab.py Author: Alain Spineux Date: Fri Apr 28 14:19:52 2017 +0200 regress: adjust all .py scripts to blab.py and vacuum-ng Author: Alain Spineux Date: Fri Apr 28 14:13:59 2017 +0200 regress: improve and adjust blab.py for vacuum-ng - adjust blab.py for vacuum-ng output - kill -USR1 generate a stacktrace of running python thread - all .py scripts support --no-cleanup, --debug and --verbose options - support for 'dedup-simple' and 'dedup-autochanger' profiles - new blab.StartTest() and EndTest() - blab.GetJob() - blab.Log(logging.CTRITICAL, ) force a exit Author: Alain Spineux Date: Mon Apr 24 14:57:34 2017 +0200 regress: blab.py based on copy-dedup-confs instead of copy-plugin-confs - use DiskChanger instead (STORAGE=DiskChanger) - new EZThread base class for thread - add --no-cleanup PART 1 - GetJob(jobid) and jobid=-1 instead of GetJob(pos) - handle number like "12,345,567" with ',' Author: Alain Spineux Date: Fri Apr 21 15:03:22 2017 +0200 regress: blab.py add ListMedia() Author: Alain Spineux Date: Thu Apr 6 21:56:38 2017 +0200 regress: blab.py add ListJobs() and ZapContainers() - ListJobs() return list jobs - GetJob(pos) return one job (use -1) - ZapContainers() fill all containers full of zeroes Author: Alain Spineux Date: Fri Mar 24 14:49:12 2017 +0100 blab.py parse dedup usage and more vacuum - parse dedup usage - parse vacuum "need optimize" - add some function helper to DDE Author: Alain Spineux Date: Fri Dec 2 16:10:57 2016 +0100 blab.py add BconsoleScriptOut() - BconsoleScriptOut() decode stdout - Add GetVar() - handle "exit" in the Shell Author: Alain Spineux Date: Thu Nov 10 09:26:06 2016 +0100 regress: improve blab.py - does stdout & stderr passthrough instead of active forwardin - accurate detection of the end of the shell commands - delete /tmp/std{out,err}XXXX using atexit Author: Alain Spineux Date: Fri Oct 21 16:11:47 2016 +0200 regress: make regress python friendly - modules like bconsole.py and blab.py goes in regress/scrips - regress/tests/regress.py is a helper that load modules in scripts without boring with librady path - py-sample-test.py is the first sample - add new objects lab.{fd,sd,dir} that are manage the related Daemon - add lab.sd.dde of class DDE that provides some basic helper for the DDE - file FileReader can search a log or trace file - lab.GetVolume(volname) return information about a volume --- regress/scripts/blab.py | 872 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 872 insertions(+) create mode 100755 regress/scripts/blab.py diff --git a/regress/scripts/blab.py b/regress/scripts/blab.py new file mode 100755 index 000000000..8580e76a4 --- /dev/null +++ b/regress/scripts/blab.py @@ -0,0 +1,872 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Bacula(R) - The Network Backup Solution + + Copyright (C) 2000-2020 Kern Sibbald + + The original author of Bacula is Kern Sibbald, with contributions + from many others, a complete list can be found in the file AUTHORS. + + You may use this file and others of this release according to the + license defined in the LICENSE file, which includes the Affero General + Public License, v3.0 ("AGPLv3") and some additional permissions and + terms pursuant to its AGPLv3 Section 7. + + This notice must be preserved when any source code is + conveyed and/or propagated. + + Bacula(R) is a registered trademark of Kern Sibbald. +# +# author: alain@baculasystems.com + +import os +import sys +import argparse +import subprocess +import threading +import hashlib +import time +import queue +import codecs +import logging +import warnings +import stat +import re +import struct +import atexit + +import bconsole + +if True: + import code, traceback, signal + + def debug(sig, frame): + """Interrupt running process, and provide a python prompt for + interactive debugging.""" + d={'_frame':frame} # Allow access to frame object. + d.update(frame.f_globals) # Unless shadowed by global + d.update(frame.f_locals) + + i = code.InteractiveConsole(d) + message = "Signal received : entering python shell.\nTraceback:\n" + message += ''.join(traceback.format_stack(frame)) + i.interact(message) + + def dumpstacks(signal, frame): + id2name = dict([(th.ident, th.name) for th in threading.enumerate()]) + code = [] + for threadId, stack in sys._current_frames().items(): + if id2name.get(threadId,"").startswith('pydevd.'): + continue + code.append("\n# %d Thread: %s(%d)" % (os.getpid(), id2name.get(threadId,""), threadId)) + for filename, lineno, name, line in traceback.extract_stack(stack): + code.append('File: "%s", line %d, in %s' % (filename, lineno, name)) + if line: + code.append(" %s" % (line.strip())) + print("\n".join(code), file=sys.stderr) + + def listen(): + signal.signal(signal.SIGUSR1, dumpstacks) # Register handler + + listen() + +def atexit_delete_file(paths): + for path in paths: + try: + os.unlink(path) + except FileNotFoundError: + pass + +class Obj: + pass + +class DDE: + BUCKETID_SHIFT=(64-16) + BUCKETID_MASK=(0x7FFF<\d{2}-\w{3}-\d{4} \d{2}:\d{2}:\d{2}) Start forceoptimize=(?P\d+) holepunching=(?P\d+) checkindex=(?P\d+) checkvolume=(?P\d+)') + regex.vacuum_end=re.compile(daemon_head+'Vacuum: (?P\d{2}-\w{3}-\d{4} \d{2}:\d{2}:\d{2}) End\s+-+') + regex.vacuum_bnum_min=re.compile(daemon_head+'Vacuum: bnum_min=(?P\d+) bnum_max=(?P\d+) mlock_max=(?P\d+) mlock_strategy=(?P\d+)') + regex.vacuum_hole_size=re.compile(daemon_head+'Vacuum: hole_size=(?P\d+)') + regex.vacuum_hash_count=re.compile(daemon_head+'Vacuum: hash_count=(?P\d+)/(?P\d+) chunk_count=(?P\d+) connections=(?P\d+)') + regex.vacuum_preload=re.compile(daemon_head+'Vacuum: preload orphan count=(?P\d+) from (?P\d+)') + regex.vacuum_volumes=re.compile(daemon_head+'Vacuum: (?P\d{2}-\w{3}-\d{4} \d{2}:\d{2}:\d{2}) Number of volumes handled (?P\d+)/(?P\d+) suspect_ref=(?P\d+)') + regex.vacuumbadref=re.compile(daemon_head+'VacuumBadRef (?P\S+) FI=(?P\d+) SessId=(?P\d+) SessTime=(?P\d+)( : ref\(\#(?P[0-9A-Fa-f]+) addr=(?P0x[0-9A-Fa-f]+) size=(?P\d+)\) (?P.*)| : size=(?P\d+)|)') + regex.vacuum_optimize=re.compile(daemon_head+'Vacuum: (?P\d{2}-\w{3}-\d{4} \d{2}:\d{2}:\d{2}) optimize_index count=(?P\d+) del=(?P\d+) add=(?P\d+) optimize_err=(?P\d+)') + regex.vacuum_need_optimize=re.compile(daemon_head+'Vacuum: need optimize, (?P.*)') + regex.vacuum_ref_count=re.compile(daemon_head+'Vacuum: ref_count=(?P\d+) error=(?P\d+) suspect_ref=(?P\d+)') + regex.vacuum_idxfix=re.compile(daemon_head+'Vacuum: idxfix=(?P\d+) 2miss=(?P\d+) orphan=(?P\d+) recoverable=(?P\d+)') + regex.vacuum_idxmiss=re.compile(daemon_head+'Vacuum: 2miss=(?P\d+) orphan=(?P\d+)') + regex.vacuum_idxfix_err=re.compile(daemon_head+'Vacuum: idxupd_err=(?P\d+) chunk_read=(?P\d+) chunk_read_err=(?P\d+) chunkdb_err=(?P\d+)') + + vacuum_vars='error,suspect_ref,idxfix,idx_2miss,idxmiss_2miss,recoverable,orphan,suspect_ref,idxfix_err,chunk_read,chunk_read_err,chunk_write_err,chunkdb_err,idxmiss_2miss'.split(',') + orphan_struct=struct.Struct('!Q') + + regex.dedup_usage=re.compile("""\ +Dedupengine status:\s+"(?P.*)" +\s+DDE:\s+hash_count=(?P\d+)\s+ref_count=(?P\d+)\s+ref_size=(?P\d+(\.\d*)?)\s(?P[KMGTPEZY]?B) +\s+ref_ratio=(?P\d+(\.\d*)?)\s+size_ratio=(?P\d+(\.\d*)?)\s+dde_errors=(?P\d+) +\s+Config:\s+bnum=(?P\d+)\s+bmin=(?P\d+)\s+bmax=(?P\d+)\s+mlock_strategy=(?P\d+)\s+mlocked=(?P\d+)MB\s+mlock_max=(?P\d+)MB(\\n\s+mlock_error="(?P.*)")? +\s+HolePunching:\s+hole_size=(?P\d+)\s+KB +\s+Containers:\s+chunk_allocated=(?P\d+)\s+chunk_used=(?P\d+) +\s+disk_space_allocated=(?P\d+(\.\d*)?)\s+(?P[KMGTPEZY]?B)\s+disk_space_used=(?P\d+(\.\d*)?)\s+(?P[KMGTPEZY]?B)\s+containers_errors=(?P\d+) +\s+Vacuum:\s+last_run="(?P.*)"\s+duration=(?P\d+)s\s+ref_count=(?P\d+)\sref_size=(?P\d+(\.\d*)?)\s+(?P[KMGTPEZY]?B) +\s+vacuum_errors=(?P\d+)\s+orphan_ref=(?P\d+)\s+suspect_ref=(?P\d+)\s+(progress=(?P\d+)%%)? +\s+Scrubber:\s+last_run="(?P.*)".* +\s+Stats:\s+read_chunk=(?P\d+)\s+query_hash=(?P\d+)\s+new_hash=(?P\d+)\s+calc_hash=(?P\d+) +""", re.M) + + def __init__(self, dedup_dir, dedup_index_dir): + self.dedup_dir=dedup_dir + self.dedup_index_dir=dedup_index_dir + self.archdir=self.dedup_index_dir + self.vacuum_orphan_lst_path=os.path.join(self.archdir, "orphanaddr.bin") + + def GetDedupDir(self): + return self.dedup_dir + + def GetMetaDir(self): + return self.dedup_index_dir + + def GetFSMPath(self): + return os.path.join(self.dedup_index_dir, 'bee_dde.idx') + + def GetIndexPath(self): + return os.path.join(self.dedup_index_dir, 'bee_dde.tch') + + def SaveOrphan(self, orphans, appending=False): + f=open(self.vacuum_orphan_lst_path, 'ab' if appending else 'wb') + for orphan in orphans: + f.write(self.orphan_struct.pack(orphan)) + f.close() + + def ChunkAddr2ContainerId(addr): + return (addr & self.BUCKETID_MASK)>>self.BUCKETID_SHIFT + + def ChunkAddr2Idx(addr): + return (addr & ~self.BUCKETIDX_MASK) + + def CalcChunkAddr(self, containerid, containeridx): + return containerid<0: + dedup_options=args[0] + if not isinstance(dedup_options, (list, tuple)): + dedup_options=[dedup_options ] + dedup_option=self.GetVar('DEDUP_FS_OPTION') + if dedup_option not in dedup_options: + self.Exit(0, "DEDUP_FS_OPTION don't match, skip test {}".format(self.testname)) + else: + self.Die("Unknow config argument {}".format(force)) + + def StartTest(self): + if self.cleanup: + self.Shell('start_test') + else: + self.Shell('reset_test') + self.StartBacula() + + def EndTest(self): + self.StopBacula() + self.Shell('end_test') + self.shell.Close() + + def StartBacula(self): + open(os.path.join(self.vars.tmp, 'bconcmds'), 'w').write('quit\n') + self.Shell('run_bacula') + + def StopBacula(self): + self.Shell('stop_bacula') + + def MakePlugin(self): + warnings.warn("deprecated", DeprecationWarning) + self.MakePlugins('fd:test-dedup') + + def MakePlugins(self, lst): + """expect a list of plugins like [ 'fd:test-dedup' ]""" + if not isinstance(lst, list): + lst=[ lst ] + _code, pwd, _err=self.shell.ExecOut('pwd') + for pl in lst: + daemon, plugin=pl.split(':') + self.shell.Exec('cd ${cwd}/build/src/plugins/'+daemon) + self.shell.Exec('make') + self.shell.Exec('make install-'+plugin) + self.shell.Exec('cd '+pwd) + + def GetVolume(self, volname): + volume=dict() + volume['name']=volname + volume['path']=os.path.join(self.vars.tmp, volname) + return volume + + def s2usize(self, val, unit, comment=""): + units=dict(B=1, KB=1E3, MB=1E6, GB=1E9, TB=1E12, EB=1E15, ZB=1E18, YB=1E21) + val=val.translate({44:None}) + try: + v=float(val)*units[unit] + except Exception as ex: + raise Exception('error conversing %r %r (%r)' % (val, unit, comment)) + return v + + def s2time(self, val, comment=""): + try: + v=val # TODO + except Exception as ex: + raise Exception('error conversing %r %r (%r)' % (val, unit, comment)) + return v + + def s2int(self, val): + return int(val.translate({44:None})) + + def DedupUsage(self, storage=None): + if storage==None: + storage=self.GetVar('STORAGE') + returncode, out, err=self.BconsoleScriptOut('dedup usage storage=%s' % (storage,)) + pos=out.find('Dedupengine status:') + self.Assert(pos!=-1, "dedup usage not found\n%s\n%s" % (out, err)) + st=out[pos:] + match=DDE.regex.dedup_usage.match(st) + if not match: + """embeded debugging""" + regex='' + for line in DDE.regex.dedup_usage.pattern.split('\n'): + if regex: + regex+='\n'+line + else: + regex=line + + self.Log(logging.ERROR, "line=%r", line) + if not re.match(regex, st, re.M): + self.Log(logging.ERROR, "%s", st) + self.Log(logging.ERROR, "dedup usage regex fail here:") + self.Log(logging.ERROR, "%r", line) + break + + self.Assert(match, "dedup usage regex don't match:\n%s" % (st, )) + values=match.groupdict() + convert=dict(hash_count=int, ref_count=int, ref_size=self.s2usize, + ref_ratio=float, size_ratio=float, dde_errors=int, + bnum=int, bmin=int, bmax=int, mlock_strategy=int, mlocked_mb=int, mlock_max_mb=int, + hole_size_kb=int, + chunk_allocated=int, chunk_used=int, + disk_space_allocated=self.s2usize, disk_space_used=self.s2usize, containers_errors=int, + vac_last_run=self.s2time, vac_duration=int, vac_ref_count=int, vac_ref_size=self.s2usize, + vac_errors=int, vac_orphan_ref=int, vac_suspect_ref=int, + read_chunk=int, query_hash=int, new_hash=int, calc_hash=int) + for k in convert: + if convert[k]==self.s2usize: + values[k]=self.s2usize(values[k], values[k+'_unit']) + else: + if False and k in ('bmin', 'bmax'): + self.Log(logging.INFO, '%s=%r -> %r', k, values[k], convert[k](values[k])) + values[k]=convert[k](values[k]) + + return values + + def DedupSetMaximumContainerSize(self, mcs): + lab.Shell("""$bperl -e 'add_attribute("$conf/bacula-sd.conf", "MaximumContainerSize", "{}B", "Storage")'""".format(mcs)) + + def Assert(self, cond, message="Unexpected error"): + if not cond: + raise Exception(message) + + def SetupProfile(self): + if self.profile not in ('dedup-simple', 'dedup-autochanger'): + self.Log(loggin.CRITICAL, "unknown profile: %s", self.profile) + + if self.profile.startswith('dedup-'): + if 'MaximumContainerSize' in self.profile_args: + self.SetVar('DEDUP_MAXIMUM_CONTAINER_SIZE', '{}KB'.format(self.profile_args['MaximumContainerSize']//1024)) + if self.cleanup: + self.SetupDedup() + + if self.profile=='dedup-simple': + if self.cleanup: + self.Shell('scripts/copy-plugin-confs') + self.SetVar('STORAGE', 'File') + elif self.profile=='dedup-autochanger': + if self.cleanup: + self.Shell('scripts/copy-dedup-confs') + self.SetVar('STORAGE', 'DiskChanger') + + if self.profile in ('dedup-simple', 'dedup-autochanger'): + self.SetVar('JobName', 'DedupPluginTest') + if self.cleanup: + self.Shell("""$bperl -e 'add_attribute("$conf/bacula-dir.conf", "Maximum Concurrent Jobs", "10", "Director")'""") + self.Shell("""$bperl -e 'add_attribute("$conf/bacula-dir.conf", "Maximum Concurrent Jobs", "10", "Client")'""") + self.Shell("""$bperl -e 'add_attribute("$conf/bacula-dir.conf", "Maximum Concurrent Jobs", "10", "Storage", "File")'""") + self.Shell("""$bperl -e 'add_attribute("$conf/bacula-dir.conf", "Maximum Concurrent Jobs", "10", "Job", "$JobName")'""") + self.Shell("""$bperl -e 'add_attribute("$conf/bacula-sd.conf", "Volume Poll Interval", "5s", "Device", "FileStorage")'""") + #self.Shell("""$bperl -e 'add_attribute("$conf/bacula-dir.conf", "Media Type", "File", "Storage", "File1")'""") + #self.Shell("""$bperl -e 'add_attribute("$conf/bacula-dir.conf", "Maximum Concurrent Jobs", "10", "Storage", "File1")'""") + #self.Shell("""$bperl -e 'add_attribute("$conf/bacula-sd.conf", "Media Type", "File", "Device", "FileStorage1")'""") + + if self.profile.startswith('dedup-'): + self.ResetDedup() + + def SetupDedup(self): + self.Shell('check_dedup_enable') + self.Shell('require_query_dde') + #self.MakePlugins([ 'fd:test-dedup', ]) + # create 'select-cfg.sh' file to use in bconsole command like this : + # @exec "{tmp}/select-cfg.sh 0" + # run job=DedupPluginTest level=Full storage=File1 yes + # @exec "{tmp}/select-cfg.sh next" + f=open(os.path.join(self.vars.tmp, 'select-cfg.sh'), 'wt') + f.write("""\ +# The plugin unlink the file just after open() +# echo "$$ `date` $@" >> /tmp/log +while [ -f {tmp}/dedupstreams.conf ] ; do sleep 1; done +for var in "$@" ; do + if [ "${{var}}" = "next" ] ; then + var=0 + if [ -f {tmp}/dedupstreams.last ] ; then + var=`cat {tmp}/dedupstreams.last` + var=$(($var + 1)) + fi + fi + echo {tmp}/stream${{var}}.dedup >> {tmp}/dedupstreams.conf + chmod a+x {tmp}/dedupstreams.conf # let the plugin does the unlink + echo ${{var}} > {tmp}/dedupstreams.last +done +# echo "$$ `date` done" >> /tmp/log +""".format(tmp=self.vars.tmp)) + os.fchmod(f.fileno(), stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH) + f.close() + + # create a default 'stream0.dedup' file + f=open(os.path.join(self.vars.tmp, 'stream0.dedup'), 'wt') + f.write("""\ +global_size=102400M +chunk_min_size=3K +chunk_max_size=64K +deviation=10 +seed=1234 +size=128M +start=-1 +""") + f.close() + + def ResetDedup(self): + # cleanup 'dedupstreams.conf' + try: + os.unlink(os.path.join(self.vars.tmp, 'dedupstreams.conf')) + except FileNotFoundError: + pass + + def ListMedia(self, type=None, status=None): + """typ & status can be list""" + convert=dict(MediaId=self.s2int, VolumeName=str, MediaType=str, VolBytes=self.s2int, \ + VolStatus=str, ) + returncode, out, err=self.BconsoleScriptOut('llist media\n', verbose=False) + media=None + medias=dict() + skipping_garbadge=True + for line in out.splitlines(False): + if skipping_garbadge: + skipping_garbadge=not line.startswith('Using Catalog') + continue + line=line.strip() + if line: + try: + k, v=line.split(':', 1) + except: + if line not in ('No results to list.', 'You have messages.'): + print(out) + print("=> {!r}".format(line), file=sys.stderr) + + try: + conv=convert[k] + except: + continue + val=conv(v.strip()) + if media: + setattr(media, k, val) + if k=='MediaId': + media=Obj() + media.MediaId=val + medias[val]=media + elif k=='MediaType': + if type!=None and val not in type and media.MediaId in medias: + del medias[media.MediaId] + continue + elif k=='VolStatus': + if status!=None and val not in status and media.MediaId in medias: + del medias[media.MediaId] + continue + else: + media=None + return medias + + def ListJobs(self, type=None, status=None): + """typ & status can be a string""" + convert=dict(JobId=self.s2int, Name=str, Type=str, Level=str, JobStatus=str, \ + JobErrors=self.s2int,) + returncode, out, err=self.BconsoleScriptOut('llist jobs\n', verbose=False) + job=None + jobs=dict() + skipping_garbadge=True + for line in out.splitlines(False): + if skipping_garbadge: + skipping_garbadge=not line.startswith('Using Catalog') + continue + line=line.strip() + if line: + try: + k, v=line.split(':', 1) + except: + if line not in ('No results to list.', 'You have messages.'): + print(out) + print("=> {!r}".format(line), file=sys.stderr) + + try: + conv=convert[k] + except: + continue + val=conv(v.strip()) + if job: + setattr(job, k, val) + if k=='JobId': + job=Obj() + job.JobId=val + jobs[val]=job + elif k=='Type': + if type!=None and val not in type and job.JobId in jobs: + del jobs[job.JobId] + continue + elif k=='JobStatus': + if status!=None and val not in status and job.JobId in jobs: + del jobs[job.JobId] + continue + else: + job=None + return jobs + + def GetJob(self, jobid, type=None): + """use -1 for last jobid""" + jobs=self.ListJobs(type=type) + if not jobs: + return None + if jobid<0: + if jobid==-1: + jobid=max(jobs.keys()) + else: + s=sorted(list(jobs.keys())) + jobid=s[jobid] + return jobs[jobid] + + def AssertNoJobError(self, exclude=None, type=None): + """exclude is a list of job to that can be in error""" + jobs=self.ListJobs(type=type) + errors=[] + for jobid in jobs: + job=jobs[jobid] + if exclude!=None and jobid in exclude: + continue + if job.JobStatus not in 'T' or job.JobErrors!=0: + self.Log("job %d status=%s errors=%d", jobid, job.JobStatus, job.JobErrors) + errors.append(str(job.JobId)) + self.Assert(len(errors)==0, "%d jobs have errors: %s" % (len(errors), ','.join(errors))) + + def Log(self, lvl, msg, *args, **kwargs): + assert not kwargs, "dont handle this" + if lvl>logging.WARNING or self.debug or (lvl>=logging.INFO and self.verbose): + print(msg % args, file=sys.stderr) + sys.stderr.flush() + if lvl>=logging.CRITICAL: + sys.exit(1) + + def Exit(self, code, msg=None): + if msg: + self.Log(logging.INFO, msg) + sys.exit(code) + + def Die(self, msg=None): + if msg: + self.Log(logging.CRITICAL, msg) + else: + sys.exit(1) + + +class EZThread(threading.Thread): + cont=True # Continue + stopping=False + + def AsyncStop0(self): + pass + + def AsyncStop(self): + if not self.stopping: + self.AsyncStop0() + self.stopping=True + self.cont=False + + def Join(self): + self.AsyncStop() + self.join() + + +mainparser=argparse.ArgumentParser(description='Bacula regression test') +mainparser.add_argument('--debug', action='store_true', help="idem REGRESS_DEBUG=1") +mainparser.add_argument('--verbose', action='store_true', help="be verbose") +mainparser.add_argument('--cleanup', action='store_true', help="don't reset data", default=True) +mainparser.add_argument('--no-cleanup', dest='cleanup', action='store_false', help="don't reset data") + +if __name__ == "__main__": + + s=Shell() + out=s.ExecOut('echo hello') + print('--> %r' % (out,)) + out=s.ExecOut('ls /dontexist') + print('--> %r' % (out,)) + out=s.ExecOut('sleep 1') + print('--> %r' % (out,)) + out=s.ExecOut('false') + print('--> %r' % (out,)) + s.Exec('ls /tmp') + s.Close() -- 2.47.3