factory-archiver: handle the error cases of tar command

Implement the logic for error cases of tar command.

In addition,
 - fix and improve the clean-up and in the unittest.
 - add "gpg -k" to trigger the first initialization.

BUG=chrome-os-partner:27567
TEST=make lint LINT_WHITELIST=\
"py/lumberjack/archiver.py \
 py/lumberjack/archiver_cli.py \
 py/lumberjack/archiver_config.py \
 py/lumberjack/archiver_exception.py \
 py/lumberjack/common.py \
 py/lumberjack/archiver_unittest.py "
TEST=make test

Change-Id: I61069c7e7115f78f59991e55417ab269c229be0a
Reviewed-on: https://chromium-review.googlesource.com/198160
Tested-by: Chun-ta Lin <itspeter@chromium.org>
Reviewed-by: Ricky Liang <jcliang@chromium.org>
Commit-Queue: Chun-ta Lin <itspeter@chromium.org>
diff --git a/py/lumberjack/archiver.py b/py/lumberjack/archiver.py
index 803105f..266fc75 100644
--- a/py/lumberjack/archiver.py
+++ b/py/lumberjack/archiver.py
@@ -8,6 +8,7 @@
 import hashlib
 import logging
 import os
+import pprint
 import re
 import shutil
 import tempfile
@@ -15,6 +16,7 @@
 import uuid
 import yaml
 
+from archiver_exception import ArchiverFieldError
 from subprocess import PIPE, Popen
 from twisted.internet import reactor
 
@@ -28,6 +30,10 @@
                          'archiver_cli.py', 'archiver_config.py']
 # Global variable to keep locked file open during process life-cycle
 locks = []
+# Global variable to postpone the time (i.e. give few more archiving cycle
+# a chance to recover automatically) of raising exception
+archive_failures = []
+MAX_ALOOWED_FAILURES = 5
 
 
 def _ListEligibleFiles(dir_path):
@@ -401,10 +407,16 @@
         config.archived_dir, _GenerateArchiveName(config))
     tmp_archive = generated_archive + '.part'
     logging.info('Compressing %r into %r', tmp_dir, tmp_archive)
-    # TODO(itspeter): Handle the failure cases.
-    output_tuple = Popen(  # pylint: disable=W0612
-        ['tar', '-cvJf', tmp_archive, '-C', tmp_dir, '.'],
-        stdout=PIPE, stderr=PIPE).communicate()
+    cmd_line = ['tar', '-cvJf', tmp_archive, '-C', tmp_dir, '.']
+    p = Popen(cmd_line, stdout=PIPE, stderr=PIPE)
+    stdout, stderr = p.communicate()
+    if p.returncode != 0:
+      error_msg = ('Command %r failed. retcode[%r]\nstdout:\n%s\n\n'
+                   'stderr:\n%s\n' % (cmd_line, p.returncode, stdout, stderr))
+      logging.error(error_msg)
+      archive_failures.append(error_msg)
+      raise ArchiverFieldError(error_msg)
+
     # Remove .part suffix.
     os.rename(tmp_archive, generated_archive)
     return generated_archive
@@ -431,9 +443,19 @@
     if len(archive_metadata['files']) > 0:
       _UpdateMetadata(archive_metadata, started_time)  # Write other info
       # TODO(itspeter): Handle the .zip compress_format.
-      generated_archive = _CompressIntoTarXz(tmp_dir, archive_metadata, config)
-      # Update metadata data for archived files.
-      _UpdateArchiverMetadata(archive_metadata['files'], config)
+      try:
+        generated_archive = _CompressIntoTarXz(tmp_dir,
+                                               archive_metadata, config)
+        # Update metadata data for archived files only when compression
+        # succeed.
+        _UpdateArchiverMetadata(archive_metadata['files'], config)
+      except ArchiverFieldError:
+        # Do not raise error since it will stop the archiver. We would like to
+        # give few more retires in coming cycle.
+        if len(archive_failures) > MAX_ALOOWED_FAILURES:
+          raise ArchiverFieldError(
+              'Archiver failed %d times. The error messages are\n%s\n' %
+              pprint.pformat(archive_failures))
     else:
       logging.info('No data available for archiving for %r', config.data_type)
 
diff --git a/py/lumberjack/archiver_config.py b/py/lumberjack/archiver_config.py
index 6abe8c7..d907ab4 100644
--- a/py/lumberjack/archiver_config.py
+++ b/py/lumberjack/archiver_config.py
@@ -16,7 +16,7 @@
 from archiver import locks
 from archiver_exception import ArchiverFieldError
 from common import CheckAndLockFile, CheckExecutableExist, TryMakeDirs
-from subprocess import PIPE, Popen
+from subprocess import check_call, PIPE, Popen
 
 
 ALLOWED_DATA_TYPE = set(['eventlog', 'reports', 'regcode'])
@@ -260,6 +260,11 @@
     if not CheckExecutableExist('gpg'):
       raise ArchiverFieldError(
           'GnuPG(gpg) is not callable. It is required for encryption.')
+    # List the existing keys via "gpg -k". This step is to make sure local
+    # gpg initializes its database so following commands can be run wihtout
+    # issues.
+    check_call(['gpg', '-k'])
+
     # Check if the public key's format and recipient are valid.
     # Since we don't have the private key, we can only verify if the public
     # key is working properly with gpg.
diff --git a/py/lumberjack/archiver_unittest.py b/py/lumberjack/archiver_unittest.py
index f3223c2..761bc79 100755
--- a/py/lumberjack/archiver_unittest.py
+++ b/py/lumberjack/archiver_unittest.py
@@ -49,24 +49,25 @@
   def tearDown(self):
     os.chdir(self.pwd)
     directories_to_delete = [
-      os.path.join(TEST_DATA_PATH, 'archives'),
-      os.path.join(TEST_DATA_PATH, 'raw/report'),
-      os.path.join(TEST_DATA_PATH, 'raw/regcode'),
+      'archives', 'raw/report', 'raw/regcode',
       # Clean-up to make git status cleaner
-      os.path.join(TEST_DATA_PATH, 'raw/eventlog/20140406/.archiver'),
-      os.path.join(TEST_DATA_PATH, 'raw/eventlog/20140419/.archiver'),
-      os.path.join(TEST_DATA_PATH, 'raw/eventlog/20140420/.archiver'),
-      os.path.join(TEST_DATA_PATH, 'raw/eventlog/20140421/.archiver')]
+      'raw/eventlog/20140406/.archiver', 'raw/eventlog/20140419/.archiver',
+      'raw/eventlog/20140420/.archiver', 'raw/eventlog/20140421/.archiver']
     for directory in directories_to_delete:
       try:
-        shutil.rmtree(directory)
+        shutil.rmtree(os.path.join(TEST_DATA_PATH, directory))
       except: # pylint: disable=W0702
         pass
+
     # Delete lock file
-    try:
-      os.unlink(os.path.join(TEST_DATA_PATH, 'raw/eventlog/.archiver.lock'))
-    except: # pylint: disable=W0702
-      pass
+    lock_file_to_delete = [
+        'raw/eventlog/.archiver.lock',
+        'raw/.mocked_regcode.csv.archiver.lock']
+    for lock_file in lock_file_to_delete:
+      try:
+        os.unlink(os.path.join(TEST_DATA_PATH, lock_file))
+      except: # pylint: disable=W0702
+        pass
 
   def testYAMLConfigNonExist(self):
     argv = ['dry-run', 'nonexist.yaml']