--- /dev/null
+import docopt
+
+import teuthology.config
+import teuthology.prune
+
+doc = """
+usage:
+ teuthology-prune-logs -h
+ teuthology-prune-logs [-v] [options]
+
+Prune old logfiles from the archive
+
+optional arguments:
+ -h, --help Show this help message and exit
+ -v, --verbose Be more verbose
+ -a ARCHIVE, --archive ARCHIVE
+ The base archive directory
+ [default: {archive_base}]
+ --dry-run Don't actually delete anything; just log what would be
+ deleted
+ -p DAYS, --pass DAYS Remove all logs for jobs which passed and are older
+ than DAYS. Negative values will skip this operation.
+ [default: 14]
+ -r DAYS, --remotes DAYS
+ Remove the 'remote' subdir of jobs older than DAYS.
+ Negative values will skip this operation.
+ [default: 60]
+""".format(archive_base=teuthology.config.config.archive_base)
+
+
+def main():
+ args = docopt.docopt(doc)
+ teuthology.prune.main(args)
--- /dev/null
+import logging
+import os
+import shutil
+import time
+
+import teuthology
+from teuthology.contextutil import safe_while
+
+log = logging.getLogger(__name__)
+
+
+# If we see this in any directory, we do not prune it
+PRESERVE_FILE = '.preserve'
+
+
+def main(args):
+ """
+ Main function; parses args and calls prune_archive()
+ """
+ verbose = args['--verbose']
+ if verbose:
+ teuthology.log.setLevel(logging.DEBUG)
+ archive_dir = args['--archive']
+ dry_run = args['--dry-run']
+ pass_days = int(args['--pass'])
+ remotes_days = int(args['--remotes'])
+
+ prune_archive(archive_dir, pass_days, remotes_days, dry_run)
+
+
+def prune_archive(archive_dir, pass_days, remotes_days, dry_run=False):
+ """
+ Walk through the archive_dir, calling the cleanup functions to process
+ directories that might be old enough
+ """
+ max_days = max(pass_days, remotes_days)
+ run_dirs = list()
+ log.debug("Archive {archive} has {count} children".format(
+ archive=archive_dir, count=len(os.listdir(archive_dir))))
+ for child in listdir(archive_dir):
+ item = os.path.join(archive_dir, child)
+ # Ensure that the path is not a symlink, is a directory, and is old
+ # enough to process
+ if (not os.path.islink(item) and os.path.isdir(item) and
+ is_old_enough(item, max_days)):
+ run_dirs.append(item)
+ for run_dir in run_dirs:
+ log.debug("Processing %s ..." % run_dir)
+ maybe_remove_passes(run_dir, pass_days, dry_run)
+ maybe_remove_remotes(run_dir, remotes_days, dry_run)
+
+
+def listdir(path):
+ with safe_while(sleep=1, increment=1, tries=10) as proceed:
+ while proceed():
+ try:
+ return os.listdir(path)
+ except OSError:
+ log.exception("Failed to list %s !" % path)
+
+
+def should_preserve(dir_name):
+ """
+ Should the directory be preserved?
+
+ :returns: True if the directory contains a file named '.preserve'; False
+ otherwise
+ """
+ preserve_path = os.path.join(dir_name, PRESERVE_FILE)
+ if os.path.isdir(dir_name) and os.path.exists(preserve_path):
+ return True
+ return False
+
+
+def is_old_enough(file_name, days):
+ """
+ :returns: True if the file's modification date is earlier than the amount
+ of days specified
+ """
+ now = time.time()
+ secs_to_days = lambda s: s / (60 * 60 * 24)
+ age = now - os.path.getmtime(file_name)
+ if secs_to_days(age) > days:
+ return True
+ return False
+
+
+def remove(path):
+ """
+ Attempt to recursively remove a directory. If an OSError is encountered,
+ log it and continue.
+ """
+ try:
+ shutil.rmtree(path)
+ except OSError:
+ log.exception("Failed to remove %s !" % path)
+
+
+def maybe_remove_passes(run_dir, days, dry_run=False):
+ """
+ Remove entire job log directories if they are old enough and the job passed
+ """
+ if days < 0:
+ return
+ contents = listdir(run_dir)
+ if PRESERVE_FILE in contents:
+ return
+ for child in contents:
+ item = os.path.join(run_dir, child)
+ # Ensure the path isn't marked for preservation, that it is a
+ # directory, and that it is old enough
+ if (should_preserve(item) or not os.path.isdir(item) or not
+ is_old_enough(item, days)):
+ continue
+ # Is it a job dir?
+ summary_path = os.path.join(item, 'summary.yaml')
+ if not os.path.exists(summary_path):
+ continue
+ # Is it a passed job?
+ summary_lines = [line.strip() for line in
+ file(summary_path).readlines()]
+ if 'success: true' in summary_lines:
+ log.info("{job} is a {days}-day old passed job; removing".format(
+ job=item, days=days))
+ if not dry_run:
+ remove(item)
+
+
+def maybe_remove_remotes(run_dir, days, dry_run=False):
+ """
+ Remove remote logs (not teuthology logs) from job directories if they are
+ old enough
+ """
+ if days < 0:
+ return
+ contents = listdir(run_dir)
+ if PRESERVE_FILE in contents:
+ return
+ for child in contents:
+ item = os.path.join(run_dir, child)
+ # Ensure the path isn't marked for preservation, that it is a
+ # directory, and that it is old enough
+ if (should_preserve(item) or not os.path.isdir(item) or not
+ is_old_enough(item, days)):
+ continue
+ # Does it have a remote subdir?
+ remote_path = os.path.join(item, 'remote')
+ if not os.path.isdir(remote_path):
+ continue
+ log.info("{job} is {days} days old; removing remote logs".format(
+ job=item, days=days))
+ if not dry_run:
+ remove(remote_path)