faft: Capture logs from servo log-dir, revised

This is a rework of CL:1808442, with several changes:

* Use try/catch to handle rotate_servod_logs control not existing.
  This can happen if a labstation is out of date.

* Move the method body, to improve the call stack reported in syslog:
  autotest[1234]: from [rotate_servod_logs|rotate_servod_logs|run]...
  autotest[1234]: from [initialize|rotate_servod_logs|run] ssh_run...

* Delete the old 'latest' link, to ensure old logs won't be reused
  even if log rotation is switched on or off between runs.

BUG=chromium:1011382
BUG=chromium:932820
TEST=Run suite:faft_lv1, check results for files matching servod.*

Change-Id: I0bd35c0b296fc312ba19f5f85bc54088a12884bf
Reviewed-on: https://chromium-review.googlesource.com/1846520
Tested-by: Dana Goyette <dgoyette@chromium.org>
Commit-Ready: ChromeOS CL Exonerator Bot <chromiumos-cl-exonerator@appspot.gserviceaccount.com>
Legacy-Commit-Queue: Commit Bot <commit-bot@chromium.org>
Reviewed-by: Ruben Rodriguez Buchillon <coconutruben@chromium.org>
Reviewed-by: Mary Ruthven <mruthven@chromium.org>
diff --git a/server/cros/servo/servo.py b/server/cros/servo/servo.py
index f37595f..acf0825 100644
--- a/server/cros/servo/servo.py
+++ b/server/cros/servo/servo.py
@@ -314,16 +314,73 @@
         """Returns the serial number of the servo board."""
         return self._servo_serial
 
-    def fetch_servod_log(self, filename=None, skip_old=False):
-        """Save the servod log into the given local file.
+    def rotate_servod_logs(self, filename=None, directory=None):
+        """Save the latest servod log into a local directory, then rotate logs.
 
-        @param filename: save the contents into a file with the given name.
-        @param skip_old: if True, skip past the old data in the log file.
-        @type filename: str
-        @type skip_old: bool
-        @rtype: None
+        The files will be <filename>.DEBUG, <filename>.INFO, <filename>.WARNING,
+        or just <filename>.log if not using split level logging.
+
+        @param filename: local filename prefix (no file extension) to use.
+                         If None, rotate log but don't save it.
+        @param directory: local directory to save logs into (if unset, use cwd)
         """
-        return self._servo_host.fetch_servod_log(filename, skip_old)
+        if self.is_localhost():
+            # Local servod usually runs without log-dir, so can't be collected.
+            # TODO(crbug.com/1011516): allow localhost when rotation is enabled
+            return
+
+        log_dir = '/var/log/servod_%s' % self._servo_host.servo_port
+
+        if filename:
+            # TODO(crrev.com/c/1793030): remove no-level case once CL is pushed
+            for level_name in ('', 'DEBUG', 'INFO', 'WARNING'):
+
+                remote_path = os.path.join(log_dir, 'latest')
+                if level_name:
+                    remote_path += '.%s' % level_name
+
+                local_path = '%s.%s' % (filename, level_name or 'log')
+                if directory:
+                    local_path = os.path.join(directory, local_path)
+
+                try:
+                    self._servo_host.get_file(
+                            remote_path, local_path, try_rsync=False)
+
+                except error.AutoservRunError as e:
+                    result = e.result_obj
+                    if result.exit_status != 0:
+                        stderr = result.stderr.strip()
+
+                        # File not existing is okay, but warn for anything else.
+                        if 'no such' not in stderr.lower():
+                            logging.warn(
+                                    "Couldn't retrieve servod log: %s",
+                                    stderr or '\n%s' % result)
+
+                try:
+                    if os.stat(local_path).st_size == 0:
+                        os.unlink(local_path)
+                except EnvironmentError:
+                    pass
+
+        else:
+            # No filename given, so caller wants to discard the log lines.
+            # Remove the symlinks to prevent old log-dir links from being
+            # picked up multiple times when using servod without log-dir.
+            remote_path = os.path.join(log_dir, 'latest*')
+            self._servo_host.run(
+                    "rm %s" % remote_path,
+                    stderr_tee=None, ignore_status=True)
+
+        # Servod log rotation renames current log, then creates a new file with
+        # the old name: log.<date> -> log.<date>.1.tbz2 -> log.<date>.2.tbz2
+
+        # Must rotate after copying, or the copy would be the new, empty file.
+        try:
+            self.set_nocheck('rotate_servod_logs', 'yes')
+        except ControlUnavailableError as e:
+            logging.warn("Couldn't rotate servod logs: %s", str(e))
 
     def get_power_state_controller(self):
         """Return the power state controller for this Servo.