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