Home | History | Annotate | Download | only in site_utils
      1 #!/usr/bin/python
      2 #
      3 # Copyright (c) 2015 The Chromium OS Authors. All rights reserved.
      4 # Use of this source code is governed by a BSD-style license that can be
      5 # found in the LICENSE file.
      6 
      7 
      8 """Tool for cleaning up labels that are not in use.
      9 
     10 Delete given labels from database when they are not in use.
     11 Labels that match the query `SELECT_USED_LABELS_FORMAT` are considered in use.
     12 When given labels are not in the used labels, those labels are deleted.
     13 
     14 For example, following command deletes all labels whose name begins with
     15 'cros-version' and are not in use.
     16 
     17 ./label_cleaner.py -p cros-version
     18 
     19 If '-p' option is not given, we delete labels whose name is exactly
     20 'cros-version' and are not in use.
     21 """
     22 
     23 
     24 import argparse
     25 import logging
     26 import MySQLdb
     27 import socket
     28 import sys
     29 import traceback
     30 
     31 import common
     32 from autotest_lib.client.common_lib import global_config
     33 from autotest_lib.client.common_lib import logging_config
     34 from autotest_lib.server import frontend
     35 
     36 
     37 GLOBAL_AFE = global_config.global_config.get_config_value(
     38         'SERVER', 'global_afe_hostname')
     39 DB_SERVER = global_config.global_config.get_config_value('AUTOTEST_WEB', 'host')
     40 USER = global_config.global_config.get_config_value('AUTOTEST_WEB', 'user')
     41 PASSWD = global_config.global_config.get_config_value(
     42         'AUTOTEST_WEB', 'password')
     43 DATABASE = global_config.global_config.get_config_value(
     44         'AUTOTEST_WEB', 'database')
     45 
     46 SELECT_USED_LABELS_FORMAT = """
     47 SELECT DISTINCT(label_id) FROM afe_autotests_dependency_labels UNION
     48 SELECT DISTINCT(label_id) FROM afe_hosts_labels UNION
     49 SELECT DISTINCT(label_id) FROM afe_jobs_dependency_labels UNION
     50 SELECT DISTINCT(label_id) FROM afe_shards_labels UNION
     51 SELECT DISTINCT(label_id) FROM afe_parameterized_jobs UNION
     52 SELECT DISTINCT(meta_host) FROM afe_host_queue_entries
     53 """
     54 
     55 SELECT_LABELS_FORMAT = """
     56 SELECT id FROM afe_labels WHERE name %s
     57 """
     58 
     59 DELETE_LABELS_FORMAT = """
     60 DELETE FROM afe_labels WHERE id in (%s)
     61 """
     62 
     63 
     64 def get_used_labels(conn):
     65     """Get labels that are currently in use.
     66 
     67     @param conn: MySQLdb Connection object.
     68 
     69     @return: A list of label ids.
     70     """
     71     cursor = conn.cursor()
     72     sql = SELECT_USED_LABELS_FORMAT
     73     try:
     74         cursor.execute(sql)
     75         rows = cursor.fetchall()
     76     except:
     77         logging.error("Query failed: %s", sql)
     78         raise
     79     return set(r[0] for r in rows)
     80 
     81 
     82 def fetch_labels(conn, label, prefix):
     83     """Fetch labels from database.
     84 
     85     @param conn: MySQLdb Connection object.
     86     @param label: Label name to fetch.
     87     @param prefix: If True, use `label` as a prefix. Otherwise, fetch
     88                    labels whose name is exactly same as `label`.
     89 
     90     @return: A list of label ids.
     91     """
     92     cursor = conn.cursor()
     93     if prefix:
     94         sql = SELECT_LABELS_FORMAT % ('LIKE "%s%%"' % label)
     95     else:
     96         sql = SELECT_LABELS_FORMAT % ('= "%s"' % label)
     97     try:
     98         cursor.execute(sql)
     99         rows = cursor.fetchall()
    100     except:
    101         logging.error("Query failed: %s", sql)
    102         raise
    103     return set(r[0] for r in rows)
    104 
    105 
    106 def _delete_labels(conn, labels):
    107     """Helper function of `delete_labels`."""
    108     labels_str = ','.join([str(l) for l in labels])
    109     logging.info("Deleting following labels: %s ..", labels_str)
    110     sql = DELETE_LABELS_FORMAT % labels_str
    111     try:
    112         conn.cursor().execute(sql)
    113         conn.commit()
    114     except:
    115         logging.error("Query failed: %s", sql)
    116         raise
    117     logging.info("Done.")
    118 
    119 
    120 def delete_labels(conn, labels, max_delete):
    121     """Delete given labels from database.
    122 
    123     @param conn: MySQLdb Connection object.
    124     @param labels: Labels to delete. Set type.
    125     @param max_delete: Max number of records to delete in a query.
    126     """
    127     if not labels:
    128         logging.warn("No label to delete.")
    129         return
    130     while labels:
    131         labels_to_del = set()
    132         for i in xrange(min(len(labels), max_delete)):
    133             labels_to_del.add(labels.pop())
    134         _delete_labels(conn, labels_to_del)
    135 
    136 
    137 def is_primary_server():
    138     """Check if this server's status is primary
    139 
    140     @return: True if primary, False otherwise.
    141     """
    142     server = frontend.AFE(server=GLOBAL_AFE).run(
    143             'get_servers', hostname=socket.getfqdn())
    144     if server and server[0]['status'] == 'primary':
    145         return True
    146     return False
    147 
    148 
    149 def main():
    150     parser = argparse.ArgumentParser(
    151             formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    152     parser.add_argument('--db', dest='db_server',
    153                         help='Database server', default=DB_SERVER)
    154     parser.add_argument('-p', dest='prefix', action='store_true',
    155             help=('Use argument <label> as a prefix for matching. '
    156                   'For example, when the argument <label> is "cros-version" '
    157                   'and this option is enabled, then labels whose name '
    158                   'beginning with "cros-version" are matched. When this '
    159                   'option is disabled, we match labels whose name is '
    160                   'exactly same as the argument <label>.'))
    161     parser.add_argument('-n', dest='max_delete', type=int,
    162            help=('Max number of records to delete in each query.'),
    163            default=100)
    164     parser.add_argument('-s', dest='check_status', action='store_true',
    165            help=('Enforce to run only in a server that has primary status'))
    166     parser.add_argument('label', help='Label name to delete')
    167     options = parser.parse_args()
    168 
    169     logging_config.LoggingConfig().configure_logging(
    170             datefmt='%Y-%m-%d %H:%M:%S')
    171 
    172     try:
    173         msg = 'Label cleaner starts. Will delete '
    174         if options.prefix:
    175             msg += 'all labels whose prefix is "%s".'
    176         else:
    177             msg += 'a label "%s".'
    178         logging.info(msg, options.label)
    179         logging.info('Target database: %s.', options.db_server)
    180         if options.check_status and not is_primary_server():
    181             logging.error('Cannot run in a non-primary server.')
    182             return 1
    183 
    184         conn = MySQLdb.connect(host=options.db_server, user=USER,
    185                                passwd=PASSWD, db=DATABASE)
    186         used_labels = get_used_labels(conn)
    187         labels = fetch_labels(conn, options.label, options.prefix)
    188         delete_labels(conn, labels - used_labels, options.max_delete)
    189         logging.info('Done.')
    190     except:
    191         logging.error(traceback.format_exc())
    192         return 1
    193 
    194 
    195 if __name__ == '__main__':
    196     sys.exit(main())
    197