Append ansible yaml parse errors to job log file

If ansible can't parse the yaml, it dies immediately with return code 4.
This happens before callback plugins, so nothing gets captured to
the log. Save the first 200 lines in case there result is a syntax
error. If we detect one, append directly to the job's log file.

Change-Id: I059a61357c471f61485f141f92c35b2bcf2d168d
diff --git a/zuul/executor/server.py b/zuul/executor/server.py
index 94678d7..0a43ca8 100644
--- a/zuul/executor/server.py
+++ b/zuul/executor/server.py
@@ -13,6 +13,7 @@
 # under the License.
 
 import collections
+import datetime
 import json
 import logging
 import os
@@ -33,6 +34,7 @@
 import zuul.ansible
 from zuul.lib import commandsocket
 
+BUFFER_LINES_FOR_SYNTAX = 200
 COMMANDS = ['stop', 'pause', 'unpause', 'graceful', 'verbose',
             'unverbose']
 
@@ -1298,13 +1300,16 @@
                 env=env_copy,
             )
 
+        syntax_buffer = []
         ret = None
         if timeout:
             watchdog = Watchdog(timeout, self._ansibleTimeout,
                                 ("Ansible timeout exceeded",))
             watchdog.start()
         try:
-            for line in iter(self.proc.stdout.readline, b''):
+            for idx, line in enumerate(iter(self.proc.stdout.readline, b'')):
+                if idx < BUFFER_LINES_FOR_SYNTAX:
+                    syntax_buffer.append(line)
                 line = line[:1024].rstrip()
                 self.log.debug("Ansible output: %s" % (line,))
             self.log.debug("Ansible output terminated")
@@ -1327,6 +1332,18 @@
         elif ret == -9:
             # Received abort request.
             return (self.RESULT_ABORTED, None)
+        elif ret == 4:
+            # Ansible could not parse the yaml.
+            self.log.debug("Ansible parse error")
+            # TODO(mordred) If/when we rework use of logger in ansible-playbook
+            # we'll want to change how this works to use that as well. For now,
+            # this is what we need to do.
+            with open(self.jobdir.job_output_file, 'a') as job_output:
+                job_output.write("{now} | ANSIBLE PARSE ERROR\n".format(
+                    now=datetime.datetime.now()))
+                for line in syntax_buffer:
+                    job_output.write("{now} | {line}".format(
+                        now=datetime.datetime.now(), line=line))
 
         return (self.RESULT_NORMAL, ret)