Brad Bishop | bec4ebc | 2022-08-03 09:55:16 -0400 | [diff] [blame] | 1 | import pathlib |
| 2 | import pexpect |
| 3 | import os |
| 4 | |
| 5 | from oeqa.core.target.ssh import OESSHTarget |
| 6 | from fvp import conffile, runner |
| 7 | |
| 8 | |
| 9 | class OEFVPSSHTarget(OESSHTarget): |
| 10 | """ |
| 11 | Base class for meta-arm FVP targets. |
| 12 | Contains common logic to start and stop an FVP. |
| 13 | """ |
| 14 | def __init__(self, logger, target_ip, server_ip, timeout=300, user='root', |
Patrick Williams | 7784c42 | 2022-11-17 07:29:11 -0600 | [diff] [blame^] | 15 | port=None, dir_image=None, rootfs=None, bootlog=None, **kwargs): |
Brad Bishop | bec4ebc | 2022-08-03 09:55:16 -0400 | [diff] [blame] | 16 | super().__init__(logger, target_ip, server_ip, timeout, user, port) |
| 17 | image_dir = pathlib.Path(dir_image) |
| 18 | # rootfs may have multiple extensions so we need to strip *all* suffixes |
| 19 | basename = pathlib.Path(rootfs) |
| 20 | basename = basename.name.replace("".join(basename.suffixes), "") |
| 21 | self.fvpconf = image_dir / (basename + ".fvpconf") |
| 22 | self.config = conffile.load(self.fvpconf) |
Patrick Williams | 7784c42 | 2022-11-17 07:29:11 -0600 | [diff] [blame^] | 23 | self.bootlog = bootlog |
Brad Bishop | bec4ebc | 2022-08-03 09:55:16 -0400 | [diff] [blame] | 24 | |
| 25 | if not self.fvpconf.exists(): |
| 26 | raise FileNotFoundError(f"Cannot find {self.fvpconf}") |
| 27 | |
Patrick Williams | 7784c42 | 2022-11-17 07:29:11 -0600 | [diff] [blame^] | 28 | def _after_start(self): |
Brad Bishop | bec4ebc | 2022-08-03 09:55:16 -0400 | [diff] [blame] | 29 | pass |
| 30 | |
Brad Bishop | bec4ebc | 2022-08-03 09:55:16 -0400 | [diff] [blame] | 31 | def start(self, **kwargs): |
Patrick Williams | 7784c42 | 2022-11-17 07:29:11 -0600 | [diff] [blame^] | 32 | self.fvp_log = self._create_logfile("fvp") |
| 33 | self.fvp = runner.FVPRunner(self.logger) |
| 34 | self.fvp.start(self.config, stdout=self.fvp_log) |
| 35 | self.logger.debug(f"Started FVP PID {self.fvp.pid()}") |
| 36 | self._after_start() |
Brad Bishop | bec4ebc | 2022-08-03 09:55:16 -0400 | [diff] [blame] | 37 | |
| 38 | def stop(self, **kwargs): |
Patrick Williams | 7784c42 | 2022-11-17 07:29:11 -0600 | [diff] [blame^] | 39 | returncode = self.fvp.stop() |
| 40 | self.logger.debug(f"Stopped FVP with return code {returncode}") |
| 41 | |
| 42 | def _create_logfile(self, name): |
| 43 | if not self.bootlog: |
| 44 | return None |
| 45 | |
| 46 | test_log_path = pathlib.Path(self.bootlog).parent |
| 47 | test_log_suffix = pathlib.Path(self.bootlog).suffix |
| 48 | fvp_log_file = f"{name}_log{test_log_suffix}" |
| 49 | fvp_log_path = pathlib.Path(test_log_path, fvp_log_file) |
| 50 | fvp_log_symlink = pathlib.Path(test_log_path, f"{name}_log") |
| 51 | try: |
| 52 | os.remove(fvp_log_symlink) |
| 53 | except: |
| 54 | pass |
| 55 | os.symlink(fvp_log_file, fvp_log_symlink) |
| 56 | return open(fvp_log_path, 'wb') |
Brad Bishop | bec4ebc | 2022-08-03 09:55:16 -0400 | [diff] [blame] | 57 | |
| 58 | |
| 59 | class OEFVPTarget(OEFVPSSHTarget): |
| 60 | """ |
| 61 | For compatibility with OE-core test cases, this target's start() method |
| 62 | waits for a Linux shell before returning to ensure that SSH commands work |
| 63 | with the default test dependencies. |
| 64 | """ |
Patrick Williams | 7784c42 | 2022-11-17 07:29:11 -0600 | [diff] [blame^] | 65 | def __init__(self, logger, target_ip, server_ip, **kwargs): |
Brad Bishop | bec4ebc | 2022-08-03 09:55:16 -0400 | [diff] [blame] | 66 | super().__init__(logger, target_ip, server_ip, **kwargs) |
Patrick Williams | 7784c42 | 2022-11-17 07:29:11 -0600 | [diff] [blame^] | 67 | self.logfile = self.bootlog and open(self.bootlog, "wb") or None |
Brad Bishop | bec4ebc | 2022-08-03 09:55:16 -0400 | [diff] [blame] | 68 | |
| 69 | # FVPs boot slowly, so allow ten minutes |
| 70 | self.boot_timeout = 10 * 60 |
| 71 | |
Patrick Williams | 7784c42 | 2022-11-17 07:29:11 -0600 | [diff] [blame^] | 72 | def _after_start(self): |
| 73 | with open(self.fvp_log.name, 'rb') as logfile: |
| 74 | parser = runner.ConsolePortParser(logfile) |
| 75 | self.logger.debug(f"Awaiting console on terminal {self.config['consoles']['default']}") |
| 76 | port = parser.parse_port(self.config['consoles']['default']) |
| 77 | console = self.fvp.create_pexpect(port) |
| 78 | try: |
| 79 | console.expect("login\\:", timeout=self.boot_timeout) |
| 80 | self.logger.debug("Found login prompt") |
| 81 | except pexpect.TIMEOUT: |
| 82 | self.logger.info("Timed out waiting for login prompt.") |
| 83 | self.logger.info("Boot log follows:") |
| 84 | self.logger.info(b"\n".join(console.before.splitlines()[-200:]).decode("utf-8", errors="replace")) |
| 85 | raise RuntimeError("Failed to start FVP.") |
Brad Bishop | bec4ebc | 2022-08-03 09:55:16 -0400 | [diff] [blame] | 86 | |
| 87 | |
| 88 | class OEFVPSerialTarget(OEFVPSSHTarget): |
| 89 | """ |
| 90 | This target is intended for interaction with the target over one or more |
| 91 | telnet consoles using pexpect. |
Patrick Williams | 7784c42 | 2022-11-17 07:29:11 -0600 | [diff] [blame^] | 92 | |
Brad Bishop | bec4ebc | 2022-08-03 09:55:16 -0400 | [diff] [blame] | 93 | This still depends on OEFVPSSHTarget so SSH commands can still be run on |
| 94 | the target, but note that this class does not inherently guarantee that |
| 95 | the SSH server is running prior to running test cases. Test cases that use |
| 96 | SSH should first validate that SSH is available, e.g. by depending on the |
| 97 | "linuxboot" test case in meta-arm. |
| 98 | """ |
| 99 | DEFAULT_CONSOLE = "default" |
| 100 | |
Patrick Williams | 7784c42 | 2022-11-17 07:29:11 -0600 | [diff] [blame^] | 101 | def __init__(self, logger, target_ip, server_ip, **kwargs): |
Brad Bishop | bec4ebc | 2022-08-03 09:55:16 -0400 | [diff] [blame] | 102 | super().__init__(logger, target_ip, server_ip, **kwargs) |
| 103 | self.terminals = {} |
| 104 | |
Patrick Williams | 7784c42 | 2022-11-17 07:29:11 -0600 | [diff] [blame^] | 105 | def _after_start(self): |
| 106 | with open(self.fvp_log.name, 'rb') as logfile: |
| 107 | parser = runner.ConsolePortParser(logfile) |
| 108 | for name, console in self.config["consoles"].items(): |
| 109 | logfile = self._create_logfile(name) |
| 110 | self.logger.info(f'Creating terminal {name} on {console}') |
| 111 | port = parser.parse_port(console) |
| 112 | self.terminals[name] = \ |
| 113 | self.fvp.create_pexpect(port, logfile=logfile) |
Brad Bishop | bec4ebc | 2022-08-03 09:55:16 -0400 | [diff] [blame] | 114 | |
Patrick Williams | 7784c42 | 2022-11-17 07:29:11 -0600 | [diff] [blame^] | 115 | # testimage.bbclass expects to see a log file at `bootlog`, |
| 116 | # so make a symlink to the 'default' log file |
| 117 | if name == 'default': |
| 118 | default_test_file = f"{name}_log{self.test_log_suffix}" |
| 119 | os.symlink(default_test_file, self.bootlog) |
Brad Bishop | bec4ebc | 2022-08-03 09:55:16 -0400 | [diff] [blame] | 120 | |
| 121 | def _get_terminal(self, name): |
| 122 | return self.terminals[name] |
| 123 | |
| 124 | def __getattr__(self, name): |
| 125 | """ |
| 126 | Magic method which automatically exposes the whole pexpect API on the |
| 127 | target, with the first argument being the terminal name. |
| 128 | |
| 129 | e.g. self.target.expect(self.target.DEFAULT_CONSOLE, "login\\:") |
| 130 | """ |
| 131 | def call_pexpect(terminal, *args, **kwargs): |
| 132 | attr = getattr(self.terminals[terminal], name) |
| 133 | if callable(attr): |
| 134 | return attr(*args, **kwargs) |
| 135 | else: |
| 136 | return attr |
| 137 | |
| 138 | return call_pexpect |