1#!/usr/bin/python2 2# Copyright (c) 2012 The Chromium OS Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""Clean Staged Images. 7 8This script is responsible for removing older builds from the Chrome OS 9devserver. It walks through the files in the images folder, check each found 10staged.timestamp and do following. 111. Check if the build target is in the list of targets that need to keep the 12 latest build. Skip processing the directory if that's True. 132. Check if the modified time of the timestamp file is older than a given cutoff 14 time, e.g., 24 hours before the current time. 153. If that's True, delete the folder containing staged.timestamp. 164. Check if the parent folder of the deleted foler is empty. If that's True, 17 delete the parent folder as well. Do so recursively, until it hits the top 18 folder, e.g., |~/images|. 19""" 20 21import logging 22import optparse 23import os 24import re 25import sys 26import shutil 27import time 28 29# This filename must be kept in sync with devserver's downloader.py 30_TIMESTAMP_FILENAME = 'staged.timestamp' 31_HOURS_TO_SECONDS = 60 * 60 32_EXEMPTED_DIRECTORIES = [] 33 34def get_all_timestamp_dirs(root): 35 """Get all directories that has timestamp file. 36 37 @param root: The top folder to look for timestamp file. 38 @return: An iterator of directories that have timestamp file in it. 39 """ 40 for dir_path, dir_names, file_names in os.walk(root): 41 if os.path.basename(dir_path) in _EXEMPTED_DIRECTORIES: 42 logging.debug('Skipping %s', dir_path) 43 dir_names[:] = [] 44 elif _TIMESTAMP_FILENAME in file_names: 45 dir_names[:] = [] 46 yield dir_path 47 48 49def file_is_too_old(build_path, max_age_hours): 50 """Test to see if the build at |build_path| is older than |max_age_hours|. 51 52 @param build_path: The path to the build (ie. 'build_dir/R21-2035.0.0') 53 @param max_age_hours: The maximum allowed age of a build in hours. 54 @return: True if the build is older than |max_age_hours|, False otherwise. 55 """ 56 cutoff = time.time() - max_age_hours * _HOURS_TO_SECONDS 57 timestamp_path = os.path.join(build_path, _TIMESTAMP_FILENAME) 58 if os.path.exists(timestamp_path): 59 age = os.stat(timestamp_path).st_mtime 60 if age < cutoff: 61 return True 62 return False 63 64 65def try_delete_parent_dir(path, root): 66 """Try to delete parent directory if it's empty. 67 68 Recursively attempt to delete parent directory of given path. Only stop if: 69 1. parent directory is the root directory used to stage images. 70 2. The base name of given path is a valid build path, e.g., R31-4532.0.0 or 71 4530.0.0 (for builds staged in *-channel/[platform]/). 72 3. The parent directory is not empty. 73 74 @param path: Start path that attempt to delete whose parent directory. 75 @param root: root directory that devserver used to stage images, e.g., 76 |/usr/local/google/home/dshi/images|, must be an absolute path. 77 """ 78 pattern = '(\d+\.\d+\.\d+)' 79 match = re.search(pattern, os.path.basename(path)) 80 if match: 81 return 82 83 parent_dir = os.path.abspath(os.path.join(path, os.pardir)) 84 if parent_dir == root: 85 return 86 87 try: 88 os.rmdir(parent_dir) 89 try_delete_parent_dir(parent_dir, root) 90 except OSError: 91 pass 92 93 94def prune_builds(builds_dir, keep_duration, keep_paladin_duration): 95 """Prune the build dirs and also delete old labels. 96 97 @param builds_dir: The builds dir where all builds are staged. 98 on the chromeos-devserver this is ~chromeos-test/images/ 99 @param keep_duration: How old of regular builds to keep around. 100 @param keep_paladin_duration: How old of Paladin builds to keep around. 101 """ 102 for timestamp_dir in get_all_timestamp_dirs(builds_dir): 103 logging.debug('Processing %s', timestamp_dir) 104 if '-paladin/' in timestamp_dir: 105 keep = keep_paladin_duration 106 else: 107 keep = keep_duration 108 if file_is_too_old(timestamp_dir, keep): 109 logging.debug('Deleting %s', timestamp_dir) 110 shutil.rmtree(timestamp_dir) 111 # Resursively delete parent folders 112 try_delete_parent_dir(timestamp_dir, builds_dir) 113 114 115def main(): 116 """Main routine.""" 117 usage = 'usage: %prog [options] images_dir' 118 parser = optparse.OptionParser(usage=usage) 119 parser.add_option('-a', '--max-age', default=24, type=int, 120 help='Number of hours to keep normal builds: %default') 121 parser.add_option('-p', '--max-paladin-age', default=24, type=int, 122 help='Number of hours to keep paladin builds: %default') 123 parser.add_option('-v', '--verbose', 124 dest='verbose', action='store_true', default=False, 125 help='Run in verbose mode') 126 options, args = parser.parse_args() 127 if len(args) != 1: 128 parser.print_usage() 129 sys.exit(1) 130 131 builds_dir = os.path.abspath(args[0]) 132 if not os.path.exists(builds_dir): 133 logging.error('Builds dir %s does not exist', builds_dir) 134 sys.exit(1) 135 136 if options.verbose: 137 logging.getLogger().setLevel(logging.DEBUG) 138 else: 139 logging.getLogger().setLevel(logging.INFO) 140 141 prune_builds(builds_dir, options.max_age, options.max_paladin_age) 142 143 144if __name__ == '__main__': 145 main() 146