def evemu_create_device(self, did, descriptor): """ Create a new input device :param str did: device id, or name of the device (eg: *mouse 1*, *keyboard main*, *left joytick*...) :param str descriptor: input device descriptor, as described by Linux's *evemu-describe* (without the comments); see :const:`descriptor_mouse`, :const:`descriptor_kbd`. """ # we'll evolve this into being able to create multiple mice of # different implementations (eg: evemu based vs hardware # based)...because we might want to type working with multiple mice assert isinstance(did, basestring), \ "did is type %s; expected string" % type(did) descriptor = descriptor.strip() if not isinstance(did, basestring) \ or not descriptor_valid_regex.search(descriptor.strip()): raise tc.error_e( "did %s: descriptor (%s) is not a string or it does not" " match the expected format in" " tcfl.target_ext_input.descriptor_valid_regex" % (did, type(descriptor)), dict(descriptor=descriptor)) target = self.target # Copy the device descriptor to the device, using SSH if # possible (faster) if hasattr(target, 'ssh') and target.ssh.tunnel_up(): with open(os.path.join(target.testcase.tmpdir, "input-dev.desc"), "w") as f: f.write(descriptor) f.flush() target.ssh.copy_to(f.name, "/tmp/input-dev.desc") else: target.shell.string_copy_to_file(descriptor, "/tmp/input-dev.desc") output = target.shell.run("%s /tmp/input-dev.desc & sleep 1s" % self.evemu_device, output=True, trim=True) # this prints ## NAME: /dev/input/eventX, so get it input_regex = re.compile( r"(?P<name>[^:]+\s*)(?P<devname>\/dev\/input\/event.*)$") m = input_regex.search(output) if not m: raise tc.error_e( "can't locate /dev/input/event* name in output to" " create new input device", dict(output=output)) # take only the basename; /dev/input will be added by the FIFO # itself, it is less chars to print self.devices[did] = os.path.basename(m.groupdict()['devname'])
def run(self, timeout=None): """\ Run the expectation loop on the testcase until all expectations pass or the timeout is exceeded. :param int timeout: (optional) maximum time to wait for all expectations to be met (defaults to :attr:`tcfl.expecter.expecter_c.timeout`) """ if timeout == None: timeout = self._timeout else: assert isinstance(timeout, int) # Fresh start self.buffers.clear() self.ts0 = time.time() _active_ts = self.ts0 functors_pending = self.functors while True: ts = time.time() td = ts - self.ts0 _functors_pending = list(functors_pending) for f, a, has_to_pass in _functors_pending: if a == None: args = (self, ) else: args = (self, ) + a try: self._log("running %s %s @%s" % (f.__name__, f.origin, args), dlevel=5) _ts0 = time.time() r = f(*args) _ts1 = time.time() except tc.pass_e as e: if has_to_pass: # Report pass information, including attachments tc.result_c.report_from_exception(self.testcase, e) r = True # Once it passes, we don't check it anymore if has_to_pass and r != None: self.remove(f, a) # if this returned normally or raised a tc.pass_e, # then it means the test passed; except if it # returns None, then ignore it (might have # been a poller) if self.have_to_pass == 0: raise tc.pass_e("all expectations found") # Check like this because we want to run the check # functions before determining a timeout as some of them # might also check on their own and offer better messages if td >= self.timeout: raise tc.error_e("Timed out (%.2fs)" % self.timeout) if ts - _active_ts > self.active_period: self.testcase._targets_active() _active_ts = ts time.sleep(self._poll_period)
def _efibootmgr_output_parse(target, output): boot_order_match = _boot_order_regex.search(output) if not boot_order_match: raise tc.error_e("can't extract boot order", attachments = dict(target = target, output = output)) boot_order = boot_order_match.groupdict()['boot_order'].split(',') entry_matches = re.findall(_entry_regex, output) # returns a list of [ ( HHHH, ENTRYNAME ) ], HHHH hex digits, all str boot_entries = [] for entry in entry_matches: if _name_is_pos_boot(entry[1]): section = 0 # POS (PXE, whatever), boot first elif _name_is_local_boot(entry[1]): section = 10 # LOCAL, boot after else: section = 20 # others, whatever try: boot_index = boot_order.index(entry[0]) boot_entries.append(( entry[0], entry[1], section, boot_index )) except ValueError: # if the entry is not in the boot order, that is fine, # ignore it pass return boot_order, boot_entries
def run(self, timeout = None): """\ Run the expectation loop on the testcase until all expectations pass or the timeout is exceeded. :param int timeout: (optional) maximum time to wait for all expectations to be met (defaults to :attr:`tcfl.expecter.expecter_c.timeout`) """ if timeout == None: timeout = self._timeout else: assert isinstance(timeout, int) # Fresh start self.buffers.clear() self.ts0 = time.time() _active_ts = self.ts0 functors_pending = self.functors while True: ts = time.time() td = ts - self.ts0 _functors_pending = list(functors_pending) for f, a, has_to_pass in _functors_pending: if a == None: args = (self,) else: args = (self, ) + a try: self._log( "running %s %s @%s" % (f.__name__, f.origin, args), dlevel = 5) _ts0 = time.time() r = f(*args) _ts1 = time.time() except tc.pass_e as e: if has_to_pass: # Report pass information, including attachments tc.result_c.report_from_exception(self.testcase, e) r = True # Once it passes, we don't check it anymore if has_to_pass and r != None: self.remove(f, a) # if this returned normally or raised a tc.pass_e, # then it means the test passed; except if it # returns None, then ignore it (might have # been a poller) if self.have_to_pass == 0: raise tc.pass_e("all expectations found") # Check like this because we want to run the check # functions before determining a timeout as some of them # might also check on their own and offer better messages if td >= self.timeout: raise tc.error_e("Timed out (%.2fs)" % self.timeout) if ts - _active_ts > self.active_period: self.testcase._targets_active() _active_ts = ts time.sleep(self._poll_period)
def _efibootmgr_output_parse(target, output): boot_order_match = _boot_order_regex.search(output) if not boot_order_match: raise tc.error_e("can't extract boot order", attachments=dict(target=target, output=output)) boot_order = boot_order_match.groupdict()['boot_order'].split(',') entry_matches = re.findall(_entry_regex, output) # returns a list of [ ( HHHH, ENTRYNAME ) ], HHHH hex digits, all str boot_entries = [] for entry in entry_matches: if _name_is_pos_boot(entry[1]): section = 0 # POS (PXE, whatever), boot first elif _name_is_tcf_local_boot(entry[1]): section = 05 # TCF local boots, always first so we # can control elif _name_is_local_boot(entry[1]): section = 10 # LOCAL, boot after else: section = 20 # others, whatever try: boot_index = boot_order.index(entry[0]) boot_entries.append((entry[0], entry[1], section, boot_index)) except ValueError: # if the entry is not in the boot order, that is fine, # ignore it pass return boot_order, boot_entries
def check_filter(self, _objdir, arch, board, _filter, origin=None): """ This is going to be called by the App Builder's build function to evaluate if we need to filter out a build of a testcase. In any other case, it will be ignored. :param str objdir: name of the Zephyr's object directory where to find configuration files :param str arch: name of the architecture we are building :param str board: Zephyr's board we are building :param str _filter: Zephyr's sanity Check style filter expression :param str origin: where does this come from? """ if not origin: origin = commonl.origin_get() if _filter == None or _filter == "": return self.target.report_info("filter: processing '%s' @%s" % (_filter, origin), dlevel=1) self.target.report_info("filter: reading defconfig", dlevel=2) _defconfig = self.config_file_read() defconfig = {} for key, value in _defconfig.iteritems(): # The testcase.ini filter language doesn't prefix the # CONFIG_ stuff, so we are going to strip it with [7:] if key.startswith("CONFIG_"): defconfig[key[7:]] = value # The testcase.yaml filter language prefixes with # CONFIG_ stuff, so we don't strip it defconfig[key] = value self.target.report_info("filter: evaluating", dlevel=2) try: res = commonl.expr_parser.parse(_filter, defconfig) self.target.report_info("filter: evaluated defconfig: %s" % res, dlevel=1) if res == False: raise tc.skip_e("filter '%s' @ %s causes TC to be skipped" % (_filter, origin)) else: self.target.report_info( "filter '%s' @ %s causes TC to be continued" % (_filter, origin), dlevel=2) except SyntaxError as se: raise tc.error_e("filter: failed processing '%s' @ %s: %s" % (_filter, origin, se))
def _device_get(self, did): if did in self.devices: return self.devices[did] # if this is an evemu-device (syntehtic) that is already # running but we don't really know about it, let's try to find # it. File /tmp/evemu-DID.log was created output = self.target.shell.run("cat /tmp/evemu-%s.log || true" % did, output=True, trim=True) eventX = self._evemu_device_log_extract(output) if eventX: self.devices[did] = eventX return eventX raise tc.error_e("INPUT: device ID %s does not exist" % did)
def check_filter(self, _objdir, arch, board, _filter, origin = None): """ This is going to be called by the App Builder's build function to evaluate if we need to filter out a build of a testcase. In any other case, it will be ignored. :param str objdir: name of the Zephyr's object directory where to find configuration files :param str arch: name of the architecture we are building :param str board: Zephyr's board we are building :param str _filter: Zephyr's sanity Check style filter expression :param str origin: where does this come from? """ if not origin: origin = commonl.origin_get() if _filter == None or _filter == "": return self.target.report_info("filter: processing '%s' @%s" % (_filter, origin), dlevel = 1) self.target.report_info("filter: reading defconfig", dlevel = 2) _defconfig = self.config_file_read() defconfig = {} for key, value in _defconfig.iteritems(): # The testcase.ini filter language doesn't prefix the # CONFIG_ stuff, so we are going to strip it with [7:] if key.startswith("CONFIG_"): defconfig[key[7:]] = value # The testcase.yaml filter language prefixes with # CONFIG_ stuff, so we don't strip it defconfig[key] = value self.target.report_info("filter: evaluating", dlevel = 2) try: res = commonl.expr_parser.parse(_filter, defconfig) self.target.report_info("filter: evaluated defconfig: %s" % res, dlevel = 1) if res == False: raise tc.skip_e("filter '%s' @ %s causes TC to be skipped" % (_filter, origin)) else: self.target.report_info( "filter '%s' @ %s causes TC to be continued" % (_filter, origin), dlevel = 2) except SyntaxError as se: raise tc.error_e("filter: failed processing '%s' @ %s: %s" % (_filter, origin, se))
def setup(self, console = None): """ Setup the shell for scripting operation In the case of a bash shell, this: - sets the prompt to something easer to latch on to - disables command line editing - traps errors in shell execution """ target = self.target testcase = target.testcase # dereference which console we are using; we don't want to # setup our trackers below to the "default" console and leave # it floating because then it will get confused if we switch # default consoles. console = target.console._console_get(console) self.run('export PS1="TCF-%s:$PS1"' % self.target.kws['tc_hash'], console = console) # disable line editing for proper recording of command line # when running bash; otherwise the scrolling readline does # messes up the output self.run('test ! -z "$BASH" && set +o vi +o emacs', console = console) # Trap the shell to complain loud if a command fails, and catch it # See that '' in the middle, is so the catcher later doesn't # get tripped by the command we sent to set it up self.run("trap 'echo ERROR''-IN-SHELL' ERR", console = console) testcase.expect_global_append( # add a detector for a shell error, make sure to name it # after the target and console it will monitor so it # doesn't override other targets/consoles we might be # touching in parallel target.console.text( "ERROR-IN-SHELL", name = "%s:%s: shell error" % (target.want_name, console), console = console, timeout = 0, poll_period = 1, raise_on_found = tc.error_e("error detected in shell")), # if we have already added detectors for this, that's # fine, ignore them skip_duplicate = True )
def _efibootmgr_output_parse(target, output): boot_order_match = _boot_order_regex.search(output) if not boot_order_match: raise tc.error_e("can't extract boot order", attachments=dict(target=target, output=output)) boot_order = boot_order_match.groupdict()['boot_order'].split(',') entry_matches = re.findall(_entry_regex, output) # returns a list of [ ( HHHH, ENTRYNAME ) ], HHHH hex digits, all str target.report_info("POS/UEFI: found %d EFI boot entries" % len(entry_matches), dlevel=3, attachments=dict(boot_entries=entry_matches)) boot_to_pos = target.pos.capabilities.get('boot_to_pos', None) boot_entries = [] for entry in entry_matches: if _name_is_pos_boot(entry[1], boot_to_pos): section = 0 # POS (PXE, whatever), boot first elif _name_is_tcf_local_boot(entry[1], boot_to_pos): section = 05 # TCF local boots, always first so we # can control elif _name_is_local_boot(entry[1], boot_to_pos): section = 10 # LOCAL, boot after else: section = 20 # others, whatever try: boot_index = boot_order.index(entry[0]) # FIXME: should keep this list sorted? boot_entries.append((entry[0], entry[1], section, boot_index)) except ValueError: # if the entry is not in the boot order, that is fine, # ignore it pass target.report_info( "POS/UEFI: sorted %d EFI boot entries with boot_to_pos method '%s'" % (len(entry_matches), boot_to_pos), dlevel=1, alevel=0, attachments=dict(sorted_boot_entries=boot_entries)) return boot_order, boot_entries
def boot_config_multiroot(target, boot_dev, image): """ Configure the target to boot using the multiroot """ boot_dev = target.kws['pos_boot_dev'] # were we have mounted the root partition root_dir = "/mnt" linux_kernel_file, linux_initrd_file, linux_options = \ _linux_boot_guess(target, image) if linux_kernel_file == None: raise tc.blocked_e( "Cannot guess a Linux kernel to boot", dict(target = target)) # remove absolutization (some specs have it), as we need to copy from # mounted filesystems if os.path.isabs(linux_kernel_file): linux_kernel_file = linux_kernel_file[1:] if linux_initrd_file and os.path.isabs(linux_initrd_file): linux_initrd_file = linux_initrd_file[1:] if linux_options == None or linux_options == "": target.report_info("WARNING! can't figure out Linux cmdline " "options, taking defaults") # below we'll add more stuff linux_options = "console=tty0 root=SOMEWHERE" # MULTIROOT: indicate which image has been flashed to this # partition # saved by pos_multiroot.mountfs root_part_dev = target.root_part_dev root_part_dev_base = os.path.basename(root_part_dev) target.property_set('pos_root_' + root_part_dev_base, image) # /boot EFI system partition is always /dev/DEVNAME1 (first # partition), we partition like that # FIXME: we shouldn't harcode this boot_part_dev = boot_dev + target.kws['p_prefix'] + "1" kws = dict( boot_dev = boot_dev, boot_part_dev = boot_part_dev, root_part_dev = root_part_dev, root_part_dev_base = root_part_dev_base, root_dir = root_dir, linux_kernel_file = linux_kernel_file, linux_kernel_file_basename = os.path.basename(linux_kernel_file), linux_initrd_file = linux_initrd_file, linux_options = linux_options, ) if linux_initrd_file: kws['linux_initrd_file_basename'] = os.path.basename(linux_initrd_file) else: kws['linux_initrd_file_basename'] = None kws.update(target.kws) if linux_options: # # Maybe mess with the Linux boot options # target.report_info("linux cmdline options: %s" % linux_options) # FIXME: can this come from config? linux_options_replace = { # we want to use hard device name rather than LABELS/UUIDs, as # we have reformated and those will have changed "root": "/dev/%(root_part_dev_base)s" % kws, # we have created this in pos_multiroot and will only # replace it if the command line option is present. "resume": "/dev/disk/by-label/tcf-swap", } # FIXME: can this come from config? # We harcode a serial console on the device where we know the # framework is listening linux_options_append = \ "console=%(linux_serial_console_default)s,115200n8" % kws linux_options_append += " " + target.rt.get('linux_options_append', "") linux_options += " " + linux_options_append for option, value in linux_options_replace.iteritems(): regex = re.compile(r"\b" + option + r"=\S+") if regex.search(linux_options): linux_options = re.sub( regex, option + "=" + linux_options_replace[option], linux_options) else: linux_options += " " + option + "=" + value kws['linux_options'] = linux_options target.report_info("linux cmdline options (modified): %s" % linux_options) # Now generate the UEFI system partition that will boot the # system; we always override it, so we don't need to decide if it # is corrupted or whatever; we'll mount it in /boot (which now is # the POS /boot) # # Mount the /boot fs # # Try to assume it is ok, try to repair it if not; rsync the # kernels in there, it is faster for repeated operation/ # target.report_info("POS/EFI: checking %(boot_part_dev)s" % kws) output = target.shell.run( "fsck.fat -aw /dev/%(boot_part_dev)s || true" % kws, output = True, trim = True) # FIXME: parse the output to tell if there was a problem; when bad # but recovered, we'll see # # 0x41: Dirty bit is set. Fs was not properly unmounted and some data may be corrupt. # Automatically removing dirty bit. # Performing changes. # /dev/sda1: 11 files, 4173/261372 clusters # When ok # ## $ sudo fsck.vfat -wa /dev/nvme0n1p1 ## fsck.fat 4.1 (2017-01-24) ## /dev/sda1: 39 files, 2271/33259 clusters # When really hosed it won't print the device line, so we look for # that # ## $ fsck.vfat -wa /dev/trash ## fsck.fat 4.1 (2017-01-24) ## Logical sector size (49294 bytes) is not a multiple of the physical sector size. good_regex = re.compile("^/dev/%(boot_part_dev)s: [0-9]+ files, " "[0-9]+/[0-9]+ clusters$" % kws, re.MULTILINE) if not good_regex.search(output): target.report_info( "POS/EFI: /dev/%(boot_part_dev)s: formatting EFI " "filesystem, fsck couldn't fix it" % kws, dict(output = output)) target.shell.run("mkfs.fat -F32 /dev/%(boot_part_dev)s; sync" % kws) target.report_info( "POS/EFI: /dev/%(boot_part_dev)s: mounting in /boot" % kws) target.shell.run(" mount /dev/%(boot_part_dev)s /boot; " " mkdir -p /boot/loader/entries " % kws) # Do we have enough space? if not, remove the oldest stuff that is # not the file we are looking for # This prints ## $ df --output=pcent /boot ## Use% ## 6% output = target.shell.run("df --output=pcent /boot", output = True) regex = re.compile(r"^\s*(?P<percent>[\.0-9]+)%$", re.MULTILINE) match = regex.search(output) if not match: raise tc.error_e("Can't determine the amount of free space in /boot", dict(output = output)) used_space = float(match.groupdict()['percent']) if used_space > 75: target.report_info( "POS/EFI: /dev/%(boot_part_dev)s: freeing up space" % kws) # List files in /boot, sort by last update (when we rsynced # them) ## 2018-10-29+08:48:48.0000000000 84590 /boot/EFI/BOOT/BOOTX64.EFI ## 2018-10-29+08:48:48.0000000000 84590 /boot/EFI/systemd/systemd-bootx64.efi ## 2019-05-14+13:25:06.0000000000 7192832 /boot/vmlinuz-4.12.14-110-default ## 2019-05-14+13:25:08.0000000000 9688340 /boot/initrd-4.12.14-110-default ## 2019-05-14+13:25:14.0000000000 224 /boot/loader/entries/tcf-boot.conf ## 2019-05-14+13:25:14.0000000000 54 /boot/loader/loader.conf output = target.shell.run( # that double \\ needed so the shell is the one using it # as a \t, not python converting to a sequence "find /boot/ -type f -printf '%T+\\t%s\\t%p\\n' | sort", output = True, trim = True) # delete the first half entries over 300k except those that # match the kernels we are installing to_remove = [] _linux_initrd_file = kws.get("linux_initrd_file", "%%NONE%%") if linux_initrd_file == None: _linux_initrd_file = "%%NONE%%" for line in output.splitlines(): _timestamp, size_s, path = line.split(None, 3) size = int(size_s) if size > 300 * 1024 \ and not kws['linux_kernel_file_basename'] in path \ and not _linux_initrd_file in path: to_remove.append(path) # get older half and wipe it--this means the first half, as we # sort from older to newer to_remove = to_remove[:len(to_remove)//2] for path in to_remove: # we could do them in a single go, but we can exceed the # command line length -- lazy to optimize it target.shell.run("rm -f %s" % path) # Now copy all the files needed to boot to the root of the EFI # system partition mounted in /boot; remember they are in /mnt/, # our root partition target.shell.run( "time -p rsync --force --inplace /mnt/%(linux_kernel_file)s" " /boot/%(linux_kernel_file_basename)s" % kws) if kws.get("linux_initrd_file", None): target.shell.run( "time -p rsync --force --inplace /mnt/%(linux_initrd_file)s" " /boot/%(linux_initrd_file_basename)s" % kws) # we are the only one who cuts the cod here (yeah, direct Spanish # translation for the reader's entertainment), and if not wipe'm to # eternity; otherwise systemd will boot something prolly in # alphabetical order, not what we want target.shell.run("/usr/bin/rm -rf /boot/loader/entries/*.conf") # remember paths to the bootloader are relative to /boot # merge these two tcf_boot_conf = """\ cat <<EOF > /boot/loader/entries/tcf-boot.conf title TCF-driven local boot linux /%(linux_kernel_file_basename)s options %(linux_options)s """ if kws.get("linux_initrd_file", None): tcf_boot_conf += "initrd /%(linux_initrd_file_basename)s\n" tcf_boot_conf += "EOF\n" target.shell.run(tcf_boot_conf % kws) # Install new -- we wiped the /boot fs new anyway; if there are # multiple options already, bootctl shall be able to handle it. # Don't do variables in the any casewe will poke with them later # on anyway. Why? Because there is space for a race condition that # will leave us with the system booting off the localdisk vs the # network for PXE--see efibootmgr_setup() target.shell.run("bootctl update --no-variables" " || bootctl install --no-variables;" " sync") # Now mess with the EFIbootmgr # FIXME: make this a function and a configuration option (if the # target does efibootmgr) _efibootmgr_setup(target, boot_dev, 1) # umount only if things go well # Shall we try to unmount in case of error? nope, we are going to # have to redo the whole thing anyway, so do not touch it, in case # we are jumping in for manual debugging target.shell.run("umount /dev/%(boot_part_dev)s" % kws)
def up(self, tempt=None, user=None, login_regex=re.compile('login:'******'[Pp]assword:'), shell_setup=True, timeout=120): """Wait for the shell in a console to be ready Giving it ample time to boot, wait for a :data:`shell prompt <shell_prompt_regex>` and set up the shell so that if an error happens, it will print an error message and raise a block exception. Optionally login as a user and password. >>> target.shell.up(user = '******', password = '******') :param str tempt: (optional) string to send before waiting for the loging prompt (for example, to send a newline that activates the login) :param str user: (optional) if provided, it will wait for *login_regex* before trying to login with this user name. :param str password: (optional) if provided, and a password prompt is found, send this password. :param str login_regex: (optional) if provided (string or compiled regex) and *user* is provided, it will wait for this prompt before sending the username. :param str password_regex: (optional) if provided (string or compiled regex) and *password* is provided, it will wait for this prompt before sending the password. :param int delay_login: (optional) wait this many seconds before sending the user name after finding the login prompt. :param bool shell_setup: (optional, default) setup the shell up by disabling command line editing (makes it easier for the automation) and set up hooks that will raise an exception if a shell command fails. :param int timeout: [optional] seconds to wait for the login prompt to appear """ assert tempt == None or isinstance(tempt, basestring) assert user == None or isinstance(user, basestring) assert isinstance(login_regex, (basestring, re._pattern_type)) assert delay_login >= 0 assert password == None or isinstance(password, basestring) assert isinstance(password_regex, (basestring, re._pattern_type)) assert isinstance(shell_setup, bool) assert timeout > 0 target = self.target def _login(target): # If we have login info, login to get a shell prompt target.expect(login_regex) if delay_login: target.report_info("Delaying %ss before login in" % delay_login) time.sleep(delay_login) target.send(user) if password: target.expect(password_regex) target.send(password) try: original_timeout = self.target.testcase.tls.expecter.timeout self.target.testcase.tls.expecter.timeout = timeout if tempt: tries = 0 while tries < self.target.testcase.tls.expecter.timeout: try: self.target.send(tempt) if user: _login(self.target) target.expect(self.linux_prompt_regex) break except tc.error_e as _e: if tries == self.target.testcase.tls.expecter.timeout: raise tc.error_e( "Waited too long (%ds) for shell to come up " "(did not receive '%s')" % (self.target.testcase.tls.expecter.timeout, self.shell_prompt_regex.pattern)) continue else: if user: _login(self.target) target.expect(self.shell_prompt_regex) finally: self.target.testcase.tls.expecter.timeout = original_timeout if shell_setup: # self.run('export PS1="TCF-%s:$PS1"' % target.kws['tc_hash']) # disable line editing for proper recording of command line # when running bash; otherwise the scrolling readline does # messes up the output self.run('test ! -z "$BASH" && set +o vi +o emacs') # Trap the shell to complain loud if a command fails, and catch it # See that '' in the middle, is so the catcher later doesn't # get tripped by the command we sent to set it up self.run("trap 'echo ERROR''-IN-SHELL' ERR") self.target.on_console_rx("ERROR-IN-SHELL", result='errr', timeout=False)
def _device_get(self, did): if did not in self.devices: raise tc.error_e("INPUT: device ID %s does not exist" % did) return self.devices[did]
def sequence_send(self, sequence): """ Send a sequence of events to one or more devices :param str sequence: sequence of events to send to input devices as a string. No syntax verification is done in the sequence string, it is assumed correct. Syntax verification is done for the list version of the sequence. :param list sequence: sequence of events to send as a list of individual events. Each event is a tuple in the forms: >>> ( 'WAIT', FLOAT ) >>> ( DEVNAME, EVTYPE, CODE, VALUE[, 'SYNC' ] ) - *DEVNAME*: device name to send it to; for *evemu* devices this will be converted in the target to */dev/input/DEVNAME* - *EVTYPE*: type of event (as supported by the hardware): - *EV_SYN*: synchronizatione vent - *EV_KEY*: press a key (eg: keyboard) - *EV_REL*: relative movement (eg: mouse) - *EV_ABS*: absolute movement (eg: touchpad) - *EV_MSC*: miscellaneous events - *EV_SW*: set a switch - *EV_LED*: set a led - *EV_SND*: play a sound - *EV_REP*: set repetition - *EV_FF*: set force feedback - *EV_PWR*: set power - *EV_FF_STATUS*: force feedback status - *CODE*: depends on the event type, which indicates the action being taken - *KEY_\**: press/release keys (eg: *KEY_A*, *KEY_B*... - *BTN_\**: press/release buttons (eg: *BTN_LEFT*, *BTN_RIGHT*...) - *ABS_\**: set absolute axes (eg: *ABS_X*, *ABS_Y*) - *SW_\**: miscelaneous switches - *MSC_\**: miscellaneous settings - *SND_{CLICK,BELL,TONE}*: play a sound - *REP_{DELAY,PERIOD}*: set repetition configuration - *LED_\**: set different leds - *VALUE*: integer value that depends on the *EVTYPE* and *CODE*; for example to press a key: - 1: press key/button - 0: release key/button See https://www.kernel.org/doc/html/latest/input/event-codes.html for more information. Examples: - press and release key *A*, holding key for 0.1 seconds: >>> [ >>> ( DEVNAME, 'EV_KEY', 'KEY_A', 1, 'SYNC') , >>> ( 'WAIT', 0.1 ), >>> ( DEVNAME, 'EV_KEY', 'KEY_A', 0, 'SYNC') >>> ] - set a touchpad with 100,100 range to the center of its range and double click left button >>> [ >>> ( DEVNAME, 'EV_ABS', 'ABS_X', 50') , >>> ( DEVNAME, 'EV_ABS', 'ABS_Y', 50') >>> ( DEVNAME, 'EV_KEY', 'BTN_LEFT', 1, 'SYNC') >>> ( 'WAIT', 0.1 ), >>> ( DEVNAME, 'EV_KEY', 'BTN_LEFT', 0, 'SYNC') >>> ( 'WAIT', 0.3 ), >>> ( DEVNAME, 'EV_KEY', 'BTN_LEFT', 1, 'SYNC') >>> ( 'WAIT', 0.1 ), >>> ( DEVNAME, 'EV_KEY', 'BTN_LEFT', 0, 'SYNC') >>> ] - Type the *@* sign (there is no keycode for it, must press shift + plus *2* then release both): >>> [ >>> ( DEVNAME, 'EV_KEY', 'KEY_LEFTSHIFT', 1 ) , >>> ( DEVNAME, 'EV_KEY', 'KEY_2', 1, 'SYNC' ) , >>> ( 'WAIT', 0.1 ), >>> ( DEVNAME, 'EV_KEY', 'KEY_A', 0, 'SYNC' ) >>> ( DEVNAME, 'EV_KEY', 'KEY_LEFTSHIFT', 0 ) , >>> ] """ if isinstance(sequence, basestring): # send it first to a file, then in a single shot to the # FIFO; this ensures the timing on sending the sequence # over the serial port (which can be slow if there is a # lot of data) does not influence the sequence's timing. self.target.shell.run("cat > /tmp/evemu.data\n%s\x04" % sequence) self.target.shell.run("cat /tmp/evemu.data > /tmp/evemu.fifo") return # expect it to be a list of tuples count = -1 self.target.send("cat > /tmp/evemu.data") for entry in sequence: count += 1 assert isinstance(entry, (list, tuple)) devcmd = entry[0] if devcmd == "WAIT": try: _ = float(entry[1]) self.target.send("WAIT %s\n" % entry[1]) except ValueError as e: raise tc.error_e("INPUT sequence #%d: WAIT:" " missing or invalid seconds field;" " can't convert to float: %s" % (count, e)) self.target.console.write("WAIT %s\n" % entry[1]) continue if len(entry) != 4 and len(entry) == 5: raise tc.error_e("INPUT sequence #%d: event:" " invalid number of entries;" " got %d, expect 4 or 5 " % (count, len(entry))) evtype = entry[1] if self.evtype_regex_valid.search(evtype): raise tc.error_e( "INPUT sequence #%d: event type '%s'" " invalid; must match %s" % (count, evtype, self.evtype_regex_valid.pattern)) code = entry[2] if self.code_regex_valid.search(code): raise tc.error_e("INPUT sequence #%d: code '%s'" " invalid; must match %s" % (count, code, self.code_regex_valid.pattern)) value = entry[3] if self.value_regex_valid.search(value): raise tc.error_e( "INPUT sequence #%d: code '%s'" " invalid; must match %s" % (count, value, self.value_regex_valid.pattern)) if len(entry) == 5: sync = entry[4] if sync != "SYNC": raise tc.error_e("INPUT sequence #%d: 5th field '%s'" " invalid; can only be 'SYNC'" % (count, sync)) else: sync = "" self.target.send("%s %s %s %s\n" % evtype, code, value, sync) # complete the cat command we started above self.target.send("\x04" % sequence) self.target.shell.run("cat /tmp/evemu.data > /tmp/evemu.fifo")
def evemu_target_setup(self, ic): """ Setup a target to use the input subsystem with EVEMU - creates a simple keyboard - creates a simple absolute mouse This can be used only on Linux machines with uinput support and will drive the mouse via commands in the console. Requirements: - The server must have been configured to drop *evemu.bin.tar.gz* in the web server for test content; thus a client accessing *http://SERVERIP/ttbd-images-misc/evemu.bin.tar.gz* will be able to get this content. The server install instructions provide for this to be the case. """ assert isinstance(ic, tc.target_c) target = self.target # Download the new evemu binaries with statically linked # libraries. output = target.shell.run("evemu-event --fifo || true", output=True) # if evemu-event is installed, it will print it's help ## Usage: evemu-event [--sync] <device> --type <type> --code <code> --value <value> # # if the --fifo extensions are installed, there will be a # --fifo in there if '<device>' in output: # evemu-event is installed self.evemu_event = "evemu-event" self.evemu_device = "evemu-device" if '--fifo' in output: # evemu-event has --fifo support self.evemu_event_fifo = "evemu-event --fifo=" target.report_info("INPUT/evemu: distro's with --fifo") else: # upload helper with msgid_c(): target.shell.string_to_file( # Each line is in the format: # # - <DEVICE> <TYPE> <CODE> <VALUE> [SYNC] # - WAIT <MILLISECS> # - empty (ignored) # # the helper is a wee hack since it has more # overhead with evemu with the support """\ #! /bin/bash rm -f $1; mkfifo $1 tail -f $1 | while read dev typetime code value sync; do [ "$dev" == "WAIT" ] && sleep $typetime && continue [ "${sync:-}" == SYNC ] && sync="--sync" echo evemu-event ${sync:-} /dev/input/$dev --type $type --code $code --value $value done """, "/usr/local/bin/evemu-event-fifo") target.shell.run( "chmod a+x /usr/local/bin/evemu-event-fifo") self.evemu_event_fifo = "/usr/local/bin/evemu-event-fifo " target.report_info( "INPUT/evemu: distro's with FIFO shell helper") else: with msgid_c(): # There is no evemu in the system, so let's upload our # semistatic build from the POS cache. rsync_server = target.kws.get( 'pos_rsync_server', ic.kws.get('pos_rsync_server', None)) if rsync_server == None: raise tc.error_e( "INPUT/evemu: there is no place where to download" " evemu.bin.tar.gz for, need the" " target or interconnect to export" " *pos_rsync_server* with the location") http_server = "http://" \ + rsync_server.replace("::images", "/ttbd-images-misc") target.shell.run("curl --noproxy '*' -sk %s/evemu.bin.tar.gz" " --output /tmp/evemu.bin.tar.gz" % http_server) target.shell.run( "tar xvvf /tmp/evemu.bin.tar.gz --overwrite -C /") self.evemu_event = "/opt/evemu/bin/evemu-event" self.evemu_device = "/opt/evemu/bin/evemu-device" self.evemu_event_fifo = "/opt/evemu/bin/evemu-event --fifo=" target.report_info("INPUT/evemu: TCF's static build w/ --fifo") self.evemu_create_device("default_mouse", descriptor_mouse) self.evemu_create_device("default_keyboard", descriptor_kbd) # start the FIFO pipe target.shell.run("nohup %s/tmp/evemu.fifo >& /tmp/evemu.log &" % self.evemu_event_fifo)
def configure(testcase, target, app_src): target.kws_required_verify([ 'zephyr_board', 'zephyr_kernelname' ]) # Adds Zephyr's build zephyr_* vars to the target's keywords testcase_file = inspect.getfile(type(testcase)) zephyr_extra_args = testcase._targets[target.want_name]['kws'].get( 'app_zephyr_options', "") + " " if len(app_src) > 1: zephyr_extra_args += " ".join(app_src[1:]) # Add tags to the target srcdir = tcfl.app.get_real_srcdir(testcase_file, app_src[0]) # Decide if this is cmake style (Zephyr almost 1.10) or not # and store it in the keywords, as we'll use in different # places to make decissions on if we can run or not. ZEPHYR_BASE = os.environ.get('ZEPHYR_BASE', "__ZEPHYR_BASE_not_defined__") if os.path.exists(os.path.join(ZEPHYR_BASE, "CMakeLists.txt")): if not os.path.exists(os.path.join(srcdir, "CMakeLists.txt")): raise tc.error_e( "%s: Zephyr App is not cmake based, but Zephyr @%s is" % (srcdir, ZEPHYR_BASE)) is_cmake = True # cmake needs the extra args in -DXXX format .. sighs this # will blow up at some point, because we are assuming that # all extra args passed are VAR=VALUE zephyr_extra_args = " ".join("-D" + arg for arg in zephyr_extra_args.split()) else: if os.path.exists(os.path.join(srcdir, "CMakeLists.txt")): raise tc.error_e( "%s: Zephyr App is cmake based, but Zephyr @%s is not" % (srcdir, ZEPHYR_BASE)) is_cmake = False target.kws_set( { 'zephyr_srcdir': srcdir, # Place all the build output in the temp directory, to # avoid polluting the source directories 'zephyr_objdir': os.path.join( testcase.tmpdir, "outdir-%(tc_hash)s-%(tg_hash)s-%(zephyr_board)s" % target.kws), 'zephyr_is_cmake': is_cmake, }, bsp = target.bsp) if not target.bsp in target.bsps_stub: # Set arguments only for this BSP, not for all of them, # and only if we are not building a stub. Note we get the # options from app_zephyr_options + OPT1 + OPT2... added # after app_src in 'app_zephyr = (SRC, OPT1, OPT2...) target.kw_set('zephyr_extra_args', zephyr_extra_args, bsp = target.bsp) else: # When we are building stubs, we do not take the options # from the project itself -- stub needs no options target.kw_set('zephyr_extra_args', "", bsp = target.bsp) # Do we introduce boot configuration or not? if not testcase.build_only: # If a testcase is build only, it won't run in HW so we # don't need to introduce a boot delay to allow serial # ports to be ready. # This is needed because build only testcases might be # disabling options that are needed to implement boot # delays, like clock stuff global boot_delay _boot_delay = boot_delay.get(target.type, 1000) if not target.bsp in target.bsps_stub: target.zephyr.config_file_write( "500_boot_config", """\ # Introduce a boot delay of 1s to make sure the system # has had time to setup the serial ports and start recording # from them CONFIG_BOOT_BANNER=y CONFIG_BOOT_DELAY=%d """ % _boot_delay, bsp = target.bsp) else: target.zephyr.config_file_write( "500_boot_config", """\ # Stubs be quiet and don't delay, ready ASAP CONFIG_BOOT_BANNER=n CONFIG_BOOT_DELAY=0 """, bsp = target.bsp) # Add stubs for other BSPs in this board if there are none # We will run this each time we go for a BSP build, but it is fine for bsp_stub in target.bsps_stub.keys(): target.stub_app_add( bsp_stub, app_zephyr, os.path.join(ZEPHYR_BASE, 'tests', 'booting', 'stub'))
def console_rx_eval(expecter, target, regex, console = None, _timeout = None, result = None, uid = None): """ Check what came on a console and act on it :param str uid: (optional) identifier to use to store offset data """ if hasattr(regex, "pattern"): what = regex.pattern else: what = regex regex = re.compile(re.escape(regex)) if not uid: uid = console_mk_uid(target, what, console, _timeout, result) console_id_name, console_code = console_mk_code(target, console) # These were set by the poller of = expecter.buffers.get(console_code, None) if of == None: # FIXME: debug lof output here? expecter->tc backlink? return None ofd = of.fileno() ts = time.time() # Get the offset we saved before as the last part where we looked # at. If none, then get the last offset the poller has # recorded. Otherwise, just default to look from the start # Note the idea is each eval function has a different offset where # it is looking at. Likewise the poller for each console. offset_poller_code = "offset_" + console_code offset = expecter.buffers_persistent.get( uid, expecter.buffers_persistent.get(offset_poller_code, 0)) if _timeout != False: timeout = expecter.timeout if _timeout == None else _timeout # We can do timeout checks that provide better information # than a generic 'timeout' if ts - expecter.ts0 > timeout: of.seek(offset) # so we report console from where searched raise tc.error_e( "expected console output '%s' from console '%s:%s' " \ "NOT FOUND after %.1f s" \ % (what, target.id, console_id_name, ts - expecter.ts0), { 'target': target, "console output": of }) # mmap the whole file (which doesn't alter the file pointer) # # We have to mmap as the file might be getting huge and thus, # reading line by line might be dumb. # # However, we only search starting at @offset, which is set later # to the last success searching we had. So we shan't really map # the whole file, shall map on demand. stat_info = os.fstat(ofd) if stat_info.st_size == 0: # Nothing to read return None with contextlib.closing(mmap.mmap(of.fileno(), 0, mmap.MAP_PRIVATE, mmap.PROT_READ, 0)) as mapping: target.report_info("looking for `%s` in console %s:%s @%d-%d at " "%.2fs [%s]" % (what, target.fullid, console_id_name, offset, stat_info.st_size, ts - expecter.ts0, of.name), dlevel = 3) m = regex.search(mapping[offset:]) if m: new_offset = offset + m.end() expecter.buffers_persistent[uid] = new_offset if result == None or result == "pass": # raising pass gets stopped at expecter.run(), so we # print instead, so we can see the console offset_tip = of.tell() # we report console from where searched of.seek(offset) # so we report console from where searched target.report_pass( "found expected `%s` in console `%s:%s` at %.2fs" % (what, target.fullid, console_id_name, ts - expecter.ts0), { "console output": of }, dlevel = 1, alevel = 2) of.seek(offset_tip) raise tc.pass_e( "found expected `%s` in console `%s:%s` at %.2fs" % (what, target.fullid, console_id_name, ts - expecter.ts0), {'target': target }) elif result == "fail": of.seek(offset) # so we report console from where searched raise tc.failed_e( "found expected (for failure) `%s` in console " "`%s:%s` at %.2fs" % (what, target.fullid, console_id_name, ts - expecter.ts0), { 'target': target, "console output": of }) elif result == "error" or result == "errr": of.seek(offset) # so we report console from where searched raise tc.error_e( "found expected (for error) `%s` in console " "`%s:%s` at %.2fs" % (what, target.fullid, console_id_name, ts - expecter.ts0), { 'target': target, "console output": of }) elif result == "skip": of.seek(offset) # so we report console from where searched raise tc.skip_e( "found expected (for skip) `%s` in console " "'%s:%s' at %.2fs" % (what, target.fullid, console_id_name, ts - expecter.ts0), { 'target': target, "console output": of }) else: of.seek(offset) # so we report console from where searched raise tc.blocked_e( "BUG: invalid result requested (%s)" % result) return None
def _disk_partition(target): # we assume we are going to work on the boot device device_basename = target.kws['pos_boot_dev'] device = "/dev/" + device_basename target.shell.run('swapoff -a || true') # in case we autoswapped # find device size (FIXME: Linux specific) dev_info = None for blockdevice in target.pos.fsinfo.get('blockdevices', []): if blockdevice['name'] == device_basename: dev_info = blockdevice break else: raise tc.error_e( "%s: can't find information about this block device -- is " "the right pos_boot_device set in the configuration?" % device_basename, dict(fsinfo=pprint.pformat(target.pos.fsinfo))) size_gb = int(dev_info['size']) / 1024 / 1024 / 1024 target.report_info("POS: %s is %d GiB in size" % (device, size_gb), dlevel=2) partsizes = target.kws.get('pos_partsizes', None) if partsizes == None: raise tc.blocked_e( "Can't partition target, it doesn't " "specify pos_partsizes tag", {'target': target}) partsize_l = partsizes.split(":") partsize_l = [int(_partsize) for _partsize in partsize_l] boot_size = partsize_l[0] swap_size = partsize_l[1] scratch_size = partsize_l[2] root_size = partsize_l[3] # note we set partition #0 as boot cmdline = """parted -a optimal -ms %(device)s unit GiB \ mklabel gpt \ mkpart primary fat32 0%% %(boot_size)s \ set 1 boot on \ mkpart primary linux-swap %(boot_size)s %(swap_end)s \ mkpart primary ext4 %(swap_end)s %(scratch_end)s \ """ % dict( device=device, boot_size=boot_size, swap_end=boot_size + swap_size, scratch_end=boot_size + swap_size + scratch_size, ) offset = boot_size + swap_size + scratch_size root_devs = [] # collect the root devices pid = 4 while offset + root_size < size_gb: cmdline += ' mkpart primary ext4 %d %d' % (offset, offset + root_size) offset += root_size root_devs.append(device_basename + target.kws['p_prefix'] + "%d" % pid) pid += 1 target.shell.run(cmdline) # Now set the root device information, so we can pick stuff to # format quick for root_dev in root_devs: target.property_set('pos_root_' + root_dev, "EMPTY") # Re-read partition tables target.shell.run('partprobe %s' % device) # now format filesystems # # note we only format the system boot partition (1), the linux # swap(2) and the linux scratch space (3) boot_dev = device + target.kws['p_prefix'] + "1" swap_dev = device + target.kws['p_prefix'] + "2" home_dev = device + target.kws['p_prefix'] + "3" # Note: use FAT vs VFAT: vfat name translation creates issues when # doing long file names; fat32 does not have that problem. target.shell.run("mkfs.fat -F32 -n TCF-BOOT " + boot_dev) target.shell.run("mkswap -L tcf-swap " + swap_dev) target.shell.run("mkfs.ext4 -FqL tcf-scratch " + home_dev)
def _disk_partition(target): # we assume we are going to work on the boot device device_basename = target.kws['pos_boot_dev'] device = "/dev/" + device_basename target.shell.run('swapoff -a || true') # in case we autoswapped # find device size (FIXME: Linux specific) dev_info = None for blockdevice in target.pos.fsinfo.get('blockdevices', []): if blockdevice['name'] == device_basename: dev_info = blockdevice break else: raise tc.error_e( "%s: can't find information about this block device -- is " "the right pos_boot_device set in the configuration?" % device_basename, dict(fsinfo = pprint.pformat(target.pos.fsinfo))) size_gb = int(dev_info['size']) / 1024 / 1024 / 1024 target.report_info("POS: %s is %d GiB in size" % (device, size_gb), dlevel = 2) partsizes = target.kws.get('pos_partsizes', None) if partsizes == None: raise tc.blocked_e( "Can't partition target, it doesn't " "specify pos_partsizes tag", { 'target': target } ) partsize_l = partsizes.split(":") partsize_l = [ int(_partsize) for _partsize in partsize_l ] boot_size = partsize_l[0] swap_size = partsize_l[1] scratch_size = partsize_l[2] root_size = partsize_l[3] # note we set partition #0 as boot cmdline = """parted -a optimal -ms %(device)s unit GiB \ mklabel gpt \ mkpart primary fat32 0%% %(boot_size)s \ set 1 boot on \ mkpart primary linux-swap %(boot_size)s %(swap_end)s \ mkpart primary ext4 %(swap_end)s %(scratch_end)s \ """ % dict( device = device, boot_size = boot_size, swap_end = boot_size + swap_size, scratch_end = boot_size + swap_size + scratch_size, ) offset = boot_size + swap_size + scratch_size root_devs = [] # collect the root devices pid = 4 while offset + root_size < size_gb: cmdline += ' mkpart primary ext4 %d %d' % (offset, offset + root_size) offset += root_size root_devs.append(device_basename + target.kws['p_prefix'] + "%d" % pid) pid += 1 target.shell.run(cmdline) # Now set the root device information, so we can pick stuff to # format quick for root_dev in root_devs: target.property_set('pos_root_' + root_dev, "EMPTY") # Re-read partition tables target.shell.run('partprobe %s' % device) # now format filesystems # # note we only format the system boot partition (1), the linux # swap(2) and the linux scratch space (3) boot_dev = device + target.kws['p_prefix'] + "1" swap_dev = device + target.kws['p_prefix'] + "2" home_dev = device + target.kws['p_prefix'] + "3" # Note: use FAT vs VFAT: vfat name translation creates issues when # doing long file names; fat32 does not have that problem. target.shell.run("mkfs.fat -F32 -n TCF-BOOT " + boot_dev) target.shell.run("mkswap -L tcf-swap " + swap_dev) target.shell.run("mkfs.ext4 -FqL tcf-scratch " + home_dev)
def up(self, tempt=None, user=None, login_regex=re.compile('login:'******'Password:'******'%s')" % (self.target.testcase.tls.expecter.timeout, self.linux_shell_prompt_regex.pattern)) continue else: if user: _login(self.target) self.target.expect(self.linux_shell_prompt_regex) finally: self.target.testcase.tls.expecter.timeout = original_timeout if shell_setup: # disable line editing for proper recording of command line # when running bash; otherwise the scrolling readline does # messes up the output self.run('test ! -z "$BASH" && set +o vi +o emacs') # Trap the shell to complain loud if a command fails, and catch it # See that '' in the middle, is so the catcher later doesn't # get tripped by the command we sent to set it up self.run("trap 'echo ERROR''-IN-SHELL' ERR") self.target.on_console_rx("ERROR-IN-SHELL", result='errr', timeout=False) # Now commands should timeout fast self.target.testcase.tls.expecter.timeout = 30
def evemu_create_device(self, did, descriptor): """ Create a new input device :param str did: device id, or name of the device (eg: *mouse 1*, *keyboard main*, *left joytick*...) :param str descriptor: input device descriptor, as described by Linux's *evemu-describe* (without the comments); see :const:`descriptor_mouse`, :const:`descriptor_kbd`. """ # we'll evolve this into being able to create multiple mice of # different implementations (eg: evemu based vs hardware # based)...because we might want to type working with multiple mice assert isinstance(did, basestring), \ "did is type %s; expected string" % type(did) descriptor = descriptor.strip() if not isinstance(did, basestring) \ or not descriptor_valid_regex.search(descriptor.strip()): raise tc.error_e( "did %s: descriptor (%s) is not a string or it does not" " match the expected format in" " tcfl.target_ext_input.descriptor_valid_regex" % (did, type(descriptor)), dict(descriptor=descriptor)) target = self.target # Copy the device descriptor to the device, using SSH if # possible (faster) if hasattr(target, 'ssh') and target.ssh.tunnel_up(): with open(os.path.join(target.testcase.tmpdir, "input-dev.desc"), "w") as f: f.write(descriptor) f.flush() target.ssh.copy_to(f.name, "/tmp/input-dev.desc") else: target.shell.string_copy_to_file(descriptor, "/tmp/input-dev.desc") tries = 4 for cnt in range(tries): # Note the /tmp/evemu-%s.log file is also checked by # _device_get() below. output = target.shell.run( "nohup %s /tmp/input-dev.desc >& /tmp/evemu-%s.log" " & sleep 1s;" " cat /tmp/evemu-%s.log" % (self.evemu_device, did, did), output=True, trim=True) # this prints ## NAME: /dev/input/eventX, so get it eventX = self._evemu_device_log_extract(output) if not eventX: self.target.report_error( "%s: can't locate /dev/input/event* name in output to" " create new input device; retrying %d/%d" % (did, cnt, tries), dict(output=output)) continue # take only the basename; /dev/input will be added by the FIFO # itself, it is less chars to print self.devices[did] = eventX return raise tc.error_e("%s: failed %d times to create input device" % (did, tries))
def up(self, tempt=None, user=None, login_regex=re.compile('login:'******'[Pp]assword:'), shell_setup=True, timeout=None, console=None): """Wait for the shell in a console to be ready Giving it ample time to boot, wait for a :data:`shell prompt <shell_prompt_regex>` and set up the shell so that if an error happens, it will print an error message and raise a block exception. Optionally login as a user and password. >>> target.shell.up(user = '******', password = '******') :param str tempt: (optional) string to send before waiting for the loging prompt (for example, to send a newline that activates the login) :param str user: (optional) if provided, it will wait for *login_regex* before trying to login with this user name. :param str password: (optional) if provided, and a password prompt is found, send this password. :param str login_regex: (optional) if provided (string or compiled regex) and *user* is provided, it will wait for this prompt before sending the username. :param str password_regex: (optional) if provided (string or compiled regex) and *password* is provided, it will wait for this prompt before sending the password. :param int delay_login: (optional) wait this many seconds before sending the user name after finding the login prompt. :param shell_setup: (optional, default) setup the shell up by disabling command line editing (makes it easier for the automation) and set up hooks that will raise an exception if a shell command fails. By default calls target.shell.setup(); if *False*, nothing will be called. Arguments are passed: - *console = CONSOLENAME*: console where to operate; can be *None* for the default console. :param int timeout: [optional] seconds to wait for the login prompt to appear; defaults to 60s plus whatever the target specifies in metadata *bios_boot_time*. :param str console: [optional] name of the console where to operate; if *None* it will update the current default console to whatever the server considers it shall be (the console called *default*). If a previous run set the default console to something else, setting it to *None* will update it to what the server considers shall be the default console (default console at boot). """ assert tempt == None or isinstance(tempt, basestring) assert user == None or isinstance(user, basestring) assert isinstance(login_regex, (basestring, re._pattern_type)) assert delay_login >= 0 assert password == None or isinstance(password, basestring) assert isinstance(password_regex, (basestring, re._pattern_type)) assert isinstance(shell_setup, bool) or callable(shell_setup) assert timeout == None or timeout > 0 assert console == None or isinstance(console, basestring) target = self.target testcase = target.testcase if timeout == None: timeout = 60 + int(target.kws.get("bios_boot_time", 0)) def _login(target): # If we have login info, login to get a shell prompt target.expect(login_regex, name="login prompt", console=console) if delay_login: target.report_info("Delaying %ss before login in" % delay_login) time.sleep(delay_login) target.send(user) if password: target.expect(password_regex, name="password prompt", console=console) target.send(password, console=console) try: if console == None: # reset the default console target.console.default = None original_timeout = testcase.tls.expect_timeout testcase.tls.expect_timeout = timeout if tempt: tries = 3 while tries > 0: try: target.send(tempt, console=console) if user: _login(self.target) target.expect(self.shell_prompt_regex, console=console, timeout=3, name="early shell prompt") break except tc.error_e as _e: if tries == 0: raise tc.error_e( "Waited too long (%ds) for shell to come up " "(did not receive '%s')" % (self.target.testcase.tls.expect_timeout, self.shell_prompt_regex.pattern)) continue finally: tries -= 1 else: if user: _login(self.target) target.expect(self.shell_prompt_regex, console=console, name="early shell prompt") finally: testcase.tls.expect_timeout = original_timeout # same as target.console.select_preferred() if shell_setup == True: # passed as a parameter target.shell.setup(console) elif callable(shell_setup): shell_setup(console)
def console_rx_eval(expecter, target, regex, console=None, _timeout=None, result=None, uid=None): """ Check what came on a console and act on it :param str uid: (optional) identifier to use to store offset data """ if hasattr(regex, "pattern"): what = regex.pattern else: what = regex regex = re.compile(re.escape(regex)) if not uid: uid = console_mk_uid(target, what, console, _timeout, result) console_id_name, console_code = console_mk_code(target, console) # These were set by the poller of = expecter.buffers.get(console_code, None) if of == None: # FIXME: debug lof output here? expecter->tc backlink? return None ofd = of.fileno() ts = time.time() # Get the offset we saved before as the last part where we looked # at. If none, then get the last offset the poller has # recorded. Otherwise, just default to look from the start # Note the idea is each eval function has a different offset where # it is looking at. Likewise the poller for each console. offset_poller_code = "offset_" + console_code offset = expecter.buffers_persistent.get( uid, expecter.buffers_persistent.get(offset_poller_code, 0)) if _timeout != False: timeout = expecter.timeout if _timeout == None else _timeout # We can do timeout checks that provide better information # than a generic 'timeout' if ts - expecter.ts0 > timeout: of.seek(offset) # so we report console from where searched raise tc.error_e( "expected console output '%s' from console '%s:%s' " \ "NOT FOUND after %.1f s" \ % (what, target.id, console_id_name, ts - expecter.ts0), { 'target': target, "console output": of }) # mmap the whole file (which doesn't alter the file pointer) # # We have to mmap as the file might be getting huge and thus, # reading line by line might be dumb. # # However, we only search starting at @offset, which is set later # to the last success searching we had. So we shan't really map # the whole file, shall map on demand. stat_info = os.fstat(ofd) if stat_info.st_size == 0: # Nothing to read return None with contextlib.closing( mmap.mmap(of.fileno(), 0, mmap.MAP_PRIVATE, mmap.PROT_READ, 0)) as mapping: target.report_info("looking for `%s` in console %s:%s @%d-%d at " "%.2fs [%s]" % (what, target.fullid, console_id_name, offset, stat_info.st_size, ts - expecter.ts0, of.name), dlevel=3) m = regex.search(mapping[offset:]) if m: new_offset = offset + m.end() expecter.buffers_persistent[uid] = new_offset if result == None or result == "pass": # raising pass gets stopped at expecter.run(), so we # print instead, so we can see the console offset_tip = of.tell() # we report console from where searched of.seek(offset) # so we report console from where searched target.report_pass( "found expected `%s` in console `%s:%s` at %.2fs @%d" % (what, target.fullid, console_id_name, ts - expecter.ts0, offset + m.start()), {"console output": of}, dlevel=1, alevel=2) of.seek(offset_tip) raise tc.pass_e( "found expected `%s` in console `%s:%s` at %.2fs" % (what, target.fullid, console_id_name, ts - expecter.ts0), {'target': target}) elif result == "fail": of.seek(offset) # so we report console from where searched raise tc.failed_e( "found expected (for failure) `%s` in console " "`%s:%s` at %.2fs" % (what, target.fullid, console_id_name, ts - expecter.ts0), { 'target': target, "console output": of }) elif result == "error" or result == "errr": of.seek(offset) # so we report console from where searched raise tc.error_e( "found expected (for error) `%s` in console " "`%s:%s` at %.2fs" % (what, target.fullid, console_id_name, ts - expecter.ts0), { 'target': target, "console output": of }) elif result == "skip": of.seek(offset) # so we report console from where searched raise tc.skip_e( "found expected (for skip) `%s` in console " "'%s:%s' at %.2fs" % (what, target.fullid, console_id_name, ts - expecter.ts0), { 'target': target, "console output": of }) else: of.seek(offset) # so we report console from where searched raise tc.blocked_e("BUG: invalid result requested (%s)" % result) return None
def configure(testcase, target, app_src): target.kws_required_verify(['zephyr_board', 'zephyr_kernelname']) # Adds Zephyr's build zephyr_* vars to the target's keywords testcase_file = inspect.getfile(type(testcase)) zephyr_extra_args = testcase._targets[target.want_name]['kws'].get( 'app_zephyr_options', "") + " " if len(app_src) > 1: zephyr_extra_args += " ".join(app_src[1:]) # Add tags to the target srcdir = tcfl.app.get_real_srcdir(testcase_file, app_src[0]) # Decide if this is cmake style (Zephyr almost 1.10) or not # and store it in the keywords, as we'll use in different # places to make decissions on if we can run or not. ZEPHYR_BASE = os.environ.get('ZEPHYR_BASE', "__ZEPHYR_BASE_not_defined__") if os.path.exists(os.path.join(ZEPHYR_BASE, "CMakeLists.txt")): if not os.path.exists(os.path.join(srcdir, "CMakeLists.txt")): raise tc.error_e( "%s: Zephyr App is not cmake based, but Zephyr @%s is" % (srcdir, ZEPHYR_BASE)) is_cmake = True # cmake needs the extra args in -DXXX format .. sighs this # will blow up at some point, because we are assuming that # all extra args passed are VAR=VALUE zephyr_extra_args = " ".join("-D" + arg for arg in zephyr_extra_args.split()) else: if os.path.exists(os.path.join(srcdir, "CMakeLists.txt")): raise tc.error_e( "%s: Zephyr App is cmake based, but Zephyr @%s is not" % (srcdir, ZEPHYR_BASE)) is_cmake = False target.kws_set( { 'zephyr_srcdir': srcdir, # Place all the build output in the temp directory, to # avoid polluting the source directories 'zephyr_objdir': os.path.join( testcase.tmpdir, "outdir-%(tc_hash)s-%(tg_hash)s-%(zephyr_board)s" % target.kws), 'zephyr_is_cmake': is_cmake, }, bsp=target.bsp) if not target.bsp in target.bsps_stub: # Set arguments only for this BSP, not for all of them, # and only if we are not building a stub. Note we get the # options from app_zephyr_options + OPT1 + OPT2... added # after app_src in 'app_zephyr = (SRC, OPT1, OPT2...) target.kw_set('zephyr_extra_args', zephyr_extra_args, bsp=target.bsp) else: # When we are building stubs, we do not take the options # from the project itself -- stub needs no options target.kw_set('zephyr_extra_args', "", bsp=target.bsp) # Do we introduce boot configuration or not? if not testcase.build_only: # If a testcase is build only, it won't run in HW so we # don't need to introduce a boot delay to allow serial # ports to be ready. # This is needed because build only testcases might be # disabling options that are needed to implement boot # delays, like clock stuff global boot_delay _boot_delay = boot_delay.get(target.type, 1000) if not target.bsp in target.bsps_stub: target.zephyr.config_file_write("500_boot_config", """\ # Introduce a boot delay of 1s to make sure the system # has had time to setup the serial ports and start recording # from them CONFIG_BOOT_BANNER=y CONFIG_BOOT_DELAY=%d """ % _boot_delay, bsp=target.bsp) else: target.zephyr.config_file_write("500_boot_config", """\ # Stubs be quiet and don't delay, ready ASAP CONFIG_BOOT_BANNER=n CONFIG_BOOT_DELAY=0 """, bsp=target.bsp) # Add stubs for other BSPs in this board if there are none # We will run this each time we go for a BSP build, but it is fine for bsp_stub in target.bsps_stub.keys(): target.stub_app_add( bsp_stub, app_zephyr, os.path.join(ZEPHYR_BASE, 'tests', 'booting', 'stub'))
def up(self, tempt = None, user = None, login_regex = re.compile('login:'******'[Pp]assword:'), shell_setup = True, timeout = 120): """Wait for the shell in a console to be ready Giving it ample time to boot, wait for a :data:`shell prompt <linux_shell_prompt_regex>` and set up the shell so that if an error happens, it will print an error message and raise a block exception. Optionally login as a user and password. >>> target.shell.up(user = '******', password = '******') :param str tempt: (optional) string to send before waiting for the loging prompt (for example, to send a newline that activates the login) :param str user: (optional) if provided, it will wait for *login_regex* before trying to login with this user name. :param str password: (optional) if provided, and a password prompt is found, send this password. :param str login_regex: (optional) if provided (string or compiled regex) and *user* is provided, it will wait for this prompt before sending the username. :param str password_regex: (optional) if provided (string or compiled regex) and *password* is provided, it will wait for this prompt before sending the password. :param int delay_login: (optional) wait this many seconds before sending the user name after finding the login prompt. :param bool shell_setup: (optional, default) setup the shell up by disabling command line editing (makes it easier for the automation) and set up hooks that will raise an exception if a shell command fails. :param int timeout: [optional] seconds to wait for the login prompt to appear """ assert tempt == None or isinstance(tempt, basestring) assert user == None or isinstance(user, basestring) assert isinstance(login_regex, ( basestring, re._pattern_type )) assert delay_login >= 0 assert password == None or isinstance(password, basestring) assert isinstance(password_regex, ( basestring, re._pattern_type )) assert isinstance(shell_setup, bool) assert timeout > 0 def _login(target): # If we have login info, login to get a shell prompt target.expect(login_regex) if delay_login: target.report_info("Delaying %ss before login in" % delay_login) time.sleep(delay_login) target.send(user) if password: target.expect(password_regex) target.send(password) try: original_timeout = self.target.testcase.tls.expecter.timeout self.target.testcase.tls.expecter.timeout = timeout if tempt: tries = 0 while tries < self.target.testcase.tls.expecter.timeout: try: self.target.send(tempt) if user: _login(self.target) self.target.expect(self.linux_shell_prompt_regex, timeout = 1) break except tc.error_e as _e: if tries == self.target.testcase.tls.expecter.timeout: raise tc.error_e( "Waited too long (%ds) for shell to come up " "(did not receive '%s')" % (self.target.testcase.tls.expecter.timeout, self.linux_shell_prompt_regex.pattern)) continue else: if user: _login(self.target) self.target.expect(self.linux_shell_prompt_regex) finally: self.target.testcase.tls.expecter.timeout = original_timeout if shell_setup: # disable line editing for proper recording of command line # when running bash; otherwise the scrolling readline does # messes up the output self.run('test ! -z "$BASH" && set +o vi +o emacs') # Trap the shell to complain loud if a command fails, and catch it # See that '' in the middle, is so the catcher later doesn't # get tripped by the command we sent to set it up self.run("trap 'echo ERROR''-IN-SHELL' ERR") self.target.on_console_rx("ERROR-IN-SHELL", result = 'errr', timeout = False) # Now commands should timeout fast self.target.testcase.tls.expecter.timeout = 30
def mount_fs(target, image, boot_dev): """ Boots a root filesystem on /mnt The partition used as a root filesystem is picked up based on the image that is going to be installed; we look for one that has the most similar image already installed and pick that. :returns: name of the root partition device """ pos_reinitialize = target.property_get("pos_reinitialize", False) if pos_reinitialize: # Need to reinit the partition table (we were told to by # setting pos_repartition to anything target.report_info("POS: repartitioning per pos_reinitialize " "property") for tag in target.rt.keys(): # remove pos_root_*, as they don't apply anymore if tag.startswith("pos_root_"): target.property_set(tag, None) _disk_partition(target) target.property_set('pos_reinitialize', None) root_part_dev = _rootfs_guess(target, image, boot_dev) # save for other functions called later target.root_part_dev = root_part_dev root_part_dev_base = os.path.basename(root_part_dev) image_prev = target.property_get("pos_root_" + root_part_dev_base, "nothing") target.report_info("POS: will use %s for root partition (had %s before)" % (root_part_dev, image_prev)) # fsinfo looks like described in target.pos._fsinfo_load() dev_info = None for blockdevice in target.pos.fsinfo.get('blockdevices', []): for child in blockdevice.get('children', []): if child['name'] == root_part_dev_base: dev_info = child if dev_info == None: # it cannot be we might have to repartition because at this # point *we* have partitoned. raise tc.error_e( "Can't find information for root device %s in FSinfo array" % root_part_dev_base, dict(fsinfo = target.pos.fsinfo)) # what format does it currently have? current_fstype = dev_info.get('fstype', 'ext4') # What format does it have to have? # # Ok, here we need to note that we can't have multiple root # filesystems with the same UUID or LABEL, so the image can't rely # on UUIDs # img_fss = target.pos.metadata.get('filesystems', {}) if '/' in img_fss: # a common origin is ok because the YAML schema forces both # fstype and mkfs_opts to be specified origin = "image's /.tcf.metadata.yaml" fsdata = img_fss.get('/', {}) else: origin = "defaults @" + commonl.origin_get(0) fsdata = {} fstype = fsdata.get('fstype', 'ext4') mkfs_opts = fsdata.get('mkfs_opts', '-Fj') # do they match? if fstype != current_fstype: target.report_info( "POS: reformatting %s because current format is '%s' and " "'%s' is needed (per %s)" % (root_part_dev, current_fstype, fstype, origin)) _mkfs(target, root_part_dev, fstype, mkfs_opts) else: target.report_info( "POS: no need to reformat %s because current format is '%s' and " "'%s' is needed (per %s)" % (root_part_dev, current_fstype, fstype, origin), dlevel = 1) for try_count in range(3): target.report_info("POS: mounting root partition %s onto /mnt " "to image [%d/3]" % (root_part_dev, try_count)) # don't let it fail or it will raise an exception, so we # print FAILED in that case to look for stuff; note the # double apostrophe trick so the regex finder doens't trip # on the command output = target.shell.run( "mount %s /mnt || echo FAI''LED" % root_part_dev, output = True) # What did we get? if 'FAILED' in output: if 'special device ' + root_part_dev \ + ' does not exist.' in output: _disk_partition(target) else: # ok, this probably means probably the partitions are not # formatted; so let's just reformat and retry _mkfs(target, root_part_dev, fstype, mkfs_opts) else: target.report_info("POS: mounted %s onto /mnt to image" % root_part_dev) return root_part_dev # it worked, we are done # fall through, retry else: raise tc.blocked_e( "POS: Tried to mount too many times and failed", dict(target = target))
def boot_config_multiroot(target, boot_dev, image): """ Configure the target to boot using the multiroot """ boot_dev = target.kws['pos_boot_dev'] # were we have mounted the root partition root_dir = "/mnt" linux_kernel_file, linux_initrd_file, linux_options = \ _linux_boot_guess(target, image) if linux_kernel_file == None: raise tc.blocked_e("Cannot guess a Linux kernel to boot", dict(target=target)) # remove absolutization (some specs have it), as we need to copy from # mounted filesystems if os.path.isabs(linux_kernel_file): linux_kernel_file = linux_kernel_file[1:] if linux_initrd_file and os.path.isabs(linux_initrd_file): linux_initrd_file = linux_initrd_file[1:] if linux_options == None or linux_options == "": target.report_info("WARNING! can't figure out Linux cmdline " "options, taking defaults") # below we'll add more stuff linux_options = "console=tty0 root=SOMEWHERE" # MULTIROOT: indicate which image has been flashed to this # partition # saved by pos_multiroot.mountfs root_part_dev = target.root_part_dev root_part_dev_base = os.path.basename(root_part_dev) target.property_set('pos_root_' + root_part_dev_base, image) # /boot EFI system partition is always /dev/DEVNAME1 (first # partition), we partition like that # FIXME: we shouldn't harcode this boot_part_dev = boot_dev + target.kws['p_prefix'] + "1" kws = dict( boot_dev=boot_dev, boot_part_dev=boot_part_dev, root_part_dev=root_part_dev, root_part_dev_base=root_part_dev_base, root_dir=root_dir, linux_kernel_file=linux_kernel_file, linux_kernel_file_basename=os.path.basename(linux_kernel_file), linux_initrd_file=linux_initrd_file, linux_options=linux_options, ) if linux_initrd_file: kws['linux_initrd_file_basename'] = os.path.basename(linux_initrd_file) else: kws['linux_initrd_file_basename'] = None kws.update(target.kws) if linux_options: # # Maybe mess with the Linux boot options # target.report_info("linux cmdline options: %s" % linux_options) # FIXME: can this come from config? linux_options_replace = { # we want to use hard device name rather than LABELS/UUIDs, as # we have reformated and those will have changed "root": "/dev/%(root_part_dev_base)s" % kws, # we have created this in pos_multiroot and will only # replace it if the command line option is present. "resume": "/dev/disk/by-label/tcf-swap", } # FIXME: can this come from config? # We harcode a serial console on the device where we know the # framework is listening linux_options_append = \ "console=%(linux_serial_console_default)s,115200n8" % kws linux_options_append += " " + target.rt.get('linux_options_append', "") linux_options += " " + linux_options_append for option, value in linux_options_replace.iteritems(): regex = re.compile(r"\b" + option + r"=\S+") if regex.search(linux_options): linux_options = re.sub( regex, option + "=" + linux_options_replace[option], linux_options) else: linux_options += " " + option + "=" + value kws['linux_options'] = linux_options target.report_info("linux cmdline options (modified): %s" % linux_options) # Now generate the UEFI system partition that will boot the # system; we always override it, so we don't need to decide if it # is corrupted or whatever; we'll mount it in /boot (which now is # the POS /boot) # # Mount the /boot fs # # Try to assume it is ok, try to repair it if not; rsync the # kernels in there, it is faster for repeated operation/ # target.report_info("POS/EFI: checking %(boot_part_dev)s" % kws) output = target.shell.run("fsck.fat -aw /dev/%(boot_part_dev)s || true" % kws, output=True, trim=True) # FIXME: parse the output to tell if there was a problem; when bad # but recovered, we'll see # # 0x41: Dirty bit is set. Fs was not properly unmounted and some data may be corrupt. # Automatically removing dirty bit. # Performing changes. # /dev/sda1: 11 files, 4173/261372 clusters # When ok # ## $ sudo fsck.vfat -wa /dev/nvme0n1p1 ## fsck.fat 4.1 (2017-01-24) ## /dev/sda1: 39 files, 2271/33259 clusters # When really hosed it won't print the device line, so we look for # that # ## $ fsck.vfat -wa /dev/trash ## fsck.fat 4.1 (2017-01-24) ## Logical sector size (49294 bytes) is not a multiple of the physical sector size. good_regex = re.compile( "^/dev/%(boot_part_dev)s: [0-9]+ files, " "[0-9]+/[0-9]+ clusters$" % kws, re.MULTILINE) if not good_regex.search(output): target.report_info( "POS/EFI: /dev/%(boot_part_dev)s: formatting EFI " "filesystem, fsck couldn't fix it" % kws, dict(output=output)) target.shell.run("mkfs.fat -F32 /dev/%(boot_part_dev)s; sync" % kws) target.report_info("POS/EFI: /dev/%(boot_part_dev)s: mounting in /boot" % kws) target.shell.run(" mount /dev/%(boot_part_dev)s /boot" " && mkdir -p /boot/loader/entries " % kws) # Do we have enough space? if not, remove the oldest stuff that is # not the file we are looking for # This prints ## $ df --output=pcent /boot ## Use% ## 6% output = target.shell.run("df --output=pcent /boot", output=True) regex = re.compile(r"^\s*(?P<percent>[\.0-9]+)%$", re.MULTILINE) match = regex.search(output) if not match: raise tc.error_e("Can't determine the amount of free space in /boot", dict(output=output)) used_space = float(match.groupdict()['percent']) if used_space > 75: target.report_info( "POS/EFI: /dev/%(boot_part_dev)s: freeing up space" % kws) # List files in /boot, sort by last update (when we rsynced # them) ## 2018-10-29+08:48:48.0000000000 84590 /boot/EFI/BOOT/BOOTX64.EFI ## 2018-10-29+08:48:48.0000000000 84590 /boot/EFI/systemd/systemd-bootx64.efi ## 2019-05-14+13:25:06.0000000000 7192832 /boot/vmlinuz-4.12.14-110-default ## 2019-05-14+13:25:08.0000000000 9688340 /boot/initrd-4.12.14-110-default ## 2019-05-14+13:25:14.0000000000 224 /boot/loader/entries/tcf-boot.conf ## 2019-05-14+13:25:14.0000000000 54 /boot/loader/loader.conf output = target.shell.run( # that double \\ needed so the shell is the one using it # as a \t, not python converting to a sequence "find /boot/ -type f -printf '%T+\\t%s\\t%p\\n' | sort", output=True, trim=True) # delete the first half entries over 300k except those that # match the kernels we are installing to_remove = [] _linux_initrd_file = kws.get("linux_initrd_file", "%%NONE%%") if linux_initrd_file == None: _linux_initrd_file = "%%NONE%%" for line in output.splitlines(): _timestamp, size_s, path = line.split(None, 3) size = int(size_s) if size > 300 * 1024 \ and not kws['linux_kernel_file_basename'] in path \ and not _linux_initrd_file in path: to_remove.append(path) # get older half and wipe it--this means the first half, as we # sort from older to newer to_remove = to_remove[:len(to_remove) // 2] for path in to_remove: # we could do them in a single go, but we can exceed the # command line length -- lazy to optimize it target.shell.run("rm -f %s" % path) # Now copy all the files needed to boot to the root of the EFI # system partition mounted in /boot; remember they are in /mnt/, # our root partition target.shell.run( "time -p rsync --force --inplace /mnt/%(linux_kernel_file)s" " /boot/%(linux_kernel_file_basename)s" % kws) if kws.get("linux_initrd_file", None): target.shell.run( "time -p rsync --force --inplace /mnt/%(linux_initrd_file)s" " /boot/%(linux_initrd_file_basename)s" % kws) # we are the only one who cuts the cod here (yeah, direct Spanish # translation for the reader's entertainment), and if not wipe'm to # eternity; otherwise systemd will boot something prolly in # alphabetical order, not what we want target.shell.run("/usr/bin/rm -rf /boot/loader/entries/*.conf") # remember paths to the bootloader are relative to /boot # merge these two tcf_boot_conf = """\ cat <<EOF > /boot/loader/entries/tcf-boot.conf title TCF-driven local boot linux /%(linux_kernel_file_basename)s options %(linux_options)s """ if kws.get("linux_initrd_file", None): tcf_boot_conf += "initrd /%(linux_initrd_file_basename)s\n" tcf_boot_conf += "EOF\n" target.shell.run(tcf_boot_conf % kws) # Install new -- we wiped the /boot fs new anyway; if there are # multiple options already, bootctl shall be able to handle it. # Don't do variables in the any casewe will poke with them later # on anyway. Why? Because there is space for a race condition that # will leave us with the system booting off the localdisk vs the # network for PXE--see efibootmgr_setup() target.shell.run("bootctl update --no-variables" " || bootctl install --no-variables;" " sync") # Now mess with the EFIbootmgr # FIXME: make this a function and a configuration option (if the # target does efibootmgr) _efibootmgr_setup(target, boot_dev, 1) # umount only if things go well # Shall we try to unmount in case of error? nope, we are going to # have to redo the whole thing anyway, so do not touch it, in case # we are jumping in for manual debugging target.shell.run("umount /dev/%(boot_part_dev)s" % kws)
def mount_fs(target, image, boot_dev): """ Boots a root filesystem on /mnt The partition used as a root filesystem is picked up based on the image that is going to be installed; we look for one that has the most similar image already installed and pick that. :returns: name of the root partition device """ # does the disk have a partition scheme we recognize? pos_partsizes = target.rt['pos_partsizes'] # the name we'll give to the boot partition; see # _disk_partition(); if we can't find it, we assume the things # needs to be repartitioned. Note it includes the sizes, so if we # change the sizes in the config it reformats automatically. # # TCF-multiroot-NN:NN:NN:NN target._boot_label_name = "TCF-multiroot-" + pos_partsizes pos_reinitialize_force = True boot_dev_base = os.path.basename(boot_dev) child = target.pos.fsinfo_get_child_by_partlabel(boot_dev_base, target._boot_label_name) if child: pos_reinitialize_force = False else: target.report_info("POS: repartitioning due to different" " partition schema") pos_reinitialize = target.property_get("pos_reinitialize", False) if pos_reinitialize: target.report_info("POS: repartitioning per pos_reinitialize " "property") if pos_reinitialize or pos_reinitialize_force: # Need to reinit the partition table (we were told to by # setting pos_repartition to anything or we didn't recognize # the existing partitioning scheme) for tag in target.rt.keys(): # remove pos_root_*, as they don't apply anymore if tag.startswith("pos_root_"): target.property_set(tag, None) _disk_partition(target) target.pos.fsinfo_read(target._boot_label_name) target.property_set('pos_reinitialize', None) root_part_dev = _rootfs_guess(target, image, boot_dev) # save for other functions called later target.root_part_dev = root_part_dev root_part_dev_base = os.path.basename(root_part_dev) image_prev = target.property_get("pos_root_" + root_part_dev_base, "nothing") target.report_info("POS: will use %s for root partition (had %s before)" % (root_part_dev, image_prev)) # fsinfo looks like described in target.pos.fsinfo_read() dev_info = None for blockdevice in target.pos.fsinfo.get('blockdevices', []): for child in blockdevice.get('children', []): if child['name'] == root_part_dev_base: dev_info = child if dev_info == None: # it cannot be we might have to repartition because at this # point *we* have partitioned. raise tc.error_e( "Can't find information for root device %s in FSinfo array" % root_part_dev_base, dict(fsinfo=target.pos.fsinfo)) # what format does it currently have? current_fstype = dev_info.get('fstype', 'ext4') # What format does it have to have? # # Ok, here we need to note that we can't have multiple root # filesystems with the same UUID or LABEL, so the image can't rely # on UUIDs # img_fss = target.pos.metadata.get('filesystems', {}) if '/' in img_fss: # a common origin is ok because the YAML schema forces both # fstype and mkfs_opts to be specified origin = "image's /.tcf.metadata.yaml" fsdata = img_fss.get('/', {}) else: origin = "defaults @" + commonl.origin_get(0) fsdata = {} fstype = fsdata.get('fstype', 'ext4') mkfs_opts = fsdata.get('mkfs_opts', '-Fj') # do they match? if fstype != current_fstype: target.report_info( "POS: reformatting %s because current format is '%s' and " "'%s' is needed (per %s)" % (root_part_dev, current_fstype, fstype, origin)) _mkfs(target, root_part_dev, fstype, mkfs_opts) else: target.report_info( "POS: no need to reformat %s because current format is '%s' and " "'%s' is needed (per %s)" % (root_part_dev, current_fstype, fstype, origin), dlevel=1) for try_count in range(3): target.report_info("POS: mounting root partition %s onto /mnt " "to image [%d/3]" % (root_part_dev, try_count)) # don't let it fail or it will raise an exception, so we # print FAILED in that case to look for stuff; note the # double apostrophe trick so the regex finder doens't trip # on the command output = target.shell.run("mount %s /mnt || echo FAI''LED" % root_part_dev, output=True) # What did we get? if 'FAILED' in output: if 'special device ' + root_part_dev \ + ' does not exist.' in output: _disk_partition(target) target.pos.fsinfo_read(target._boot_label_name) else: # ok, this probably means probably the partitions are not # formatted; so let's just reformat and retry _mkfs(target, root_part_dev, fstype, mkfs_opts) else: target.report_info("POS: mounted %s onto /mnt to image" % root_part_dev) return root_part_dev # it worked, we are done # fall through, retry else: raise tc.blocked_e("POS: Tried to mount too many times and failed", dict(target=target))
def up(self, tempt=None, user=None, login_regex=re.compile('login:'******'[Pp]assword:'), shell_setup=True, timeout=None, console=None, wait_for_early_shell_prompt=True): """Wait for the shell in a console to be ready Giving it ample time to boot, wait for a :data:`shell prompt <prompt_regex>` and set up the shell so that if an error happens, it will print an error message and raise a block exception. Optionally login as a user and password. Note this resets any shell prompt set by the script to what is assumed to be the original one after a power up. >>> target.shell.up(user = '******', password = '******') :param str tempt: (optional) string to send before waiting for the loging prompt (for example, to send a newline that activates the login) :param str user: (optional) if provided, it will wait for *login_regex* before trying to login with this user name. :param str password: (optional) if provided, and a password prompt is found, send this password. :param str login_regex: (optional) if provided (string or compiled regex) and *user* is provided, it will wait for this prompt before sending the username. :param str password_regex: (optional) if provided (string or compiled regex) and *password* is provided, it will wait for this prompt before sending the password. :param int delay_login: (optional) wait this many seconds before sending the user name after finding the login prompt. :param shell_setup: (optional, default) setup the shell up by disabling command line editing (makes it easier for the automation) and set up hooks that will raise an exception if a shell command fails. By default calls target.shell.setup(); if *False*, nothing will be called. Arguments are passed: - *console = CONSOLENAME*: console where to operate; can be *None* for the default console. :param int timeout: [optional] seconds to wait for the login prompt to appear; defaults to 60s plus whatever the target specifies in metadata *bios_boot_time*. :param str console: [optional] name of the console where to operate; if *None* it will update the current default console to whatever the server considers it shall be (the console called *default*). If a previous run set the default console to something else, setting it to *None* will update it to what the server considers shall be the default console (default console at boot). """ assert tempt == None or isinstance(tempt, basestring) assert user == None or isinstance(user, basestring) assert login_regex == None or isinstance( login_regex, (basestring, re._pattern_type)) assert delay_login >= 0 assert password == None or isinstance(password, basestring) assert isinstance(password_regex, (basestring, re._pattern_type)) assert isinstance(shell_setup, bool) or callable(shell_setup) assert timeout == None or timeout > 0 assert console == None or isinstance(console, basestring) target = self.target testcase = target.testcase if timeout == None: timeout = 60 + int(target.kws.get("bios_boot_time", 0)) # Set the original shell prompt self.prompt_regex = self.prompt_regex_default def _login(target): # If we have login info, login to get a shell prompt if login_regex != None: # we have already recevied this, so do not expect it target.expect(login_regex, name="login prompt", console=console) if delay_login: target.report_info("Delaying %ss before login in" % delay_login) time.sleep(delay_login) target.send(user) if password: target.expect(password_regex, name="password prompt", console=console) target.send(password, console=console) original_timeout = testcase.tls.expect_timeout try: if console == None: # reset the default console target.console.default = None # this will yield the default console name, not None # as we set it above; bad API console = target.console.default testcase.tls.expect_timeout = timeout ts0 = time.time() ts = ts0 inner_timeout = 3 * timeout / 20 while ts - ts0 < 3 * timeout: # if this was an SSH console that was left # enabled, it will die and auto-disable when # the machine power cycles, so make sure it is enabled action = "n/a" try: if console.startswith("ssh"): target.console.disable() action = "enable console %s" % console target.console.setup(console, user=user, password=password) target.console.enable(console=console) ts = time.time() target.report_info("shell-up: %s: success at +%.1fs" % (action, ts - ts0), dlevel=2) if tempt: action = "tempt console %s" % console target.send(tempt, console=console) ts = time.time() target.report_info("shell-up: %s: success at +%.1fs" % (action, ts - ts0), dlevel=2) if not console.startswith("ssh"): if user: action = "login in via console %s" % console # _login uses this 'console' definition _login(self.target) ts = time.time() target.report_info( "shell-up: %s: success at +%.1fs" % (action, ts - ts0), dlevel=2) if wait_for_early_shell_prompt: action = "wait for shell prompt" target.expect(self.prompt_regex, console=console, name="early shell prompt", timeout=inner_timeout) break except (tc.error_e, tc.failed_e) as e: ts = time.time() target.report_info( "shell-up: action '%s' failed at +%.1fs; retrying: %s" % (action, ts - ts0, e), dict(target=target, exception=e), dlevel=2) time.sleep(inner_timeout) ts = time.time() if console.startswith("ssh"): target.console.disable(console=console) continue else: raise tc.error_e( "Waited too long (%ds) for shell to come up on" " console '%s' (did not receive '%s')" % (3 * timeout, console, self.prompt_regex.pattern)) finally: testcase.tls.expect_timeout = original_timeout # same as target.console.select_preferred() if shell_setup == True: # passed as a parameter target.shell.setup(console) elif callable(shell_setup): shell_setup(console) # False, so we don't call shell setup # don't set a timeout here, leave it to whatever it was defaulted return