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