def set(self, field, value): """ Set a field in the database :param str field: name of the field to set :param str value: value to stored; None to remove that field """ if value != None: assert isinstance(value, basestring) assert len(value) < 1023 location = os.path.join(self.location, field) if value == None: try: os.unlink(location) except OSError as e: if e.errno == errno.ENOENT: pass else: raise else: # New location, add a unique thing to it so there is no # collision if more than one process is trying to modify # at the same time; they can override each other, that's # ok--the last one wins. location_new = location + "-%s-%s-%s" \ % (os.getpid(), self.uuid_seed, time.time()) commonl.rm_f(location_new) os.symlink(value, location_new) os.rename(location_new, location)
def delete_file(self, target, _who, args, _files, user_path): file_path = self.arg_get(args, 'file_path', str) file_path_final, rw = self._validate_file_path(target, file_path, user_path) if not rw: raise PermissionError(f"{file_path}: is a read only location") commonl.rm_f(file_path_final) return dict()
def _power_off_do(self): # Make sure the pidfile is removed, somehow it fails to do so pid = self._power_get() if pid != None: commonl.process_terminate(pid, pidfile=self.pidfile, tag="QEMU: ") self.fsdb.set("qemu-cmdline", None) commonl.rm_f(self.pidfile + ".qmp") console_out_fname = os.path.join(self.state_dir, "console-1.log") commonl.rm_f(console_out_fname)
def delete_file(self, _target, _who, args, _files, user_path): file_path = self.arg_get(args, 'file_path', basestring) if os.path.isabs(file_path): raise RuntimeError( "%s: trying to delete a file from an area that is not allowed" % file_path) file_path_final = self._validate_file_path(file_path, user_path) commonl.rm_f(file_path_final) return dict()
def _power_off_do(self): # Make sure the pidfile is removed, somehow it fails to do so pid = self._power_get() if pid != None: commonl.process_terminate(pid, pidfile = self.pidfile, tag = "QEMU: ") self.fsdb.set("qemu-cmdline", None) commonl.rm_f(self.pidfile + ".qmp") console_out_fname = os.path.join(self.state_dir, "console-1.log") commonl.rm_f(console_out_fname)
def on(self, target, component): # open serial port to set the baud rate, then ncat gets # started and it keeps the setting; default is 9600 8n1 no # flow control, so we explicitly set what the device needs 115200. with serial.Serial(self.serial_device, 115200) as f: self.stdin = f kws = dict(target.kws) kws['name'] = 'ncat' kws['component'] = component commonl.rm_f( os.path.join(target.state_dir, f"{component}-ncat.socket")) ttbl.power.daemon_c.on(self, target, component)
def _power_off_do_bsp(self, bsp): # Make sure the pidfile is removed, somehow it fails to do so pid = self._power_get_bsp(bsp) if pid != None: commonl.process_terminate(pid, pidfile = self.pidfile[bsp], tag = "QEMU[%s]: " % bsp) self.fsdb.set("qemu-cmdline-%s" % bsp, None) commonl.rm_f(self.pidfile[bsp] + ".qmp") console_out_fname = os.path.join( self.state_dir, "console-%s.log" % bsp) commonl.rm_f(console_out_fname)
def _power_off_do_bsp(self, bsp): # Make sure the pidfile is removed, somehow it fails to do so pid = self._power_get_bsp(bsp) if pid != None: commonl.process_terminate(pid, pidfile=self.pidfile[bsp], tag="QEMU[%s]: " % bsp) self.fsdb.set("qemu-cmdline-%s" % bsp, None) commonl.rm_f(self.pidfile[bsp] + ".qmp") console_out_fname = os.path.join(self.state_dir, "console-%s.log" % bsp) commonl.rm_f(console_out_fname)
def _healthcheck(self): # not much we can do here without knowing what the interfaces # can do, we can start and stop them, they might fail to start # since they might need the target to be powered on target = self.target testcase = target.testcase for i in range(10): name = "cert%d" % i with testcase.subcase(name): with testcase.subcase("creation"): self.get(name) target.report_pass("creation works") with testcase.subcase("check-exists"): l = self.list() if name in l: target.report_pass(f"created '{name}' listed") else: target.report_fail(f"created '{name}' not listed") with testcase.subcase("save"): self.get(name, save=True) target.report_pass(f"save worked") with testcase.subcase("save_key"): commonl.rm_f(f"{target.tmpdir}.{name}.key") self.get(name, key_path=f"{target.tmpdir}.{name}.key") if os.path.isfile(f"{target.tmpdir}.{name}.key"): target.report_pass(f"save key worked") else: target.report_fail(f"save key: no file?") with testcase.subcase("save_cert"): commonl.rm_f(f"{target.tmpdir}.{name}.cert") self.get(name, cert_path=f"{target.tmpdir}.{name}.cert") if os.path.isfile(f"{target.tmpdir}.{name}.cert"): target.report_pass(f"save cert worked") else: target.report_fail(f"save cert: no file?") with testcase.subcase("removal"): self.remove(name) target.report_pass("removal works") with testcase.subcase("check-removed"): l = self.list() if name in l: target.report_fail(f"removed '{name}' is still listed") else: target.report_pass(f"removed '{name}' not listed")
def flush(self, testcase, run_name, buffers_poll, buffers, results): if 'collateral' in buffers_poll: # write the collateral images, which basically have # squares drawn on the icons we were asked to look for--we # marked the squares in detect()--we wrote one square per # expectation per polled image collateral_img = buffers_poll['collateral'] # so we can draw all the detections on the same screenshot collateral_filename = \ testcase.report_file_prefix \ + "%s.detected.png" % run_name cv2.imwrite(collateral_filename, collateral_img) del buffers_poll['collateral'] del collateral_img if not results: # if we have no results about this expectation, it # means we missed it, so record a miss for reference # First generate collateral for the screenshot, if still # not recorded collateral_missed_filename = buffers_poll.get('collateral_missed', None) if not collateral_missed_filename: collateral_missed_filename = \ testcase.report_file_prefix \ + "%s.missed.%s.png" % (run_name, self.poll_context()) screenshots = buffers_poll.get('screenshots', [ ]) if not screenshots: self.target.report_info( "%s/%s: no screenshot collateral, " "since no captures where done" % (run_name, self.name)) return last_screenshot = screenshots[-1] commonl.rm_f(collateral_missed_filename) shutil.copy(last_screenshot, collateral_missed_filename) buffers_poll['collateral_missed'] = collateral_missed_filename # lastly, symlink the specific missed expectation to the # screenshot--remember we might be sharing the screenshot # for many expectations collateral_filename = \ testcase.report_file_prefix \ + "%s.missed.%s.%s.png" % ( run_name, self.poll_context(), self.name) # make sure we symlink in the same directory commonl.rm_f(collateral_filename) os.symlink(os.path.basename(collateral_missed_filename), collateral_filename)
def _local_preexec_fn(): self._qemu_preexec_nw() # Open file descriptors for stdout/stderr to a # logfile, because -D is not really working. We # need it to check startup errors for things that # need a retry. commonl.rm_f(errfname) logfd = os.open(errfname, # O_CREAT: Always a new file, so # we can check for errors and not # get confused with previous runs os.O_WRONLY | os.O_EXCL |os.O_CREAT, 0o0644) os.dup2(logfd, 1) os.dup2(logfd, 2) os.close(logfd)
def __init__(self, userid, fail_if_new=False, roles=None): path = self.create_filename(userid) self.userid = userid if not os.path.isdir(path) and fail_if_new == False: commonl.rm_f(path) # cleanup, just in case commonl.makedirs_p(path) try: self.fsdb = ttbl.fsdb_symlink_c(path) except (AssertionError, ttbl.fsdb_c.exception) as e: if fail_if_new: raise self.user_not_existant_e("%s: no such user" % userid) self.fsdb.set('userid', userid) if roles: assert isinstance(roles, list) for role in roles: self.role_add(role)
def known_user_list(): # this now is a HACK, FIXME, repeats a lot of code, but when # the user database is moved to fsdb it will be cleaned up user_path = os.path.join(User.state_dir, "_user_*") l = [] for path in glob.glob(user_path): try: # FIXME: ugly hack, fix when we have the cache fsdb = ttbl.fsdb_symlink_c(path) userid = fsdb.get('userid', None) if userid: l.append(userid) except Exception as e: # FIXME: move to invalid_e logging.warning("cannot load user DB '%s': %s", path, e) # Wipe the file, it might have errors--it might be not # a file, so wipe hard commonl.rm_f(path) return l
def start(self, target, capturer, path): if self.macaddr != None: # got a MAC address! use it macaddr = self.macaddr else: # got to extract the MAC address # no interconnect given, let's take the ONLY one there should be interconnects = target.tags['interconnects'] if self.ic_name == None: if len(interconnects) > 1: raise RuntimeError( f"CONFIG BUG: {target.id} declares {len(interconnects)}" f" interconnects but tcpdump capturer wasn't configured " f" to select which one to capture from") ic_name = list(interconnects.keys())[0] macaddr = target.tags['interconnects'][ic_name]['mac_addr'] elif '%(' in self.ic_name: # we have a templated interconnect name # this will be expanded by generic_stream.start() # might be sth like interconnects.%(FIELD)s-%(FIELD2)s.mac_addr macaddr = f'interconnects.{self.ic_name}.mac_addr' else: # we have a fixed interconnect name if self.ic_name not in interconnects: raise RuntimeError( f"capture/{capturer}: CONFIG BUG: tcpdump_c" f" configured to use interconnect " f" '{self.ic_name}';" f" target {target.id} is not connected to it") macaddr = target.tags['interconnects'][ self.ic_name]['mac_addr'] # set this for generic_stream.start() -> note this will be # available in templates as _impl.macaddr self.kws['macaddr'] = macaddr # this matches what generic_stream.start() will do stream_filename = os.path.join(path, f"{capturer}{self.extension}") commonl.rm_f(stream_filename) r = generic_stream.start(self, target, capturer, path) commonl.verify_timeout(f"{target.id}/capture/{capturer}:tcpdump", 5, os.path.isfile, stream_filename) return r
def tb_state_save(self, filepath): """Save cookies in *path* so they can be loaded by when the object is created. :param path: Filename where to save to :type path: str """ url_safe = commonl.file_name_make_safe(self._url) if not os.path.isdir(filepath): logger.warning("%s: created state storage directory", filepath) os.mkdir(filepath) fname = filepath + "/cookies-%s.pickle" % url_safe if self.cookies == {}: logger.debug("%s: state deleted (no cookies)", self._url) commonl.rm_f(fname) else: with os.fdopen(os.open(fname, os.O_CREAT | os.O_WRONLY, 0o600), "w") as f, \ self.lock: cPickle.dump(self.cookies, f, protocol=2) logger.debug("%s: state saved %s", self._url, pprint.pformat(self.cookies))
def _known_hosts_wipe(self): # wipe known_hosts, to avoid key issues commonl.rm_f(self._known_hosts_path())
def _qemu_launch(self, bsp, kws): gdb_tcp_port = commonl.tcp_port_assigner( 1, port_range=ttbl.config.tcp_port_range) self.fsdb.set("debug-%s-gdb-tcp-port" % bsp, "%s" % gdb_tcp_port) console_out_fname = os.path.join(self.state_dir, "console-%s.log" % bsp) errfname = os.path.join(self.state_dir, "%s-stderr.log" % bsp) try: # Make sure we wipe the PID file -- sometimes a pidfile is # left over and it seems to override it, so the reading # becomes corrupt commonl.rm_f(self.pidfile[bsp]) qemu_cmdline = \ (self.qemu_cmdlines[bsp] % kws).split() \ + [ # Don't add -daemonize! This way this is part of # the process tree and killed when we kill the # parent process # We use QMP to find the PTY assigned "-qmp", "unix:%s.qmp,server,nowait" % self.pidfile[bsp], # Always start in debug mode -- this way the # whole thing is stopped until we unleash it # with QMP; this allows us to first start # daemons that we might need to start "-S", "-pidfile", self.pidfile[bsp], "-gdb", "tcp:0.0.0.0:%d" % gdb_tcp_port, ] self.fsdb.set("qemu-cmdline-%s" % bsp, qemu_cmdline[0]) except KeyError as e: msg = "bad QEMU command line specification: " \ "uninitialized key %s" % e self.log.error(msg) commonl.raise_from(RuntimeError(msg), e) self.log.debug("QEMU cmdline %s" % " ".join(qemu_cmdline)) self.tags['bsps'][bsp]['cmdline'] = " ".join(qemu_cmdline) try: _preexec_fn = getattr(self, "qemu_preexec_fn", None) def _local_preexec_fn(): if _preexec_fn: _preexec_fn() # Open file descriptors for stdout/stderr to a # logfile, because -D is not really working. We # need it to check startup errors for things that # need a retry. commonl.rm_f(errfname) logfd = os.open( errfname, # O_CREAT: Always a new file, so # we can check for errors and not # get confused with previous runs os.O_WRONLY | os.O_EXCL | os.O_CREAT, 0o0644) os.dup2(logfd, 1) os.dup2(logfd, 2) os.close(logfd) p = subprocess.Popen(qemu_cmdline, shell=False, cwd=self.state_dir, close_fds=True, preexec_fn=_local_preexec_fn) self.log.debug("QEMU %s: console @ %s" % (bsp, console_out_fname)) # Give it a few secs to start, the pidfile has been # deleted before starting -- note 4 was found by # ad-hoc experimentation, sometimes depending on system load it # takes more or less. timeout = 10 ts0 = time.time() while True: if time.time() - ts0 > timeout: lines = [] with open(errfname) as f: count = 0 for line in f: lines.append("log: " + line) if count > 5: lines.append("log: ...") break raise RuntimeError("QEMU %s: did not start after %.0fs\n" "%s" % (bsp, timeout, "\n".join(lines))) try: if self._qmp_running(bsp): # FIXME: race condition ttbl.daemon_pid_add(p.pid) return True except RuntimeError as e: self.log.warning("QEMU %s: can't read QMP: %s" % (bsp, str(e))) # fall through, let it retry # Check errors during startup with open(errfname, "r") as logf: causes_for_retry = [ # bah, race condition: since we chose the # port we wanted to use and until we # started using it someone took it. Retry 'Failed to bind socket: Address already in use', ] for line in logf: for cause in causes_for_retry: if cause in line: self.log.info( "QEMU %s: retrying because found in " "logfile: %s", bsp, cause) return False time.sleep(0.25) # nothing runs after this, either it returns or raises except (OSError, ValueError) as e: self.log.debug("QEMU %s: launch failure: %s", bsp, e) raise
def on(self, target, component): stderrf_name = os.path.join(target.state_dir, component + "-" + self.name + ".stderr") kws = dict(target.kws) kws.update(self.kws) kws['component'] = component # render the real commandline against kws _cmdline = [] for i in self.cmdline: # some older Linux distros complain if this string is unicode _cmdline.append(str(i % kws)) target.log.info("%s: command line: %s" % (component, " ".join(_cmdline))) if self.env_add: env = dict(os.environ) env.update(self.env_add) else: env = os.environ pidfile = self.pidfile % kws commonl.rm_f(pidfile) stderrf = open(stderrf_name, "w+") try: p = subprocess.Popen(_cmdline, env=env, cwd=target.state_dir, stderr=stderrf, bufsize=0, shell=False, universal_newlines=False) if self.mkpidfile: with open(pidfile, "w+") as pidf: pidf.write("%s" % p.pid) except TypeError as e: # This happens on misconfiguration ## TypeError: execve() arg 3 contains a non-string value if 'execve() arg 3' in str(e): target.log.exception( "Ensure environment settings are not set to None", e) if 'execve()' in str(e): target.log.exception("Possible target misconfiguration: %s", e) count = 0 for i in _cmdline: target.log.error("cmdline %d: [%s] %s", count, type(i).__name__, i) count += 1 for key, val in env.iteritems(): target.log.error("env %s: [%s] %s", key, type(val).__name__, val) raise except OSError as e: raise self.start_e("%s: %s failed to start: %s" % (component, self.name, e)) del stderrf # we don't care for this file here if self.precheck_wait: time.sleep(self.precheck_wait) pid = commonl.process_started(pidfile, self.path, component + "-" + self.name, target.log, self.verify, (_cmdline, )) if pid == None: raise self.start_e("%s: %s failed to start" % (component, self.name)) ttbl.daemon_pid_add(pid)
def _report(self, level, alevel, ulevel, _tc, tag, message, attachments): """ Report data to log files for a possible failure report later We don't even check the levels, we log everything here by INFO > 2. We report to the file ``TAG LEVEL CODE MESSAGE`` which we'll parse later to generate the report. """ # This is what marks all the testcase runs being done, so we # can use it to wrap things up. if _tc == tcfl.tc.tc_global: if message.startswith("COMPLETION"): self._finalize(_tc) return # Okie, this is a hack -- this means this is a testcase, but # we need something better. if getattr(_tc, "skip_reports", False) == True: return # We don't operate on the global reporter fake TC # Note we open the file for every thing we report -- we can be # running *A LOT* of stuff in parallel and run out of file # descriptors. if tag == "INFO" and level > 2: return code = self._get_code() if not code in self.fs: f = codecs.open(os.path.join(_tc.tmpdir, "report-" + code + ".txt"), "w", encoding='utf-8', errors='ignore') self.fs[code] = f.name else: f = codecs.open(self.fs[code], "a+b", encoding='utf-8', errors='ignore') # Extract the target name where this message came from (if the # reporter is a target) if isinstance(_tc, tcfl.tc.target_c): tgname = " @" + _tc.fullid + _tc.bsp_suffix() else: tgname = " @local" with contextlib.closing(f): # Remove the ticket from the ident string, as it will be # the same for all and makes no sense to have it. ident = tcfl.msgid_c.ident() if ident.startswith(_tc._ident): ident = ident[len(_tc._ident):] if ident == "": # If empty, give it a to snip token that we'll replace # later in mkreport ident = "<snip>" _prefix = "%s %d %s%s\t" % (tag, level, ident, tgname) self._write(f, u"%s %s\n" % (_prefix, message)) if attachments != None: assert isinstance(attachments, dict) for key, attachment in attachments.iteritems(): self._report_f_attachment(f, _prefix, key, attachment) f.flush() # FIXME: this is an unsmokable mess and needs to be fixed. # will defer cleaning up until we move the whole reporting # to be done with a Jinja2 template so the report paths # are fully split. if self.junit and message.startswith("COMPLETION") \ or message.startswith("COMPLETION failed") \ or message.startswith("COMPLETION error") \ or message.startswith("COMPLETION blocked") \ or message.startswith("COMPLETION skipped") \ or (message.startswith("COMPLETION passed") and ( _tc.tag_get('report_always', (False, ))[0] == True or self.text_report_pass )): self._mkreport(tag, code, _tc, message, f) # Wipe the file, it might have errors--it might be not # a file, so wipe hard commonl.rm_f(self.fs[code]) del self.fs[code]
def _qemu_launch(self, bsp, kws): gdb_tcp_port = commonl.tcp_port_assigner( 1, port_range = ttbl.config.tcp_port_range) self.fsdb.set("debug-%s-gdb-tcp-port" % bsp, "%s" % gdb_tcp_port) console_out_fname = os.path.join( self.state_dir, "console-%s.log" % bsp) errfname = os.path.join( self.state_dir, "%s-stderr.log" % bsp) try: # Make sure we wipe the PID file -- sometimes a pidfile is # left over and it seems to override it, so the reading # becomes corrupt commonl.rm_f(self.pidfile[bsp]) qemu_cmdline = \ (self.qemu_cmdlines[bsp] % kws).split() \ + [ # Don't add -daemonize! This way this is part of # the process tree and killed when we kill the # parent process # We use QMP to find the PTY assigned "-qmp", "unix:%s.qmp,server,nowait" % self.pidfile[bsp], # Always start in debug mode -- this way the # whole thing is stopped until we unleash it # with QMP; this allows us to first start # daemons that we might need to start "-S", "-pidfile", self.pidfile[bsp], "-gdb", "tcp:0.0.0.0:%d" % gdb_tcp_port, ] self.fsdb.set("qemu-cmdline-%s" % bsp, qemu_cmdline[0]) except KeyError as e: msg = "bad QEMU command line specification: " \ "uninitialized key %s" % e self.log.error(msg) commonl.raise_from(RuntimeError(msg), e) self.log.debug("QEMU cmdline %s" % " ".join(qemu_cmdline)) self.tags['bsps'][bsp]['cmdline'] = " ".join(qemu_cmdline) try: _preexec_fn = getattr(self, "qemu_preexec_fn", None) def _local_preexec_fn(): if _preexec_fn: _preexec_fn() # Open file descriptors for stdout/stderr to a # logfile, because -D is not really working. We # need it to check startup errors for things that # need a retry. commonl.rm_f(errfname) logfd = os.open(errfname, # O_CREAT: Always a new file, so # we can check for errors and not # get confused with previous runs os.O_WRONLY | os.O_EXCL |os.O_CREAT, 0o0644) os.dup2(logfd, 1) os.dup2(logfd, 2) os.close(logfd) p = subprocess.Popen(qemu_cmdline, shell = False, cwd = self.state_dir, close_fds = True, preexec_fn = _local_preexec_fn) self.log.debug("QEMU %s: console @ %s" % (bsp, console_out_fname)) # Give it a few secs to start, the pidfile has been # deleted before starting -- note 4 was found by # ad-hoc experimentation, sometimes depending on system load it # takes more or less. timeout = 10 ts0 = time.time() while True: if time.time() - ts0 > timeout: lines = [] with open(errfname) as f: count = 0 for line in f: lines.append("log: " + line) if count > 5: lines.append("log: ...") break raise RuntimeError("QEMU %s: did not start after %.0fs\n" "%s" % (bsp, timeout, "\n".join(lines))) try: if self._qmp_running(bsp): # FIXME: race condition ttbl.daemon_pid_add(p.pid) return True except RuntimeError as e: self.log.warning("QEMU %s: can't read QMP: %s" % (bsp, str(e))) # fall through, let it retry # Check errors during startup with open(errfname, "r") as logf: causes_for_retry = [ # bah, race condition: since we chose the # port we wanted to use and until we # started using it someone took it. Retry 'Failed to bind socket: Address already in use', ] for line in logf: for cause in causes_for_retry: if cause in line: self.log.info( "QEMU %s: retrying because found in " "logfile: %s", bsp, cause) return False time.sleep(0.25) # nothing runs after this, either it returns or raises except (OSError, ValueError) as e: self.log.debug("QEMU %s: launch failure: %s", bsp, e) raise
def on(self, target, _component): # Bring up the lower network interface; lower is called # whatever (if it is a physical device) or _bNAME; bring it # up, make it promiscuous mode = self._get_mode(target) if mode == 'vlan': # our lower is a physical device, our upper is a device # which till tag for eth vlan %(vlan) ifname = commonl.if_find_by_mac(target.tags['mac_addr'], physical=True) commonl.if_remove_maybe("b%(id)s" % target.kws) kws = dict(target.kws) kws['ifname'] = ifname subprocess.check_call( "/usr/sbin/ip link add" " link %(ifname)s name b%(id)s" " type vlan id %(vlan)s" #" protocol VLAN_PROTO" #" reorder_hdr on|off" #" gvrp on|off mvrp on|off loose_binding on|off" % kws, shell=True) subprocess.check_call( # bring lower up "/usr/sbin/ip link set dev %s up promisc on" % ifname, shell=True) elif mode == 'physical': ifname = commonl.if_find_by_mac(target.tags['mac_addr']) subprocess.check_call( # bring lower up "/usr/sbin/ip link set dev %s up promisc on" % ifname, shell=True) self._if_rename(target) elif mode == 'virtual': # We do not have a physical device, a bridge, to serve as # lower commonl.if_remove_maybe("_b%(id)s" % target.kws) subprocess.check_call("/usr/sbin/ip link add" " name _b%(id)s" " type bridge" % target.kws, shell=True) subprocess.check_call("/usr/sbin/ip link add" " link _b%(id)s name b%(id)s" " type macvlan mode bridge; " % target.kws, shell=True) subprocess.check_call( # bring lower up "/usr/sbin/ip link set" " dev _b%(id)s" " up promisc on" % target.kws, shell=True) else: raise AssertionError("Unknown mode %s" % mode) # Configure the IP addresses for the top interface subprocess.check_call( # clean up existing address "/usr/sbin/ip add flush dev b%(id)s " % target.kws, shell=True) subprocess.check_call( # add IPv6 # if this fails, check Network Manager hasn't disabled ipv6 # sysctl -a | grep disable_ipv6 must show all to 0 "/usr/sbin/ip addr add" " %(ipv6_addr)s/%(ipv6_prefix_len)s dev b%(id)s " % target.kws, shell=True) subprocess.check_call( # add IPv4 "/usr/sbin/ip addr add" " %(ipv4_addr)s/%(ipv4_prefix_len)d" " dev b%(id)s" % target.kws, shell=True) # Bring up the top interface, which sets up ther outing subprocess.check_call( "/usr/sbin/ip link set dev b%(id)s up promisc on" % target.kws, shell=True) target.fsdb.set('power_state', 'on') # Start tcpdump on the network? # # The value of the tcpdump property, if not None, is the # filename we'll capture to. tcpdump = target.fsdb.get('tcpdump') if tcpdump: assert not os.path.sep in tcpdump \ and tcpdump != "" \ and tcpdump != os.path.pardir \ and tcpdump != os.path.curdir, \ "Bad filename for TCP dump capture '%s' specified as " \ " value to property *tcpdump*: must not include" % tcpdump # per ttbd:make_ticket(), colon splits the real username # from the ticket owner = target.owner_get().split(":")[0] assert owner, "BUG? target not owned on power on?" capfile = os.path.join(target.files_path, owner, tcpdump) # Because it is in the user's area, # we assume the user knows what he is doing to overwrite it, # so we'll remove any first commonl.rm_f(capfile) pidfile = os.path.join(target.state_dir, "tcpdump.pid") logfile = os.path.join(target.state_dir, "tcpdump.log") cmdline = [ "/usr/sbin/tcpdump", "-U", "-i", "_b%(id)s" % target.kws, "-w", capfile ] try: logf = open(logfile, "a") target.log.info("Starting tcpdump with: %s", " ".join(cmdline)) p = subprocess.Popen(cmdline, shell=False, cwd=target.state_dir, close_fds=True, stdout=logf, stderr=subprocess.STDOUT) except OSError as e: raise RuntimeError("tcpdump failed to start: %s" % e) ttbl.daemon_pid_add(p.pid) # FIXME: race condition if it died? with open(pidfile, "w") as pidfilef: pidfilef.write("%d" % p.pid) pid = commonl.process_started( # Verify it started pidfile, "/usr/sbin/tcpdump", verification_f=os.path.exists, verification_f_args=(capfile, ), timeout=20, tag="tcpdump", log=target.log) if pid == None: raise RuntimeError("tcpdump failed to start after 5s")
def on(self, target, _component): ic = target # Note the rename (target -> ic) # Create records for each target that we know will connect to # this interconnect, place them in the directory TARGET/dnsmasq.hosts dirname = os.path.join(ic.state_dir, "dnsmasq.hosts") shutil.rmtree(dirname, ignore_errors=True) commonl.makedirs_p(dirname) tftp_dirname = os.path.join(ic.state_dir, "tftp.root") shutil.rmtree(tftp_dirname, ignore_errors=True) commonl.makedirs_p(tftp_dirname, 0o0775) ttbl.pxe.setup_tftp_root(tftp_dirname) # creates the dir commonl.rm_f(os.path.join(ic.state_dir, "dnsmasq.log")) # Find the targets that connect to this interconnect and # collect their IPv4/6/MAC addresses to create the record and # DHCP info; in theory we wouldn't need to create the host # info, as the DHCP host info would do it--doesn't hurt # FIXME: parallelize for many dhcp_hosts = collections.defaultdict(dict) for target in ttbl.config.targets.values(): interconnects = target.tags.get('interconnects', {}) # iterate interconnects this thing connects to for interconnect_id, interconnect in interconnects.items(): if interconnect_id != ic.id: continue addrs = [] mac_addr = interconnect.get('mac_addr', None) if mac_addr: dhcp_hosts[target]['mac_addr'] = mac_addr ipv4_addr = interconnect.get('ipv4_addr', None) if ipv4_addr: dhcp_hosts[target]['ipv4_addr'] = ipv4_addr addrs.append(ipv4_addr) ipv6_addr = interconnect.get('ipv6_addr', None) if ipv6_addr: dhcp_hosts[target]['ipv6_addr'] = ipv6_addr addrs.append(ipv6_addr) if addrs: # Create a file for each target that will connect to # this interconnect with open(os.path.join(dirname, target.id), "w+") as f: for addr in addrs: f.write("%s\t%s %s.%s\n" % (addr, target.id, target.id, ic.id)) # Create a configuration file # # configl has all the options with template values which we # expand later. with open(os.path.join(ic.state_dir, "dnsmasq.conf"), "w+") as f: configl = [ "no-hosts", # only files in... "hostsdir=%(path)s/dnsmasq.hosts", # ..this dir # we are defining a domain .NETWORKNAME "domain=%(id)s", "local=/%(id)s/", # serve only on the in the interface for this network; # listen-address not needed since we specify # interface--having a hard time making listen-address # only work anyway # FIXME: hardcoded to knowing the network interface # name is called bTARGET "interface=b%(id)s", # need to use this so we only bind to our # interface and we can run multiple dnsmasqa and coexists # with whichever are in the system "bind-interfaces", "except-interface=lo", # if a plain name (w/o domain name) is not found in the # local database, do not forward it upstream "domain-needed", # Needs an A record "%(ipv4_addr)s %(id)s", created in on() # DISABLED: unknown why, this messes up resolution of # plain names # auth-server=%(id)s,b%(id)s", "auth-zone=%(id)s,b%(id)s", "dhcp-authoritative", # Enable TFTP server to STATEDIR/tftp.root "enable-tftp", "tftp-root=%(path)s/tftp.root", # all files TFTP is to send have to be owned by the # user running it (the same one running this daemon) "tftp-secure", # logging -- can be accessed with a console, see class doc "log-dhcp", "log-facility=%(path)s/dnsmasq.log", ] # Add stuff based on having ipv4/6 support # # dhcp-range activates the DHCP server # host-record creates a record for the host that # represents the domain zone; but not sure it is working # all right. addrs = [] ic_ipv4_addr = ic.kws.get('ipv4_addr', None) if ic_ipv4_addr: addrs.append(ic_ipv4_addr) # IPv4 server address so we can do auth-server configl.append("host-record=%(id)s,%(ipv4_addr)s") # we let DNSMASQ figure out the range from the # configuration of the network interface and we only # allow (static) the ones set below with dhcp-host configl.append("dhcp-range=%(ipv4_addr)s,static") ic_ipv6_addr = ic.kws.get('ipv6_addr', None) if ic_ipv6_addr: addrs.append(ic_ipv6_addr) # IPv6 server address so we can do auth-server configl.append("host-record=%(id)s,[%(ipv6_addr)s]") # FIXME: while this is working, it is still not giving # the IPv6 address we hardcoded in the doc :/ ipv6_prefix_len = ic.kws['ipv6_prefix_len'] network = ipaddress.IPv6Network(str(ic_ipv6_addr + "/" + str(ipv6_prefix_len)), strict=False) configl.append( "dhcp-range=%s,%s,%s" % (ic_ipv6_addr, network.broadcast_address, ipv6_prefix_len)) # Create A record for the server/ domain # this is a separat file in DIRNAME/dnsmasq.hosts/NAME if addrs: configl.append("listen-address=" + ",".join(addrs)) with open(os.path.join(dirname, ic.id), "w+") as hf: for addr in addrs: hf.write("%s\t%s\n" % (addr, ic.id)) for config in configl: f.write(config % ic.kws + "\n") # For each target we know can connect, create a dhcp-host entry for target, data in dhcp_hosts.items(): infol = [ # we set a tag after the host name to match a # host-specific dhcp-option line to it "set:" + target.id, data['mac_addr'] ] if 'ipv4_addr' in data: infol.append(data['ipv4_addr']) if 'ipv6_addr' in data: # IPv6 addr in [ADDR] format, per man page infol.append("[" + data['ipv6_addr'] + "]") infol.append(target.id) infol.append("infinite") f.write("dhcp-host=" + ",".join(infol) + "\n") # next fields can be in the target or fall back to the # values from the interconnect kws = target.kws bsps = target.tags.get('bsps', {}).keys() if bsps: # take the first BSP in sort order...yeah, not a # good plan bsp = sorted(bsps)[0] kws['bsp'] = bsp ttbl.pxe.tag_get_from_ic_target(kws, 'pos_http_url_prefix', ic, target) ttbl.pxe.tag_get_from_ic_target(kws, 'pos_nfs_server', ic, target) ttbl.pxe.tag_get_from_ic_target(kws, 'pos_nfs_path', ic, target) # FIXME: this is very confusing here, since it is what # ttbl.pxe.pos_cmdline_opts is relaying on in a way # and we'd need a way to make it machine specific too; # as well, in some places like for pos_mode==pxe this # is all set in the server sides, while the client in # tcfl.pos has a lot of it in the client side; we need # a unified source. f.write( "dhcp-option=tag:%(id)s,option:root-path,%(pos_nfs_server)s:%(pos_nfs_path)s,soft,nfsvers=4\n" % kws) # If the target declares a BSP (at this point of the # game, it should), figure out which architecture is # so we can point it to the right file. if bsp: # try ARCH or efi-ARCH # override with anything the target declares in config arch = None boot_filename = None if 'pos_tftp_boot_filename' in target.tags: boot_filename = target.tags['pos_tftp_boot_filename'] elif bsp in ttbl.pxe.architectures: arch = ttbl.pxe.architectures[bsp] arch_name = bsp boot_filename = arch_name + "/" + arch.get( 'boot_filename', None) elif "efi-" + bsp in ttbl.pxe.architectures: arch_name = "efi-" + bsp arch = ttbl.pxe.architectures[arch_name] boot_filename = arch_name + "/" + arch.get( 'boot_filename', None) if boot_filename: f.write("dhcp-option=tag:%(id)s," % kws + "option:bootfile-name," + boot_filename + "\n") if ic_ipv4_addr: f.write("dhcp-option=tag:%(id)s," % kws + "option:tftp-server," + ic_ipv4_addr + "\n") if ic_ipv6_addr: f.write("dhcp-option=tag:%(id)s," % kws + "option:tftp-server," + ic_ipv4_addr + "\n") else: raise RuntimeError( "%s: TFTP/PXE boot mode selected, but no boot" " filename can be guessed for arch/BSP %s/%s;" " declare tag pos_tftp_boot_filename?" % (target.id, arch_name, bsp)) # note the rename we did target -> ic ttbl.power.daemon_c.on(self, ic, _component)
def _report(self, level, alevel, ulevel, _tc, tag, message, attachments): """ Report data to log files for a possible failure report later We don't even check the levels, we log everything here by INFO <= 4. We report to the file ``TAG LEVEL CODE MESSAGE`` which we'll parse later to generate the report. """ # This is what marks all the testcase runs being done, so we # can use it to wrap things up. if _tc == tcfl.tc.tc_global: return # Okie, this is a hack -- this means this is a testcase, but # we need something better. if getattr(_tc, "skip_reports", False) == True: return # We don't operate on the global reporter fake TC # Note we open the file for every thing we report -- we can be # running *A LOT* of stuff in parallel and run out of file # descriptors. if tag == "INFO" and level > 4: return code = self._get_code() if not code in self.fs: f = codecs.open( os.path.join(_tc.tmpdir, "report-" + code + ".txt"), "w", encoding = 'utf-8', errors = 'ignore') self.fs[code] = f.name else: f = codecs.open(self.fs[code], "a+b", encoding = 'utf-8', errors = 'ignore') # Extract the target name where this message came from (if the # reporter is a target) if isinstance(_tc, tcfl.tc.target_c): tgname = " @" + _tc.fullid + _tc.bsp_suffix() else: tgname = " @local" with contextlib.closing(f): # Remove the ticket from the ident string, as it will be # the same for all and makes no sense to have it. ident = tcfl.msgid_c.ident() if ident.startswith(_tc._ident): ident = ident[len(_tc._ident):] if ident == "": # If empty, give it a to snip token that we'll replace # later in mkreport ident = "<snip>" _prefix = "%s %d %s%s\t" % (tag, level, ident, tgname) self._write(f, u"%s %s\n" % (_prefix, message)) if attachments != None: assert isinstance(attachments, dict) for key, attachment in attachments.iteritems(): self._report_f_attachment(f, _prefix, key, attachment) f.flush() # This is an indication that the testcase is done and we # can generate final reports if message.startswith("COMPLETION "): self._mkreport(tag, code, _tc, message) # Wipe the file, it might have errors--it might be not # a file, so wipe hard commonl.rm_f(self.fs[code]) del self.fs[code]
def delete_file(self, _target, _who, args, _files, user_path): file_path = self._arg_get(args, 'file_path') file_path_final = self._validate_file_path(file_path, user_path) commonl.rm_f(file_path_final) return dict()
def _report(self, level, alevel, ulevel, _tc, tag, message, attachments): """ Report data to log files for a possible failure report later We don't even check the levels, we log everything here by INFO <= 4. We report to the file ``TAG LEVEL CODE MESSAGE`` which we'll parse later to generate the report. """ # This is what marks all the testcase runs being done, so we # can use it to wrap things up. if _tc == tcfl.tc.tc_global: return # Okie, this is a hack -- this means this is a testcase, but # we need something better. if getattr(_tc, "skip_reports", False) == True: return # We don't operate on the global reporter fake TC # Note we open the file for every thing we report -- we can be # running *A LOT* of stuff in parallel and run out of file # descriptors. if tag == "INFO" and level > 4: return code = self._get_code() if not code in self.fs: f = codecs.open(os.path.join(_tc.tmpdir, "report-" + code + ".txt"), "w", encoding='utf-8', errors='replace') self.fs[code] = f.name else: f = codecs.open(self.fs[code], "a+b", encoding='utf-8', errors='replace') # Extract the target name where this message came from (if the # reporter is a target) if isinstance(_tc, tcfl.tc.target_c): tgname = " @" + _tc.fullid + _tc.bsp_suffix() else: tgname = " @local" with contextlib.closing(f): # Remove the ticket from the ident string, as it will be # the same for all and makes no sense to have it. ident = self.ident_simplify(tcfl.msgid_c.ident(), _tc.kws.get('runid', ''), _tc.kws.get('tc_hash', "")) if ident == "": # If empty, give it a to snip token that we'll replace # later in mkreport ident = "<snip>" _prefix = "%s %d %s%s\t" % (tag, level, ident, tgname) self._write(f, u"%s %s\n" % (_prefix, _mkutf8(message))) if attachments != None: assert isinstance(attachments, dict) for key, attachment in attachments.iteritems(): self._report_f_attachment(f, _prefix, key, attachment) f.flush() # This is an indication that the testcase is done and we # can generate final reports if message.startswith("COMPLETION "): self._mkreport(tag, code, _tc, message) # Wipe the file, it might have errors--it might be not # a file, so wipe hard commonl.rm_f(self.fs[code]) del self.fs[code]
def _client_wipe(self, name, cert_client_path): # wipe without complaining if not there for extension in self.client_extensions: commonl.rm_f(os.path.join(cert_client_path, name + "." + extension))
def _qemu_console_on(self, target, component): # Run steps that are needed when we power on WRT to the # consoles console_interface = getattr(target, "console", None) if console_interface == None: # no console interface? skip return # set the generation for the consoles, so clients now the # output is new consolel = list(target.console.impls.keys()) for console in consolel: ttbl.console.generation_set(target, console) # # Find out which PTS nodes have been allocated by QEMU # # If we have declared any consoles using PTYs (preferred # method), a PTS pair has been allocated by the kernel and now # we need to find which one is it, so the console can write to # it. The generic console implementation expects a file in the # target state directory called console-NAME.write where to # write to. # # We'll symlink console-NAME.write -> /dev/pts/XYZ # # This function uses QMP to query QEMU for all the chardevs, # then goes over each finding the ones that are using # PTYs. Those that match a declared console implementation in # the target.console interface will be symlinked. # with qmp_c(os.path.join(target.state_dir, "qemu.qmp")) as qmp: # This will return a list such as: # ## [ ## {"frontend-open": true, "filename": "gdb", "label": "#chr034"}, ## {"frontend-open": true, "filename": "vc", "label": "parallel0"}, ## {"frontend-open": true, "filename": "disconnected:tcp:0.0.0.0:62949,server", "label": "gdb"}, ## {"frontend-open": true, "filename": "unix:.../qemu.qmp,server", "label": "compat_monitor0"} ## {"frontend-open": true, "filename": "pty:/dev/pts/5", "label": "ttyS0"}, ## ] r = qmp.command("query-chardev") for d in r: # Each entry is a dictionary with a bunch of fields, # for which we are interested in the *label* and # *filename* fields. # ## { ## "frontend-open": true, ## "filename": "pty:/dev/pts/5", ## "label": "ttyS0" ## } # # if no label or filename, log an error--malformed; if # no pty or not declared as a console for ttbd, log an # info, we just don't care about it. label = d.get('label', None) if label == None: target.log.error( "QEMU:%s/console: ignoring entry missing label: %s", component, json.dumps(d, skipkeys=True)) continue filename = d.get('filename', None) if filename == None: target.log.error( "QEMU:%s/console: ignoring entry '%s': missing filename", component, label) continue if not filename.startswith("pty:"): target.log.info( "QEMU:%s/console: ignoring entry '%s': no pty (%s)", component, label, filename) continue _, pts_name = filename.split(":", 1) console_impl = console_interface.impls.get(label, None) if console_impl == None: target.log.debug( "QEMU:%s/console: ignoring entry '%s': no console declared for it", component, label) continue write_filename = os.path.join(target.state_dir, "console-" + label + ".write") target.log.info("QEMU/%s/console: '%s' uses PTS %s", component, label, pts_name) commonl.rm_f(write_filename) os.symlink(pts_name, write_filename)