blob: 4dcbeadca5ae4e96032ea34193541fad5cad8802 [file] [log] [blame]
Zelalem1af7a7b2020-08-04 17:34:32 -05001#!/usr/bin/env python3
2
3"""
4Script to automate the manual juno tests that used to require the Juno board
5to be manually power cycled.
6
7"""
8
9import argparse
10import datetime
11import enum
12import logging
13import os
14import pexpect
15import re
16import shutil
17import sys
18import time
19import zipfile
20from pexpect import pxssh
21
22################################################################################
23# Classes #
24################################################################################
25
26class CriticalError(Exception):
27 """
28 Raised when a serious issue occurs that will likely mean abort.
29 """
30 pass
31
32class TestStatus(enum.Enum):
33 """
34 This is an enum to describe possible return values from test handlers.
35 """
36 SUCCESS = 0
37 FAILURE = 1
38 CONTINUE = 2
39
40class JunoBoardManager(object):
41 """
42 Manage Juno board reservation and mounts with support for context
43 management.
44 Parameters
45 ssh (pxssh object): SSH connection to remote machine
46 password (string): sudo password for mounting/unmounting
47 """
48
49 def __init__(self, ssh, password):
50 self.ssh = ssh
51 self.path = ""
52 self.password = password
53 self.mounted = False
54
55 def reserve(self):
56 """
57 Try to reserve a Juno board.
58 """
59 for path in ["/home/login/generaljuno1", "/home/login/generaljuno2"]:
60 logging.info("Trying %s...", path)
61 self.ssh.before = ""
62 self.ssh.sendline("%s/reserve.sh" % path)
63 res = self.ssh.expect(["RESERVE_SCRIPT_SUCCESS", "RESERVE_SCRIPT_FAIL", pexpect.EOF, \
64 pexpect.TIMEOUT], timeout=10)
65 if res == 0:
66 self.path = path
67 return
68 if res == 1:
69 continue
70 else:
71 logging.error(self.ssh.before.decode("utf-8"))
72 raise CriticalError("Unexpected pexpect result: %d" % res)
73 raise CriticalError("Could not reserve a Juno board.")
74
75 def release(self):
76 """
77 Release a previously reserved Juno board.
78 """
79 if self.mounted:
80 self.unmount()
81 logging.info("Unmounted Juno storage device.")
82 if self.path == "":
83 raise CriticalError("No Juno board reserved.")
84 self.ssh.before = ""
85 self.ssh.sendline("%s/release.sh" % self.path)
86 res = self.ssh.expect(["RELEASE_SCRIPT_SUCCESS", "RELEASE_SCRIPT_FAIL", pexpect.EOF, \
87 pexpect.TIMEOUT], timeout=10)
88 if res == 0:
89 return
90 logging.error(self.ssh.before.decode("utf-8"))
91 raise CriticalError("Unexpected pexpect result: %d" % res)
92
93 def mount(self):
94 """
95 Mount the reserved Juno board storage device.
96 """
97 if self.path == "":
98 raise CriticalError("No Juno board reserved.")
99 if self.mounted:
100 return
101 self.ssh.before = ""
102 self.ssh.sendline("%s/mount.sh" % self.path)
103 res = self.ssh.expect(["password for", "MOUNT_SCRIPT_SUCCESS", pexpect.TIMEOUT, \
104 pexpect.EOF, "MOUNT_SCRIPT_FAIL"], timeout=10)
105 if res == 0:
106 self.ssh.before = ""
107 self.ssh.sendline("%s" % self.password)
108 res = self.ssh.expect(["MOUNT_SCRIPT_SUCCESS", "Sorry, try again.", pexpect.TIMEOUT, \
109 pexpect.EOF, "MOUNT_SCRIPT_FAIL"], timeout=10)
110 if res == 0:
111 self.mounted = True
112 return
113 elif res == 1:
114 raise CriticalError("Incorrect sudo password.")
115 logging.error(self.ssh.before.decode("utf-8"))
116 raise CriticalError("Unexpected pexpect result: %d" % res)
117 elif res == 1:
118 self.mounted = True
119 return
120 logging.error(self.ssh.before.decode("utf-8"))
121 raise CriticalError("Unexpected pexpect result: %d" % res)
122
123 def unmount(self):
124 """
125 Unmount the reserved Juno board storage device.
126 """
127 if self.path == "":
128 raise CriticalError("No Juno board reserved.")
129 if not self.mounted:
130 return
131 self.ssh.before = ""
132 self.ssh.sendline("%s/unmount.sh" % self.path)
133 # long timeout here since linux likes to queue file IO operations
134 res = self.ssh.expect(["password for", "UNMOUNT_SCRIPT_SUCCESS", pexpect.TIMEOUT, \
135 pexpect.EOF, "UNMOUNT_SCRIPT_FAIL"], timeout=600)
136 if res == 0:
137 self.ssh.before = ""
138 self.ssh.sendline("%s" % self.password)
139 res = self.ssh.expect(["UNMOUNT_SCRIPT_SUCCESS", "Sorry, try again.", pexpect.TIMEOUT, \
140 pexpect.EOF, "UNMOUNT_SCRIPT_FAIL"], timeout=600)
141 if res == 0:
142 self.mounted = False
143 return
144 elif res == 1:
145 raise CriticalError("Incorrect sudo password.")
146 logging.error(self.ssh.before.decode("utf-8"))
147 raise CriticalError("Unexpected pexpect result: %d" % res)
148 elif res == 1:
149 self.mounted = False
150 return
151 elif res == 2:
152 raise CriticalError("Timed out waiting for unmount.")
153 logging.error(self.ssh.before.decode("utf-8"))
154 raise CriticalError("Unexpected pexpect result: %d" % res)
155
156 def get_path(self):
157 """
158 Get the path to the reserved Juno board.
159 """
160 if self.path == "":
161 raise CriticalError("No Juno board reserved.")
162 return self.path
163
164 def __enter__(self):
165 # Attempt to reserve if it hasn't been done already
166 if self.path == "":
167 self.reserve()
168
169 def __exit__(self, exc_type, exc_value, exc_traceback):
170 self.release()
171
172################################################################################
173# Helper Functions #
174################################################################################
175
176def recover_juno(ssh, juno_board, uart):
177 """
178 If mount fails, this function attempts to power cycle the juno board, cancel
179 auto-boot, and manually enable debug USB before any potentially bad code
180 can run.
181 Parameters
182 ssh (pxssh object): ssh connection to remote machine
183 juno_board (JunoBoardManager): Juno instance to attempt to recover
184 uart (pexpect): Connection to juno uart
185 """
186 power_off(ssh, juno_board.get_path())
187 time.sleep(10)
188 power_on(ssh, juno_board.get_path())
189 # Wait for auto boot message thens end an enter press
190 res = uart.expect(["Press Enter to stop auto boot", pexpect.EOF, pexpect.TIMEOUT], timeout=60)
191 if res != 0:
192 raise CriticalError("Juno auto boot prompt not detected, recovery failed.")
193 uart.sendline("")
194 # Wait for MCC command prompt then send "usb_on"
195 res = uart.expect(["Cmd>", pexpect.EOF, pexpect.TIMEOUT], timeout=10)
196 if res != 0:
197 raise CriticalError("Juno MCC prompt not detected, recovery failed.")
198 uart.sendline("usb_on")
199 # Wait for debug usb confirmation
200 res = uart.expect(["Enabling debug USB...", pexpect.EOF, pexpect.TIMEOUT], timeout=10)
201 if res != 0:
202 raise CriticalError("Debug usb not enabled, recovery failed.")
203 # Dead wait for linux to detect the USB device then try to mount again
204 time.sleep(10)
205 juno_board.mount()
206
207def copy_file_to_remote(source, dest, remote, user, password):
208 """
209 Uses SCP to copy a file to a remote machine.
210 Parameters
211 source (string): Source path
212 dest (string): Destination path
213 remote (string): Name or IP address of remote machine
214 user (string): Username to login with
215 password (string): Password to login/sudo with
216 """
217 scp = "scp -r %s %s@%s:%s" % (source, user, remote, dest)
218 copy = pexpect.spawn(scp)
219 copy.expect("password:")
220 copy.sendline(password)
221 res = copy.expect([pexpect.EOF, pexpect.TIMEOUT], timeout=600)
222 if res == 0:
223 copy.close()
224 if copy.exitstatus == 0:
225 return
226 raise CriticalError("Unexpected error occurred during SCP: %d" % copy.exitstatus)
227 elif res == 1:
228 raise CriticalError("SCP operation timed out.")
229 raise CriticalError("Unexpected pexpect result: %d" % res)
230
231def extract_zip_file(source, dest):
232 """
233 Extracts a zip file on the local machine.
234 Parameters
235 source (string): Path to input zip file
236 dest (string): Path to output directory
237 """
238 try:
239 with zipfile.ZipFile(source, 'r') as src:
240 src.extractall(dest)
241 return
242 except Exception:
243 raise CriticalError("Could not extract boardfiles.")
244
245def remote_copy(ssh, source, dest):
246 """
247 Copy files from remote workspace to Juno directory using rsync
248 Parameters
249 ssh (pxssh object): Connection to remote system
250 source (string): Source file path
251 dest (string): Destination file path
252 """
253 ssh.before = ""
254 ssh.sendline("rsync -rt %s %s" % (source, dest))
255 res = ssh.expect(["$", pexpect.EOF, pexpect.TIMEOUT], timeout=60)
256 if res != 0:
257 logging.error(ssh.before.decode("utf-8"))
258 raise CriticalError("Unexpected error occurred during rsync operation.")
259 ssh.before = ""
260 ssh.sendline("echo $?")
261 res = ssh.expect(["0", pexpect.EOF, pexpect.TIMEOUT], timeout=10)
262 if res != 0:
263 logging.error(ssh.before.decode("utf-8"))
264 raise CriticalError("rsync failed")
265 return
266
267def connect_juno_uart(host, port):
268 """
269 Spawn a pexpect object for the Juno UART
270 Parameters
271 host (string): Telnet host name or IP addres
272 port (int): Telnet port number
273 Returns
274 pexpect object if successful
275 """
276 uart = pexpect.spawn("telnet %s %d" % (host, port))
277 result = uart.expect(["Escape character is", pexpect.EOF, pexpect.TIMEOUT], timeout=10)
278 if result == 0:
279 return uart
280 raise CriticalError("Could not connect to Juno UART.")
281
282def get_uart_port(ssh, juno):
283 """
284 Get the telnet port for the Juno UART
285 Parameters
286 ssh (pxssh object): SSH session to remote machine
287 Returns
288 int: Telnet port number
289 """
290 ssh.before = ""
291 ssh.sendline("cat %s/telnetport" % juno)
292 res = ssh.expect([pexpect.TIMEOUT], timeout=1)
293 if res == 0:
294 match = re.search(r"port: (\d+)", ssh.before.decode("utf-8"))
295 if match:
296 return int(match.group(1))
297 raise CriticalError("Could not get telnet port.")
298
299def power_off(ssh, juno):
300 """
301 Power off the Juno board
302 Parameters
303 ssh (pxssh object): SSH session to remote machine
304 juno (string): Path to Juno directory on remote
305 """
306 ssh.before = ""
307 ssh.sendline("%s/poweroff.sh" % juno)
308 res = ssh.expect(["POWEROFF_SCRIPT_SUCCESS", pexpect.EOF, pexpect.TIMEOUT, \
309 "POWEROFF_SCRIPT_FAIL"], timeout=10)
310 if res == 0:
311 return
312 logging.error(ssh.before.decode("utf-8"))
313 raise CriticalError("Could not power off the Juno board.")
314
315def power_on(ssh, juno):
316 """
317 Power on the Juno board
318 Parameters
319 ssh (pxssh object): SSH session to remote machine
320 juno (string): Path to Juno directory on remote
321 """
322 ssh.before = ""
323 ssh.sendline("%s/poweron.sh" % juno)
324 res = ssh.expect(["POWERON_SCRIPT_SUCCESS", pexpect.EOF, pexpect.TIMEOUT, \
325 "POWERON_SCRIPT_FAIL"], timeout=10)
326 if res == 0:
327 return
328 logging.error(ssh.before.decode("utf-8"))
329 raise CriticalError("Could not power on the Juno board.")
330
331def erase_juno(ssh, juno):
332 """
333 Erase the mounted Juno storage device
334 Parameters
335 ssh (pxssh object): SSH session to remote machine
336 juno (string): Path to Juno directory on remote
337 """
338 ssh.before = ""
339 ssh.sendline("%s/erasejuno.sh" % juno)
340 res = ssh.expect(["ERASEJUNO_SCRIPT_SUCCESS", "ERASEJUNO_SCRIPT_FAIL", pexpect.EOF, \
341 pexpect.TIMEOUT], timeout=30)
342 if res == 0:
343 return
344 logging.error(ssh.before.decode("utf-8"))
345 raise CriticalError("Could not erase the Juno storage device.")
346
347def erase_juno_workspace(ssh, juno):
348 """
349 Erase the Juno workspace
350 Parameters
351 ssh (pxssh object): SSH session to remote machine
352 juno (string): Path to Juno directory on remote
353 """
354 ssh.before = ""
355 ssh.sendline("%s/eraseworkspace.sh" % juno)
356 res = ssh.expect(["ERASEWORKSPACE_SCRIPT_SUCCESS", "ERASEWORKSPACE_SCRIPT_FAIL", pexpect.EOF, \
357 pexpect.TIMEOUT], timeout=30)
358 if res == 0:
359 return
360 logging.error(ssh.before.decode("utf-8"))
361 raise CriticalError("Could not erase the remote workspace.")
362
363def process_uart_output(uart, timeout, handler, telnethost, telnetport):
364 """
365 This function receives UART data from the Juno board, creates a full line
366 of text, then passes it to a test handler function.
367 Parameters
368 uart (pexpect): Pexpect process containing UART telnet session
369 timeout (int): How long to wait for test completion.
370 handler (function): Function to pass each line of test output to.
371 telnethost (string): Telnet host to use if uart connection fails.
372 telnetport (int): Telnet port to use if uart connection fails.
373 """
374 # Start timeout counter
375 timeout_start = datetime.datetime.now()
376
377 line = ""
378 while True:
379 try:
380 # Check if timeout has expired
381 elapsed = datetime.datetime.now() - timeout_start
382 if elapsed.total_seconds() > timeout:
383 raise CriticalError("Test timed out, see log file.")
384
385 # Read next character from Juno
386 char = uart.read_nonblocking(size=1, timeout=1).decode("utf-8")
387 if '\n' in char:
388 logging.info("JUNO: %s", line)
389
390 result = handler(uart, line)
391 if result == TestStatus.SUCCESS:
392 return
393 elif result == TestStatus.FAILURE:
394 raise CriticalError("Test manager returned TestStatus.FAILURE")
395
396 line = ""
397 else:
398 line = line + char
399
400 # uart.read_nonblocking will throw timeouts a lot by design so catch and ignore
401 except pexpect.TIMEOUT:
402 continue
403 except pexpect.EOF:
404 logging.warning("Connection lost unexpectedly, attempting to restart.")
405 try:
406 uart = connect_juno_uart(telnethost, telnetport)
407 except CriticalError:
408 raise CriticalError("Could not reopen Juno UART")
409 continue
410 except OSError:
411 raise CriticalError("Unexpected OSError occurred.")
412 except UnicodeDecodeError:
413 continue
414 except Exception as e:
415 # This case exists to catch any weird or rare exceptions.
416 raise CriticalError("Unexpected exception occurred: %s" % str(e))
417
418################################################################################
419# Test Handlers #
420################################################################################
421
422TEST_CASE_TFTF_MANUAL_PASS_COUNT = 0
423TEST_CASE_TFTF_MANUAL_CRASH_COUNT = 0
424TEST_CASE_TFTF_MANUAL_FAIL_COUNT = 0
425def test_case_tftf_manual(uart, line):
426 """
427 This function handles TFTF tests and parses the output into a pass or fail
428 result. Any crashes or fails result in an overall test failure but skips
429 and passes are fine.
430 """
431 global TEST_CASE_TFTF_MANUAL_PASS_COUNT
432 global TEST_CASE_TFTF_MANUAL_CRASH_COUNT
433 global TEST_CASE_TFTF_MANUAL_FAIL_COUNT
434
435 # This test needs to be powered back on a few times
436 if "Board powered down, use REBOOT to restart." in line:
437 # time delay to let things finish up
438 time.sleep(3)
439 uart.sendline("reboot")
440 return TestStatus.CONTINUE
441
442 elif "Tests Passed" in line:
443 match = re.search(r"Tests Passed : (\d+)", line)
444 if match:
445 TEST_CASE_TFTF_MANUAL_PASS_COUNT = int(match.group(1))
446 return TestStatus.CONTINUE
447 logging.error(r"Error parsing line: %s", line)
448 return TestStatus.FAILURE
449
450 elif "Tests Failed" in line:
451 match = re.search(r"Tests Failed : (\d+)", line)
452 if match:
453 TEST_CASE_TFTF_MANUAL_FAIL_COUNT = int(match.group(1))
454 return TestStatus.CONTINUE
455 logging.error("Error parsing line: %s", line)
456 return TestStatus.FAILURE
457
458 elif "Tests Crashed" in line:
459 match = re.search(r"Tests Crashed : (\d+)", line)
460 if match:
461 TEST_CASE_TFTF_MANUAL_CRASH_COUNT = int(match.group(1))
462 return TestStatus.CONTINUE
463 logging.error("Error parsing line: %s", line)
464 return TestStatus.FAILURE
465
466 elif "Total tests" in line:
467 if TEST_CASE_TFTF_MANUAL_PASS_COUNT == 0:
468 return TestStatus.FAILURE
469 if TEST_CASE_TFTF_MANUAL_CRASH_COUNT > 0:
470 return TestStatus.FAILURE
471 if TEST_CASE_TFTF_MANUAL_FAIL_COUNT > 0:
472 return TestStatus.FAILURE
473 return TestStatus.SUCCESS
474
475 return TestStatus.CONTINUE
476
477TEST_CASE_LINUX_MANUAL_SHUTDOWN_HALT_SENT = False
478def test_case_linux_manual_shutdown(uart, line):
479 """
480 This handler performs a linux manual shutdown test by waiting for the linux
481 prompt and sending the appropriate halt command, then waiting for the
482 expected output.
483 """
484 global TEST_CASE_LINUX_MANUAL_SHUTDOWN_HALT_SENT
485
486 # Look for Linux prompt
487 if "/ #" in line and TEST_CASE_LINUX_MANUAL_SHUTDOWN_HALT_SENT is False:
488 time.sleep(3)
489 uart.sendline("halt -f")
490 TEST_CASE_LINUX_MANUAL_SHUTDOWN_HALT_SENT = True
491 return TestStatus.CONTINUE
492
493 # Once halt command has been issued, wait for confirmation
494 elif "reboot: System halted" in line:
495 return TestStatus.SUCCESS
496
497 # For any other result, continue.
498 return TestStatus.CONTINUE
499
500################################################################################
501# Script Main #
502################################################################################
503
504def main():
505 """
506 Main function, handles the initial set up and test dispatch to Juno board.
507 """
508 parser = argparse.ArgumentParser(description="Launch a Juno manual test.")
509 parser.add_argument("host", type=str, help="Name or IP address of Juno host system.")
510 parser.add_argument("username", type=str, help="Username to login to host system.")
511 parser.add_argument("password", type=str, help="Password to login to host system.")
512 parser.add_argument("boardfiles", type=str, help="ZIP file containing Juno boardfiles.")
513 parser.add_argument("workspace", type=str, help="Directory for scratch files.")
514 parser.add_argument("testname", type=str, help="Name of test to run.")
515 parser.add_argument("timeout", type=int, help="Time to wait for test completion.")
516 parser.add_argument("logfile", type=str, help="Path to log file to create.")
517 parser.add_argument("-l", "--list", action='store_true', help="List supported test cases.")
518 args = parser.parse_args()
519
520 # Print list if requested
521 if args.list:
522 print("Supported Tests")
523 print(" tftf-manual-generic - Should work for all TFTF tests.")
524 print(" linux-manual-shutdown - Waits for Linux prompt and sends halt command.")
525 exit(0)
526
527 # Start logging
528 print("Creating log file: %s" % args.logfile)
529 logging.basicConfig(filename=args.logfile, level=logging.DEBUG, \
530 format="[%(asctime)s] %(message)s", datefmt="%I:%M:%S")
531 logging.getLogger().addHandler(logging.StreamHandler(sys.stdout))
532
533 # Make sure test name is supported so we don't waste time if it isn't.
534 if args.testname == "tftf-manual-generic":
535 test_handler = test_case_tftf_manual
536 elif args.testname == "linux-manual-shutdown":
537 test_handler = test_case_linux_manual_shutdown
538 else:
539 logging.error("Test name \"%s\" invalid or not supported.", args.testname)
540 exit(1)
541 logging.info("Selected test \"%s\"", args.testname)
542
543 # Helper functions either succeed or raise CriticalError so no error checking is done here
544 try:
545 # Start SSH session to host machine
546 logging.info("Starting SSH session to remote machine.")
547 ssh = pxssh.pxssh()
548 if not ssh.login(args.host, args.username, args.password):
549 raise CriticalError("Could not start SSH session.")
550 # Disable character echo
551 ssh.sendline("stty -echo")
552 with ssh:
553
554 # Try to reserve a juno board
555 juno_board = JunoBoardManager(ssh, args.password)
556 juno_board.reserve()
557 juno = juno_board.get_path()
558 logging.info("Reserved %s", juno)
559 with juno_board:
560
561 # Get UART port and start telnet session
562 logging.info("Opening Juno UART")
563 port = get_uart_port(ssh, juno)
564 logging.info("Using telnet port %d", port)
565 uart = connect_juno_uart(args.host, port)
566 with uart:
567
568 # Extract boardfiles locally
569 logging.info("Extracting boardfiles.")
570 local_boardfiles = os.path.join(args.workspace, "boardfiles")
571 if os.path.exists(local_boardfiles):
572 shutil.rmtree(local_boardfiles)
573 os.mkdir(local_boardfiles)
574 extract_zip_file(args.boardfiles, local_boardfiles)
575
576 # Clear out the workspace directory on the remote system
577 logging.info("Erasing remote workspace.")
578 erase_juno_workspace(ssh, juno)
579
580 # SCP boardfiles to juno host
581 logging.info("Copying boardfiles to remote system.")
582 copy_file_to_remote(os.path.join(local_boardfiles), \
583 os.path.join(juno, "workspace"), args.host, args.username, args.password)
584
585 # Try to mount the storage device
586 logging.info("Mounting the Juno storage device.")
587 try:
588 juno_board.mount()
589 except CriticalError:
590 logging.info("Mount failed, attempting to recover Juno board.")
591 recover_juno(ssh, juno_board, uart)
592 logging.info("Juno board recovered.")
593
594 # Move boardfiles from temp directory to juno storage
595 logging.info("Copying new boardfiles to storage device.")
596 remote_copy(ssh, os.path.join(juno, "workspace", "boardfiles", "*"), \
597 os.path.join(juno, "juno"))
598
599 # Unmounting the juno board.
600 logging.info("Unmounting Juno storage device and finishing pending I/O.")
601 juno_board.unmount()
602
603 # Power cycle the juno board to reboot it. */
604 logging.info("Rebooting the Juno board.")
605 power_off(ssh, juno)
606 # dead wait to let the power supply do its thing
607 time.sleep(10)
608 power_on(ssh, juno)
609
610 # dead wait to let the power supply do its thing
611 time.sleep(10)
612
613 # Process UART output and wait for test completion
614 process_uart_output(uart, args.timeout, test_handler, args.host, port)
615 logging.info("Tests Passed!")
616
617 except CriticalError as exception:
618 logging.error(str(exception))
619 exit(1)
620
621 # Exit with 0 on successful finish
622 exit(0)
623
624################################################################################
625# Script Entry Point #
626################################################################################
627
628if __name__ == "__main__":
629 main()