Mike Frysinger | d03e6b5 | 2019-08-03 12:49:01 -0400 | [diff] [blame] | 1 | #!/usr/bin/python2 |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 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 | |
MK Ryu | a34e3b1 | 2015-08-21 16:20:47 -0700 | [diff] [blame] | 8 | """Tool for cleaning up labels that are not in use. |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 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 |
Prathmesh Prabhu | 2f02ab8 | 2018-01-11 11:15:54 -0800 | [diff] [blame] | 26 | import os |
MK Ryu | a34e3b1 | 2015-08-21 16:20:47 -0700 | [diff] [blame] | 27 | import socket |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 28 | import sys |
Prathmesh Prabhu | 2f02ab8 | 2018-01-11 11:15:54 -0800 | [diff] [blame] | 29 | import tempfile |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 30 | |
| 31 | import common |
Prathmesh Prabhu | 194b870 | 2017-08-07 15:55:50 -0700 | [diff] [blame] | 32 | # Installed via build_externals, must be after import common. |
| 33 | import MySQLdb |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 34 | from autotest_lib.client.common_lib import global_config |
| 35 | from autotest_lib.client.common_lib import logging_config |
MK Ryu | a34e3b1 | 2015-08-21 16:20:47 -0700 | [diff] [blame] | 36 | from autotest_lib.server import frontend |
Prathmesh Prabhu | 8268449 | 2017-08-07 16:28:37 -0700 | [diff] [blame] | 37 | from chromite.lib import metrics |
| 38 | from chromite.lib import ts_mon_config |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 39 | |
| 40 | |
Prathmesh Prabhu | 8268449 | 2017-08-07 16:28:37 -0700 | [diff] [blame] | 41 | _METRICS_PREFIX = 'chromeos/autotest/afe_db/admin/label_cleaner' |
| 42 | |
MK Ryu | 37325b4 | 2015-08-25 15:41:39 -0700 | [diff] [blame] | 43 | GLOBAL_AFE = global_config.global_config.get_config_value( |
| 44 | 'SERVER', 'global_afe_hostname') |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 45 | DB_SERVER = global_config.global_config.get_config_value('AUTOTEST_WEB', 'host') |
| 46 | USER = global_config.global_config.get_config_value('AUTOTEST_WEB', 'user') |
| 47 | PASSWD = global_config.global_config.get_config_value( |
| 48 | 'AUTOTEST_WEB', 'password') |
| 49 | DATABASE = global_config.global_config.get_config_value( |
| 50 | 'AUTOTEST_WEB', 'database') |
Xixuan Wu | 211ae69 | 2017-12-18 15:32:42 -0800 | [diff] [blame] | 51 | RESPECT_STATIC_LABELS = global_config.global_config.get_config_value( |
| 52 | 'SKYLAB', 'respect_static_labels', type=bool, default=False) |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 53 | |
Prathmesh Prabhu | 2f02ab8 | 2018-01-11 11:15:54 -0800 | [diff] [blame] | 54 | # Per-prefix metrics are generated only for the following prefixes. This |
| 55 | # whitelist is a second level defence against populating the 'label_prefix' |
| 56 | # field with arbitrary values provided on the commandline. |
| 57 | _LABEL_PREFIX_METRICS_WHITELIST = ( |
| 58 | 'cros-version', |
| 59 | 'fwro-version', |
| 60 | 'fwrw-version', |
| 61 | 'pool', |
| 62 | ) |
| 63 | |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 64 | SELECT_USED_LABELS_FORMAT = """ |
| 65 | SELECT DISTINCT(label_id) FROM afe_autotests_dependency_labels UNION |
| 66 | SELECT DISTINCT(label_id) FROM afe_hosts_labels UNION |
| 67 | SELECT DISTINCT(label_id) FROM afe_jobs_dependency_labels UNION |
| 68 | SELECT DISTINCT(label_id) FROM afe_shards_labels UNION |
| 69 | SELECT DISTINCT(label_id) FROM afe_parameterized_jobs UNION |
| 70 | SELECT DISTINCT(meta_host) FROM afe_host_queue_entries |
| 71 | """ |
| 72 | |
Xixuan Wu | 211ae69 | 2017-12-18 15:32:42 -0800 | [diff] [blame] | 73 | SELECT_REPLACED_LABELS = """ |
| 74 | SELECT label_id FROM afe_replaced_labels |
| 75 | """ |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 76 | |
| 77 | DELETE_LABELS_FORMAT = """ |
| 78 | DELETE FROM afe_labels WHERE id in (%s) |
| 79 | """ |
| 80 | |
| 81 | |
| 82 | def get_used_labels(conn): |
| 83 | """Get labels that are currently in use. |
| 84 | |
| 85 | @param conn: MySQLdb Connection object. |
| 86 | |
| 87 | @return: A list of label ids. |
| 88 | """ |
| 89 | cursor = conn.cursor() |
| 90 | sql = SELECT_USED_LABELS_FORMAT |
Prathmesh Prabhu | 8268449 | 2017-08-07 16:28:37 -0700 | [diff] [blame] | 91 | logging.debug('Running: %r', sql) |
| 92 | cursor.execute(sql) |
| 93 | rows = cursor.fetchall() |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 94 | return set(r[0] for r in rows) |
| 95 | |
| 96 | |
Prathmesh Prabhu | 2f02ab8 | 2018-01-11 11:15:54 -0800 | [diff] [blame] | 97 | def fetch_labels(conn, label=None, prefix=False): |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 98 | """Fetch labels from database. |
| 99 | |
| 100 | @param conn: MySQLdb Connection object. |
Prathmesh Prabhu | 2f02ab8 | 2018-01-11 11:15:54 -0800 | [diff] [blame] | 101 | @param label: (optional) Label name to fetch. |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 102 | @param prefix: If True, use `label` as a prefix. Otherwise, fetch |
| 103 | labels whose name is exactly same as `label`. |
| 104 | |
| 105 | @return: A list of label ids. |
| 106 | """ |
| 107 | cursor = conn.cursor() |
Prathmesh Prabhu | 2f02ab8 | 2018-01-11 11:15:54 -0800 | [diff] [blame] | 108 | if label is not None: |
| 109 | if prefix: |
| 110 | sql = 'SELECT id FROM afe_labels WHERE name LIKE "%s%%"' % label |
| 111 | else: |
| 112 | sql = 'SELECT id FROM afe_labels WHERE name = "%s"' % label |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 113 | else: |
Prathmesh Prabhu | 2f02ab8 | 2018-01-11 11:15:54 -0800 | [diff] [blame] | 114 | sql = 'SELECT id FROM afe_labels' |
Prathmesh Prabhu | 8268449 | 2017-08-07 16:28:37 -0700 | [diff] [blame] | 115 | logging.debug('Running: %r', sql) |
| 116 | cursor.execute(sql) |
| 117 | rows = cursor.fetchall() |
Xixuan Wu | 211ae69 | 2017-12-18 15:32:42 -0800 | [diff] [blame] | 118 | # Don't delete labels whose replaced_by_static_label=True, since they're |
| 119 | # actually maintained by afe_static_labels, not afe_labels. |
| 120 | if not RESPECT_STATIC_LABELS: |
| 121 | return set(r[0] for r in rows) |
| 122 | else: |
| 123 | cursor.execute(SELECT_REPLACED_LABELS) |
| 124 | replaced_labels = cursor.fetchall() |
| 125 | replaced_label_ids = set([r[0] for r in replaced_labels]) |
| 126 | return set(r[0] for r in rows) - replaced_label_ids |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 127 | |
| 128 | |
Prathmesh Prabhu | 2f02ab8 | 2018-01-11 11:15:54 -0800 | [diff] [blame] | 129 | def _delete_labels(conn, labels, dry_run): |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 130 | """Helper function of `delete_labels`.""" |
| 131 | labels_str = ','.join([str(l) for l in labels]) |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 132 | sql = DELETE_LABELS_FORMAT % labels_str |
Prathmesh Prabhu | 2f02ab8 | 2018-01-11 11:15:54 -0800 | [diff] [blame] | 133 | if dry_run: |
| 134 | logging.info('[DRY RUN] Would have run: %r', sql) |
| 135 | else: |
| 136 | logging.debug('Running: %r', sql) |
| 137 | conn.cursor().execute(sql) |
| 138 | conn.commit() |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 139 | |
| 140 | |
Prathmesh Prabhu | 2f02ab8 | 2018-01-11 11:15:54 -0800 | [diff] [blame] | 141 | def delete_labels(conn, labels, max_delete, dry_run=False): |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 142 | """Delete given labels from database. |
| 143 | |
| 144 | @param conn: MySQLdb Connection object. |
Prathmesh Prabhu | 8268449 | 2017-08-07 16:28:37 -0700 | [diff] [blame] | 145 | @param labels: iterable of labels to delete. |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 146 | @param max_delete: Max number of records to delete in a query. |
Prathmesh Prabhu | 2f02ab8 | 2018-01-11 11:15:54 -0800 | [diff] [blame] | 147 | @param dry_run: (Boolean) Whether this is a dry run. |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 148 | """ |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 149 | while labels: |
Prathmesh Prabhu | 8268449 | 2017-08-07 16:28:37 -0700 | [diff] [blame] | 150 | chunk = labels[:max_delete] |
| 151 | labels = labels[max_delete:] |
Prathmesh Prabhu | 2f02ab8 | 2018-01-11 11:15:54 -0800 | [diff] [blame] | 152 | _delete_labels(conn, chunk, dry_run) |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 153 | |
| 154 | |
MK Ryu | a34e3b1 | 2015-08-21 16:20:47 -0700 | [diff] [blame] | 155 | def is_primary_server(): |
| 156 | """Check if this server's status is primary |
| 157 | |
| 158 | @return: True if primary, False otherwise. |
| 159 | """ |
MK Ryu | 37325b4 | 2015-08-25 15:41:39 -0700 | [diff] [blame] | 160 | server = frontend.AFE(server=GLOBAL_AFE).run( |
| 161 | 'get_servers', hostname=socket.getfqdn()) |
| 162 | if server and server[0]['status'] == 'primary': |
MK Ryu | a34e3b1 | 2015-08-21 16:20:47 -0700 | [diff] [blame] | 163 | return True |
| 164 | return False |
| 165 | |
| 166 | |
Prathmesh Prabhu | 8268449 | 2017-08-07 16:28:37 -0700 | [diff] [blame] | 167 | def clean_labels(options): |
| 168 | """Cleans unused labels from AFE database""" |
| 169 | msg = 'Label cleaner starts. Will delete ' |
| 170 | if options.prefix: |
| 171 | msg += 'all labels whose prefix is "%s".' |
| 172 | else: |
| 173 | msg += 'a label "%s".' |
| 174 | logging.info(msg, options.label) |
| 175 | logging.info('Target database: %s.', options.db_server) |
| 176 | if options.check_status and not is_primary_server(): |
| 177 | raise Exception('Cannot run in a non-primary server') |
| 178 | |
Prathmesh Prabhu | 2f02ab8 | 2018-01-11 11:15:54 -0800 | [diff] [blame] | 179 | conn = MySQLdb.connect( |
| 180 | host=options.db_server, |
| 181 | user=options.db_user, |
| 182 | passwd=options.db_password, |
| 183 | db=DATABASE, |
| 184 | ) |
| 185 | |
| 186 | all_labels = fetch_labels(conn) |
| 187 | logging.info('Found total %d labels', len(all_labels)) |
| 188 | metrics.Gauge(_METRICS_PREFIX + '/total_labels_count').set( |
| 189 | len(all_labels), |
| 190 | fields={ |
| 191 | 'target_db': options.db_server, |
| 192 | 'label_prefix': '', |
| 193 | }, |
| 194 | ) |
Prathmesh Prabhu | 8268449 | 2017-08-07 16:28:37 -0700 | [diff] [blame] | 195 | |
| 196 | labels = fetch_labels(conn, options.label, options.prefix) |
Prathmesh Prabhu | 2f02ab8 | 2018-01-11 11:15:54 -0800 | [diff] [blame] | 197 | logging.info('Found total %d labels matching %s', len(labels), |
| 198 | options.label) |
| 199 | if options.prefix and options.label in _LABEL_PREFIX_METRICS_WHITELIST: |
| 200 | metrics.Gauge(_METRICS_PREFIX + '/total_labels_count').set( |
| 201 | len(labels), |
| 202 | fields={ |
| 203 | 'target_db': options.db_server, |
| 204 | 'label_prefix': options.label, |
| 205 | }, |
| 206 | ) |
Prathmesh Prabhu | 8268449 | 2017-08-07 16:28:37 -0700 | [diff] [blame] | 207 | |
| 208 | used_labels = get_used_labels(conn) |
| 209 | logging.info('Found %d labels are used', len(used_labels)) |
| 210 | metrics.Gauge(_METRICS_PREFIX + '/used_labels_count').set( |
| 211 | len(used_labels), fields={'target_db': options.db_server}) |
| 212 | |
| 213 | to_delete = list(labels - used_labels) |
| 214 | logging.info('Deleting %d unused labels', len(to_delete)) |
Prathmesh Prabhu | 2f02ab8 | 2018-01-11 11:15:54 -0800 | [diff] [blame] | 215 | delete_labels(conn, to_delete, options.max_delete, options.dry_run) |
Prathmesh Prabhu | 8268449 | 2017-08-07 16:28:37 -0700 | [diff] [blame] | 216 | metrics.Counter(_METRICS_PREFIX + '/labels_deleted').increment_by( |
| 217 | len(to_delete), fields={'target_db': options.db_server}) |
| 218 | |
| 219 | |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 220 | def main(): |
Prathmesh Prabhu | 8268449 | 2017-08-07 16:28:37 -0700 | [diff] [blame] | 221 | """Cleans unused labels from AFE database""" |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 222 | parser = argparse.ArgumentParser( |
| 223 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
Prathmesh Prabhu | 2f02ab8 | 2018-01-11 11:15:54 -0800 | [diff] [blame] | 224 | parser.add_argument( |
| 225 | '--db', |
| 226 | dest='db_server', |
| 227 | help='Database server', |
| 228 | default=DB_SERVER, |
| 229 | ) |
| 230 | parser.add_argument( |
| 231 | '--db-user', |
| 232 | dest='db_user', |
| 233 | help='Database user', |
| 234 | default=USER, |
| 235 | ) |
| 236 | parser.add_argument( |
| 237 | '--db-password', |
| 238 | dest='db_password', |
| 239 | help='Database password', |
| 240 | default=PASSWD, |
| 241 | ) |
| 242 | parser.add_argument( |
| 243 | '-p', |
| 244 | dest='prefix', |
| 245 | action='store_true', |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 246 | help=('Use argument <label> as a prefix for matching. ' |
| 247 | 'For example, when the argument <label> is "cros-version" ' |
| 248 | 'and this option is enabled, then labels whose name ' |
| 249 | 'beginning with "cros-version" are matched. When this ' |
| 250 | 'option is disabled, we match labels whose name is ' |
Prathmesh Prabhu | 2f02ab8 | 2018-01-11 11:15:54 -0800 | [diff] [blame] | 251 | 'exactly same as the argument <label>.'), |
| 252 | ) |
| 253 | parser.add_argument( |
| 254 | '-n', |
| 255 | dest='max_delete', |
| 256 | type=int, |
| 257 | help='Max number of records to delete in each query.', |
| 258 | default=100, |
| 259 | ) |
| 260 | parser.add_argument( |
| 261 | '-s', |
| 262 | dest='check_status', |
| 263 | action='store_true', |
| 264 | help='Enforce to run only in a server that has primary status', |
| 265 | ) |
| 266 | parser.add_argument( |
| 267 | '--dry-run', |
| 268 | dest='dry_run', |
| 269 | action='store_true', |
| 270 | help='Dry run mode. Do not actually delete any labels.', |
| 271 | ) |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 272 | parser.add_argument('label', help='Label name to delete') |
| 273 | options = parser.parse_args() |
| 274 | |
| 275 | logging_config.LoggingConfig().configure_logging( |
Prathmesh Prabhu | 8268449 | 2017-08-07 16:28:37 -0700 | [diff] [blame] | 276 | datefmt='%Y-%m-%d %H:%M:%S', |
| 277 | verbose=True) |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 278 | |
Prathmesh Prabhu | 2f02ab8 | 2018-01-11 11:15:54 -0800 | [diff] [blame] | 279 | if options.dry_run: |
| 280 | tfd, metrics_file=tempfile.mkstemp() |
| 281 | os.close(tfd) |
| 282 | ts_mon_context = ts_mon_config.SetupTsMonGlobalState( |
| 283 | 'afe_label_cleaner', |
| 284 | auto_flush=False, |
| 285 | debug_file=metrics_file, |
| 286 | ) |
| 287 | else: |
| 288 | ts_mon_context = ts_mon_config.SetupTsMonGlobalState( |
| 289 | 'afe_label_cleaner', |
| 290 | auto_flush=False, |
| 291 | ) |
| 292 | with ts_mon_context: |
Prathmesh Prabhu | 8268449 | 2017-08-07 16:28:37 -0700 | [diff] [blame] | 293 | try: |
| 294 | clean_labels(options) |
| 295 | except: |
| 296 | metrics.Counter(_METRICS_PREFIX + '/tick').increment( |
| 297 | fields={'target_db': options.db_server, |
| 298 | 'success': False}) |
| 299 | raise |
MK Ryu | 105e1c2 | 2015-09-01 15:58:43 -0700 | [diff] [blame] | 300 | else: |
Prathmesh Prabhu | 8268449 | 2017-08-07 16:28:37 -0700 | [diff] [blame] | 301 | metrics.Counter(_METRICS_PREFIX + '/tick').increment( |
| 302 | fields={'target_db': options.db_server, |
| 303 | 'success': True}) |
| 304 | finally: |
| 305 | metrics.Flush() |
Prathmesh Prabhu | 2f02ab8 | 2018-01-11 11:15:54 -0800 | [diff] [blame] | 306 | if options.dry_run: |
| 307 | logging.info('Dumped ts_mon metrics to %s', metrics_file) |
MK Ryu | 4055704 | 2015-08-18 10:59:47 -0700 | [diff] [blame] | 308 | |
| 309 | |
| 310 | if __name__ == '__main__': |
| 311 | sys.exit(main()) |