1*9c5db199SXin Li# Lint as: python2, python3 2*9c5db199SXin Li# Copyright (c) 2014 The Chromium OS Authors. All rights reserved. 3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 4*9c5db199SXin Li# found in the LICENSE file. 5*9c5db199SXin Li 6*9c5db199SXin Lifrom __future__ import division 7*9c5db199SXin Lifrom __future__ import print_function 8*9c5db199SXin Li 9*9c5db199SXin Litry: 10*9c5db199SXin Li import MySQLdb as driver 11*9c5db199SXin Liexcept ImportError: 12*9c5db199SXin Li # This module (tko) is unconditionally imported by autoserv, 13*9c5db199SXin Li # even in environments where MyQSLdb is unavailable. Thus, we 14*9c5db199SXin Li # need to cope with import failure here. 15*9c5db199SXin Li # See https://bugs.chromium.org/p/chromium/issues/detail?id=860166#c17 for 16*9c5db199SXin Li # context. 17*9c5db199SXin Li class UtterlyFakeDb(object): 18*9c5db199SXin Li """Lame fake of MySQLdb for import time needs of this file.""" 19*9c5db199SXin Li OperationalError = object() 20*9c5db199SXin Li 21*9c5db199SXin Li driver = UtterlyFakeDb 22*9c5db199SXin Li 23*9c5db199SXin Liimport math 24*9c5db199SXin Liimport os 25*9c5db199SXin Liimport random 26*9c5db199SXin Liimport re 27*9c5db199SXin Liimport sys 28*9c5db199SXin Liimport time 29*9c5db199SXin Li 30*9c5db199SXin Liimport common 31*9c5db199SXin Lifrom autotest_lib.client.common_lib import global_config 32*9c5db199SXin Lifrom autotest_lib.client.common_lib import utils 33*9c5db199SXin Lifrom autotest_lib.client.common_lib.cros import retry 34*9c5db199SXin Lifrom autotest_lib.frontend import database_settings_helper 35*9c5db199SXin Liimport six 36*9c5db199SXin Li 37*9c5db199SXin Litry: 38*9c5db199SXin Li from autotest_lib.utils.frozen_chromite.lib import metrics 39*9c5db199SXin Liexcept ImportError: 40*9c5db199SXin Li metrics = utils.metrics_mock 41*9c5db199SXin Li 42*9c5db199SXin Li 43*9c5db199SXin Lidef _log_error(msg): 44*9c5db199SXin Li """Log an error message. 45*9c5db199SXin Li 46*9c5db199SXin Li @param msg: Message string 47*9c5db199SXin Li """ 48*9c5db199SXin Li print(msg, file=sys.stderr) 49*9c5db199SXin Li sys.stderr.flush() # we want these msgs to show up immediately 50*9c5db199SXin Li 51*9c5db199SXin Li 52*9c5db199SXin Lidef _format_operational_error(e): 53*9c5db199SXin Li """Format OperationalError. 54*9c5db199SXin Li 55*9c5db199SXin Li @param e: OperationalError instance. 56*9c5db199SXin Li """ 57*9c5db199SXin Li return ("%s: An operational error occurred during a database " 58*9c5db199SXin Li "operation: %s" % (time.strftime("%X %x"), str(e))) 59*9c5db199SXin Li 60*9c5db199SXin Li 61*9c5db199SXin Liclass MySQLTooManyRows(Exception): 62*9c5db199SXin Li """Too many records.""" 63*9c5db199SXin Li pass 64*9c5db199SXin Li 65*9c5db199SXin Li 66*9c5db199SXin Lidef _connection_retry_callback(): 67*9c5db199SXin Li """Callback method used to increment a retry metric.""" 68*9c5db199SXin Li metrics.Counter('chromeos/autotest/tko/connection_retries').increment() 69*9c5db199SXin Li 70*9c5db199SXin Li 71*9c5db199SXin Liclass db_sql(object): 72*9c5db199SXin Li """Data access.""" 73*9c5db199SXin Li 74*9c5db199SXin Li def __init__(self, debug=False, autocommit=True, host=None, 75*9c5db199SXin Li database=None, user=None, password=None): 76*9c5db199SXin Li self.debug = debug 77*9c5db199SXin Li self.autocommit = autocommit 78*9c5db199SXin Li self._load_config(host, database, user, password) 79*9c5db199SXin Li 80*9c5db199SXin Li self.con = None 81*9c5db199SXin Li self._init_db() 82*9c5db199SXin Li 83*9c5db199SXin Li # if not present, insert statuses 84*9c5db199SXin Li self.status_idx = {} 85*9c5db199SXin Li self.status_word = {} 86*9c5db199SXin Li status_rows = self.select('status_idx, word', 'tko_status', None) 87*9c5db199SXin Li for s in status_rows: 88*9c5db199SXin Li self.status_idx[s[1]] = s[0] 89*9c5db199SXin Li self.status_word[s[0]] = s[1] 90*9c5db199SXin Li 91*9c5db199SXin Li machine_map = os.path.join(os.path.dirname(__file__), 92*9c5db199SXin Li 'machines') 93*9c5db199SXin Li if os.path.exists(machine_map): 94*9c5db199SXin Li self.machine_map = machine_map 95*9c5db199SXin Li else: 96*9c5db199SXin Li self.machine_map = None 97*9c5db199SXin Li self.machine_group = {} 98*9c5db199SXin Li 99*9c5db199SXin Li 100*9c5db199SXin Li def _load_config(self, host, database, user, password): 101*9c5db199SXin Li """Loads configuration settings required to connect to the database. 102*9c5db199SXin Li 103*9c5db199SXin Li This will try to connect to use the settings prefixed with global_db_. 104*9c5db199SXin Li If they do not exist, they un-prefixed settings will be used. 105*9c5db199SXin Li 106*9c5db199SXin Li If parameters are supplied, these will be taken instead of the values 107*9c5db199SXin Li in global_config. 108*9c5db199SXin Li 109*9c5db199SXin Li The setting of 'host' can be a real host, or a unix socket if it starts 110*9c5db199SXin Li with '/'. 111*9c5db199SXin Li 112*9c5db199SXin Li @param host: If set, this host will be used, if not, the host will be 113*9c5db199SXin Li retrieved from global_config. 114*9c5db199SXin Li @param database: If set, this database will be used, if not, the 115*9c5db199SXin Li database will be retrieved from global_config. 116*9c5db199SXin Li @param user: If set, this user will be used, if not, the 117*9c5db199SXin Li user will be retrieved from global_config. 118*9c5db199SXin Li @param password: If set, this password will be used, if not, the 119*9c5db199SXin Li password will be retrieved from global_config. 120*9c5db199SXin Li """ 121*9c5db199SXin Li database_settings = database_settings_helper.get_global_db_config() 122*9c5db199SXin Li 123*9c5db199SXin Li # grab the host, database 124*9c5db199SXin Li self.host = host or database_settings['HOST'] 125*9c5db199SXin Li self.database = database or database_settings['NAME'] 126*9c5db199SXin Li 127*9c5db199SXin Li # grab the user and password 128*9c5db199SXin Li self.user = user or database_settings['USER'] 129*9c5db199SXin Li self.password = password or database_settings['PASSWORD'] 130*9c5db199SXin Li 131*9c5db199SXin Li # grab the timeout configuration 132*9c5db199SXin Li self.query_timeout =( 133*9c5db199SXin Li database_settings.get('OPTIONS', {}).get('timeout', 3600)) 134*9c5db199SXin Li 135*9c5db199SXin Li # Using fallback to non-global in order to work without configuration 136*9c5db199SXin Li # overhead on non-shard instances. 137*9c5db199SXin Li get_value = global_config.global_config.get_config_value_with_fallback 138*9c5db199SXin Li self.min_delay = get_value("AUTOTEST_WEB", "global_db_min_retry_delay", 139*9c5db199SXin Li "min_retry_delay", type=int, default=20) 140*9c5db199SXin Li self.max_delay = get_value("AUTOTEST_WEB", "global_db_max_retry_delay", 141*9c5db199SXin Li "max_retry_delay", type=int, default=60) 142*9c5db199SXin Li 143*9c5db199SXin Li # TODO(beeps): Move this to django settings once we have routers. 144*9c5db199SXin Li # On test instances mysql connects through a different port. No point 145*9c5db199SXin Li # piping this through our entire infrastructure when it is only really 146*9c5db199SXin Li # used for testing; Ideally we would specify this through django 147*9c5db199SXin Li # settings and default it to the empty string so django will figure out 148*9c5db199SXin Li # the default based on the database backend (eg: mysql, 3306), but until 149*9c5db199SXin Li # we have database routers in place any django settings will apply to 150*9c5db199SXin Li # both tko and afe. 151*9c5db199SXin Li # The intended use of this port is to allow a testing shard vm to 152*9c5db199SXin Li # update the vm's database with test results. Specifying 153*9c5db199SXin Li # and empty string will fallback to not even specifying the port 154*9c5db199SXin Li # to the backend in tko/db.py. Unfortunately this means retries 155*9c5db199SXin Li # won't work on the test cluster till we've migrated to routers. 156*9c5db199SXin Li self.port = global_config.global_config.get_config_value( 157*9c5db199SXin Li "AUTOTEST_WEB", "global_db_port", type=str, default='') 158*9c5db199SXin Li 159*9c5db199SXin Li 160*9c5db199SXin Li def _init_db(self): 161*9c5db199SXin Li # make sure we clean up any existing connection 162*9c5db199SXin Li if self.con: 163*9c5db199SXin Li self.con.close() 164*9c5db199SXin Li self.con = None 165*9c5db199SXin Li 166*9c5db199SXin Li # create the db connection and cursor 167*9c5db199SXin Li self.con = self.connect(self.host, self.database, 168*9c5db199SXin Li self.user, self.password, self.port) 169*9c5db199SXin Li self.cur = self.con.cursor() 170*9c5db199SXin Li 171*9c5db199SXin Li 172*9c5db199SXin Li def _random_delay(self): 173*9c5db199SXin Li delay = random.randint(self.min_delay, self.max_delay) 174*9c5db199SXin Li time.sleep(delay) 175*9c5db199SXin Li 176*9c5db199SXin Li 177*9c5db199SXin Li @retry.retry(driver.OperationalError, timeout_min=10, 178*9c5db199SXin Li delay_sec=5, callback=_connection_retry_callback) 179*9c5db199SXin Li def connect(self, host, database, user, password, port): 180*9c5db199SXin Li """Open and return a connection to mysql database.""" 181*9c5db199SXin Li connection_args = { 182*9c5db199SXin Li 'db': database, 183*9c5db199SXin Li 'user': user, 184*9c5db199SXin Li 'passwd': password, 185*9c5db199SXin Li 'connect_timeout': 20, 186*9c5db199SXin Li } 187*9c5db199SXin Li if port: 188*9c5db199SXin Li connection_args['port'] = int(port) 189*9c5db199SXin Li 190*9c5db199SXin Li if host.startswith('/'): 191*9c5db199SXin Li return driver.connect(unix_socket=host, **connection_args) 192*9c5db199SXin Li 193*9c5db199SXin Li return driver.connect(host=host, **connection_args) 194*9c5db199SXin Li 195*9c5db199SXin Li 196*9c5db199SXin Li def run_with_retry(self, function, *args, **dargs): 197*9c5db199SXin Li """Call function(*args, **dargs) until either it passes 198*9c5db199SXin Li without an operational error, or a timeout is reached. 199*9c5db199SXin Li This will re-connect to the database, so it is NOT safe 200*9c5db199SXin Li to use this inside of a database transaction. 201*9c5db199SXin Li 202*9c5db199SXin Li It can be safely used with transactions, but the 203*9c5db199SXin Li transaction start & end must be completely contained 204*9c5db199SXin Li within the call to 'function'. 205*9c5db199SXin Li 206*9c5db199SXin Li @param function: The function to run with retry. 207*9c5db199SXin Li @param args: The arguments 208*9c5db199SXin Li @param dargs: The named arguments. 209*9c5db199SXin Li """ 210*9c5db199SXin Li success = False 211*9c5db199SXin Li start_time = time.time() 212*9c5db199SXin Li while not success: 213*9c5db199SXin Li try: 214*9c5db199SXin Li result = function(*args, **dargs) 215*9c5db199SXin Li except driver.OperationalError as e: 216*9c5db199SXin Li _log_error("%s; retrying, don't panic yet" 217*9c5db199SXin Li % _format_operational_error(e)) 218*9c5db199SXin Li stop_time = time.time() 219*9c5db199SXin Li elapsed_time = stop_time - start_time 220*9c5db199SXin Li if elapsed_time > self.query_timeout: 221*9c5db199SXin Li raise 222*9c5db199SXin Li else: 223*9c5db199SXin Li try: 224*9c5db199SXin Li self._random_delay() 225*9c5db199SXin Li self._init_db() 226*9c5db199SXin Li except driver.OperationalError as e: 227*9c5db199SXin Li _log_error('%s; panic now' 228*9c5db199SXin Li % _format_operational_error(e)) 229*9c5db199SXin Li else: 230*9c5db199SXin Li success = True 231*9c5db199SXin Li return result 232*9c5db199SXin Li 233*9c5db199SXin Li 234*9c5db199SXin Li def dprint(self, value): 235*9c5db199SXin Li """Print out debug value. 236*9c5db199SXin Li 237*9c5db199SXin Li @param value: The value to print out. 238*9c5db199SXin Li """ 239*9c5db199SXin Li if self.debug: 240*9c5db199SXin Li sys.stdout.write('SQL: ' + str(value) + '\n') 241*9c5db199SXin Li 242*9c5db199SXin Li 243*9c5db199SXin Li def _commit(self): 244*9c5db199SXin Li """Private method for function commit to call for retry. 245*9c5db199SXin Li """ 246*9c5db199SXin Li return self.con.commit() 247*9c5db199SXin Li 248*9c5db199SXin Li 249*9c5db199SXin Li def commit(self): 250*9c5db199SXin Li """Commit the sql transaction.""" 251*9c5db199SXin Li if self.autocommit: 252*9c5db199SXin Li return self.run_with_retry(self._commit) 253*9c5db199SXin Li else: 254*9c5db199SXin Li return self._commit() 255*9c5db199SXin Li 256*9c5db199SXin Li 257*9c5db199SXin Li def rollback(self): 258*9c5db199SXin Li """Rollback the sql transaction.""" 259*9c5db199SXin Li self.con.rollback() 260*9c5db199SXin Li 261*9c5db199SXin Li 262*9c5db199SXin Li def get_last_autonumber_value(self): 263*9c5db199SXin Li """Gets the last auto number. 264*9c5db199SXin Li 265*9c5db199SXin Li @return: The last auto number. 266*9c5db199SXin Li """ 267*9c5db199SXin Li self.cur.execute('SELECT LAST_INSERT_ID()', []) 268*9c5db199SXin Li return self.cur.fetchall()[0][0] 269*9c5db199SXin Li 270*9c5db199SXin Li 271*9c5db199SXin Li def _quote(self, field): 272*9c5db199SXin Li return '`%s`' % field 273*9c5db199SXin Li 274*9c5db199SXin Li 275*9c5db199SXin Li def _where_clause(self, where): 276*9c5db199SXin Li if not where: 277*9c5db199SXin Li return '', [] 278*9c5db199SXin Li 279*9c5db199SXin Li if isinstance(where, dict): 280*9c5db199SXin Li # key/value pairs (which should be equal, or None for null) 281*9c5db199SXin Li keys, values = [], [] 282*9c5db199SXin Li for field, value in six.iteritems(where): 283*9c5db199SXin Li quoted_field = self._quote(field) 284*9c5db199SXin Li if value is None: 285*9c5db199SXin Li keys.append(quoted_field + ' is null') 286*9c5db199SXin Li else: 287*9c5db199SXin Li keys.append(quoted_field + '=%s') 288*9c5db199SXin Li values.append(value) 289*9c5db199SXin Li where_clause = ' and '.join(keys) 290*9c5db199SXin Li elif isinstance(where, six.string_types): 291*9c5db199SXin Li # the exact string 292*9c5db199SXin Li where_clause = where 293*9c5db199SXin Li values = [] 294*9c5db199SXin Li elif isinstance(where, tuple): 295*9c5db199SXin Li # preformatted where clause + values 296*9c5db199SXin Li where_clause, values = where 297*9c5db199SXin Li assert where_clause 298*9c5db199SXin Li else: 299*9c5db199SXin Li raise ValueError('Invalid "where" value: %r' % where) 300*9c5db199SXin Li 301*9c5db199SXin Li return ' WHERE ' + where_clause, values 302*9c5db199SXin Li 303*9c5db199SXin Li 304*9c5db199SXin Li 305*9c5db199SXin Li def select(self, fields, table, where, distinct=False, group_by=None, 306*9c5db199SXin Li max_rows=None): 307*9c5db199SXin Li """\ 308*9c5db199SXin Li This selects all the fields requested from a 309*9c5db199SXin Li specific table with a particular where clause. 310*9c5db199SXin Li The where clause can either be a dictionary of 311*9c5db199SXin Li field=value pairs, a string, or a tuple of (string, 312*9c5db199SXin Li a list of values). The last option is what you 313*9c5db199SXin Li should use when accepting user input as it'll 314*9c5db199SXin Li protect you against sql injection attacks (if 315*9c5db199SXin Li all user data is placed in the array rather than 316*9c5db199SXin Li the raw SQL). 317*9c5db199SXin Li 318*9c5db199SXin Li For example: 319*9c5db199SXin Li where = ("a = %s AND b = %s", ['val', 'val']) 320*9c5db199SXin Li is better than 321*9c5db199SXin Li where = "a = 'val' AND b = 'val'" 322*9c5db199SXin Li 323*9c5db199SXin Li @param fields: The list of selected fields string. 324*9c5db199SXin Li @param table: The name of the database table. 325*9c5db199SXin Li @param where: The where clause string. 326*9c5db199SXin Li @param distinct: If select distinct values. 327*9c5db199SXin Li @param group_by: Group by clause. 328*9c5db199SXin Li @param max_rows: unused. 329*9c5db199SXin Li """ 330*9c5db199SXin Li cmd = ['select'] 331*9c5db199SXin Li if distinct: 332*9c5db199SXin Li cmd.append('distinct') 333*9c5db199SXin Li cmd += [fields, 'from', table] 334*9c5db199SXin Li 335*9c5db199SXin Li where_clause, values = self._where_clause(where) 336*9c5db199SXin Li cmd.append(where_clause) 337*9c5db199SXin Li 338*9c5db199SXin Li if group_by: 339*9c5db199SXin Li cmd.append(' GROUP BY ' + group_by) 340*9c5db199SXin Li 341*9c5db199SXin Li self.dprint('%s %s' % (' '.join(cmd), values)) 342*9c5db199SXin Li 343*9c5db199SXin Li # create a re-runable function for executing the query 344*9c5db199SXin Li def exec_sql(): 345*9c5db199SXin Li """Exeuctes an the sql command.""" 346*9c5db199SXin Li sql = ' '.join(cmd) 347*9c5db199SXin Li numRec = self.cur.execute(sql, values) 348*9c5db199SXin Li if max_rows is not None and numRec > max_rows: 349*9c5db199SXin Li msg = 'Exceeded allowed number of records' 350*9c5db199SXin Li raise MySQLTooManyRows(msg) 351*9c5db199SXin Li return self.cur.fetchall() 352*9c5db199SXin Li 353*9c5db199SXin Li # run the query, re-trying after operational errors 354*9c5db199SXin Li if self.autocommit: 355*9c5db199SXin Li return self.run_with_retry(exec_sql) 356*9c5db199SXin Li else: 357*9c5db199SXin Li return exec_sql() 358*9c5db199SXin Li 359*9c5db199SXin Li 360*9c5db199SXin Li def select_sql(self, fields, table, sql, values): 361*9c5db199SXin Li """\ 362*9c5db199SXin Li select fields from table "sql" 363*9c5db199SXin Li 364*9c5db199SXin Li @param fields: The list of selected fields string. 365*9c5db199SXin Li @param table: The name of the database table. 366*9c5db199SXin Li @param sql: The sql string. 367*9c5db199SXin Li @param values: The sql string parameter values. 368*9c5db199SXin Li """ 369*9c5db199SXin Li cmd = 'select %s from %s %s' % (fields, table, sql) 370*9c5db199SXin Li self.dprint(cmd) 371*9c5db199SXin Li 372*9c5db199SXin Li # create a -re-runable function for executing the query 373*9c5db199SXin Li def _exec_sql(): 374*9c5db199SXin Li self.cur.execute(cmd, values) 375*9c5db199SXin Li return self.cur.fetchall() 376*9c5db199SXin Li 377*9c5db199SXin Li # run the query, re-trying after operational errors 378*9c5db199SXin Li if self.autocommit: 379*9c5db199SXin Li return self.run_with_retry(_exec_sql) 380*9c5db199SXin Li else: 381*9c5db199SXin Li return _exec_sql() 382*9c5db199SXin Li 383*9c5db199SXin Li 384*9c5db199SXin Li def _exec_sql_with_commit(self, sql, values, commit): 385*9c5db199SXin Li if self.autocommit: 386*9c5db199SXin Li # re-run the query until it succeeds 387*9c5db199SXin Li def _exec_sql(): 388*9c5db199SXin Li self.cur.execute(sql, values) 389*9c5db199SXin Li self.con.commit() 390*9c5db199SXin Li self.run_with_retry(_exec_sql) 391*9c5db199SXin Li else: 392*9c5db199SXin Li # take one shot at running the query 393*9c5db199SXin Li self.cur.execute(sql, values) 394*9c5db199SXin Li if commit: 395*9c5db199SXin Li self.con.commit() 396*9c5db199SXin Li 397*9c5db199SXin Li 398*9c5db199SXin Li def insert(self, table, data, commit=None): 399*9c5db199SXin Li """\ 400*9c5db199SXin Li 'insert into table (keys) values (%s ... %s)', values 401*9c5db199SXin Li 402*9c5db199SXin Li data: 403*9c5db199SXin Li dictionary of fields and data 404*9c5db199SXin Li 405*9c5db199SXin Li @param table: The name of the table. 406*9c5db199SXin Li @param data: The insert data. 407*9c5db199SXin Li @param commit: If commit the transaction . 408*9c5db199SXin Li """ 409*9c5db199SXin Li fields = list(data.keys()) 410*9c5db199SXin Li refs = ['%s' for field in fields] 411*9c5db199SXin Li values = [data[field] for field in fields] 412*9c5db199SXin Li cmd = ('insert into %s (%s) values (%s)' % 413*9c5db199SXin Li (table, ','.join(self._quote(field) for field in fields), 414*9c5db199SXin Li ','.join(refs))) 415*9c5db199SXin Li self.dprint('%s %s' % (cmd, values)) 416*9c5db199SXin Li 417*9c5db199SXin Li self._exec_sql_with_commit(cmd, values, commit) 418*9c5db199SXin Li 419*9c5db199SXin Li 420*9c5db199SXin Li def delete(self, table, where, commit = None): 421*9c5db199SXin Li """Delete entries. 422*9c5db199SXin Li 423*9c5db199SXin Li @param table: The name of the table. 424*9c5db199SXin Li @param where: The where clause. 425*9c5db199SXin Li @param commit: If commit the transaction . 426*9c5db199SXin Li """ 427*9c5db199SXin Li cmd = ['delete from', table] 428*9c5db199SXin Li if commit is None: 429*9c5db199SXin Li commit = self.autocommit 430*9c5db199SXin Li where_clause, values = self._where_clause(where) 431*9c5db199SXin Li cmd.append(where_clause) 432*9c5db199SXin Li sql = ' '.join(cmd) 433*9c5db199SXin Li self.dprint('%s %s' % (sql, values)) 434*9c5db199SXin Li 435*9c5db199SXin Li self._exec_sql_with_commit(sql, values, commit) 436*9c5db199SXin Li 437*9c5db199SXin Li 438*9c5db199SXin Li def update(self, table, data, where, commit = None): 439*9c5db199SXin Li """\ 440*9c5db199SXin Li 'update table set data values (%s ... %s) where ...' 441*9c5db199SXin Li 442*9c5db199SXin Li data: 443*9c5db199SXin Li dictionary of fields and data 444*9c5db199SXin Li 445*9c5db199SXin Li @param table: The name of the table. 446*9c5db199SXin Li @param data: The sql parameter values. 447*9c5db199SXin Li @param where: The where clause. 448*9c5db199SXin Li @param commit: If commit the transaction . 449*9c5db199SXin Li """ 450*9c5db199SXin Li if commit is None: 451*9c5db199SXin Li commit = self.autocommit 452*9c5db199SXin Li cmd = 'update %s ' % table 453*9c5db199SXin Li fields = list(data.keys()) 454*9c5db199SXin Li data_refs = [self._quote(field) + '=%s' for field in fields] 455*9c5db199SXin Li data_values = [data[field] for field in fields] 456*9c5db199SXin Li cmd += ' set ' + ', '.join(data_refs) 457*9c5db199SXin Li 458*9c5db199SXin Li where_clause, where_values = self._where_clause(where) 459*9c5db199SXin Li cmd += where_clause 460*9c5db199SXin Li 461*9c5db199SXin Li values = data_values + where_values 462*9c5db199SXin Li self.dprint('%s %s' % (cmd, values)) 463*9c5db199SXin Li 464*9c5db199SXin Li self._exec_sql_with_commit(cmd, values, commit) 465*9c5db199SXin Li 466*9c5db199SXin Li 467*9c5db199SXin Li def delete_job(self, tag, commit = None): 468*9c5db199SXin Li """Delete a tko job. 469*9c5db199SXin Li 470*9c5db199SXin Li @param tag: The job tag. 471*9c5db199SXin Li @param commit: If commit the transaction . 472*9c5db199SXin Li """ 473*9c5db199SXin Li job_idx = self.find_job(tag) 474*9c5db199SXin Li for test_idx in self.find_tests(job_idx): 475*9c5db199SXin Li where = {'test_idx' : test_idx} 476*9c5db199SXin Li self.delete('tko_iteration_result', where) 477*9c5db199SXin Li self.delete('tko_iteration_perf_value', where) 478*9c5db199SXin Li self.delete('tko_iteration_attributes', where) 479*9c5db199SXin Li self.delete('tko_test_attributes', where) 480*9c5db199SXin Li self.delete('tko_test_labels_tests', {'test_id': test_idx}) 481*9c5db199SXin Li where = {'job_idx' : job_idx} 482*9c5db199SXin Li self.delete('tko_tests', where) 483*9c5db199SXin Li self.delete('tko_jobs', where) 484*9c5db199SXin Li 485*9c5db199SXin Li 486*9c5db199SXin Li def insert_job(self, tag, job, commit=None): 487*9c5db199SXin Li """Insert a tko job. 488*9c5db199SXin Li 489*9c5db199SXin Li @param tag: The job tag. 490*9c5db199SXin Li @param job: The job object. 491*9c5db199SXin Li @param commit: If commit the transaction . 492*9c5db199SXin Li """ 493*9c5db199SXin Li data = self._get_common_job_data(tag, job) 494*9c5db199SXin Li data.update({ 495*9c5db199SXin Li 'afe_job_id': job.afe_job_id, 496*9c5db199SXin Li 'afe_parent_job_id': job.afe_parent_job_id, 497*9c5db199SXin Li }) 498*9c5db199SXin Li if job.job_idx is not None: 499*9c5db199SXin Li self.update( 500*9c5db199SXin Li 'tko_jobs', data, {'job_idx': job.job_idx}, commit=commit) 501*9c5db199SXin Li else: 502*9c5db199SXin Li self.insert('tko_jobs', data, commit=commit) 503*9c5db199SXin Li job.job_idx = self.get_last_autonumber_value() 504*9c5db199SXin Li 505*9c5db199SXin Li 506*9c5db199SXin Li def _get_common_job_data(self, tag, job): 507*9c5db199SXin Li """Construct a dictionary with the common data to insert in job/task.""" 508*9c5db199SXin Li return { 509*9c5db199SXin Li 'tag':tag, 510*9c5db199SXin Li 'label': job.label, 511*9c5db199SXin Li 'username': job.user, 512*9c5db199SXin Li 'machine_idx': job.machine_idx, 513*9c5db199SXin Li 'queued_time': job.queued_time, 514*9c5db199SXin Li 'started_time': job.started_time, 515*9c5db199SXin Li 'finished_time': job.finished_time, 516*9c5db199SXin Li 'build': job.build, 517*9c5db199SXin Li 'build_version': job.build_version, 518*9c5db199SXin Li 'board': job.board, 519*9c5db199SXin Li 'suite': job.suite, 520*9c5db199SXin Li } 521*9c5db199SXin Li 522*9c5db199SXin Li 523*9c5db199SXin Li def insert_or_update_task_reference(self, job, reference_type, commit=None): 524*9c5db199SXin Li """Insert an entry in the tko_task_references table. 525*9c5db199SXin Li 526*9c5db199SXin Li The job should already have been inserted in tko_jobs. 527*9c5db199SXin Li @param job: tko.models.job object. 528*9c5db199SXin Li @param reference_type: The type of reference to insert. 529*9c5db199SXin Li One of: {'afe', 'skylab'} 530*9c5db199SXin Li @param commit: Whether to commit this transaction. 531*9c5db199SXin Li """ 532*9c5db199SXin Li assert reference_type in {'afe', 'skylab'} 533*9c5db199SXin Li if reference_type == 'afe': 534*9c5db199SXin Li task_id = job.afe_job_id 535*9c5db199SXin Li parent_task_id = job.afe_parent_job_id 536*9c5db199SXin Li else: 537*9c5db199SXin Li task_id = job.skylab_task_id 538*9c5db199SXin Li parent_task_id = job.skylab_parent_task_id 539*9c5db199SXin Li data = { 540*9c5db199SXin Li 'reference_type': reference_type, 541*9c5db199SXin Li 'tko_job_idx': job.job_idx, 542*9c5db199SXin Li 'task_id': task_id, 543*9c5db199SXin Li 'parent_task_id': parent_task_id, 544*9c5db199SXin Li } 545*9c5db199SXin Li 546*9c5db199SXin Li task_reference_id = self._lookup_task_reference(job) 547*9c5db199SXin Li if task_reference_id is not None: 548*9c5db199SXin Li self.update('tko_task_references', 549*9c5db199SXin Li data, 550*9c5db199SXin Li {'id': task_reference_id}, 551*9c5db199SXin Li commit=commit) 552*9c5db199SXin Li job.task_reference_id = task_reference_id 553*9c5db199SXin Li else: 554*9c5db199SXin Li self.insert('tko_task_references', data, commit=commit) 555*9c5db199SXin Li job.task_reference_id = self.get_last_autonumber_value() 556*9c5db199SXin Li 557*9c5db199SXin Li 558*9c5db199SXin Li def update_job_keyvals(self, job, commit=None): 559*9c5db199SXin Li """Updates the job key values. 560*9c5db199SXin Li 561*9c5db199SXin Li @param job: The job object. 562*9c5db199SXin Li @param commit: If commit the transaction . 563*9c5db199SXin Li """ 564*9c5db199SXin Li for key, value in six.iteritems(job.keyval_dict): 565*9c5db199SXin Li where = {'job_id': job.job_idx, 'key': key} 566*9c5db199SXin Li data = dict(where, value=value) 567*9c5db199SXin Li exists = self.select('id', 'tko_job_keyvals', where=where) 568*9c5db199SXin Li 569*9c5db199SXin Li if exists: 570*9c5db199SXin Li self.update('tko_job_keyvals', data, where=where, commit=commit) 571*9c5db199SXin Li else: 572*9c5db199SXin Li self.insert('tko_job_keyvals', data, commit=commit) 573*9c5db199SXin Li 574*9c5db199SXin Li 575*9c5db199SXin Li def insert_test(self, job, test, commit = None): 576*9c5db199SXin Li """Inserts a job test. 577*9c5db199SXin Li 578*9c5db199SXin Li @param job: The job object. 579*9c5db199SXin Li @param test: The test object. 580*9c5db199SXin Li @param commit: If commit the transaction . 581*9c5db199SXin Li """ 582*9c5db199SXin Li kver = self.insert_kernel(test.kernel, commit=commit) 583*9c5db199SXin Li data = {'job_idx':job.job_idx, 'test':test.testname, 584*9c5db199SXin Li 'subdir':test.subdir, 'kernel_idx':kver, 585*9c5db199SXin Li 'status':self.status_idx[test.status], 586*9c5db199SXin Li 'reason':test.reason, 'machine_idx':job.machine_idx, 587*9c5db199SXin Li 'started_time': test.started_time, 588*9c5db199SXin Li 'finished_time':test.finished_time} 589*9c5db199SXin Li is_update = hasattr(test, "test_idx") 590*9c5db199SXin Li if is_update: 591*9c5db199SXin Li test_idx = test.test_idx 592*9c5db199SXin Li self.update('tko_tests', data, 593*9c5db199SXin Li {'test_idx': test_idx}, commit=commit) 594*9c5db199SXin Li where = {'test_idx': test_idx} 595*9c5db199SXin Li self.delete('tko_iteration_result', where) 596*9c5db199SXin Li self.delete('tko_iteration_perf_value', where) 597*9c5db199SXin Li self.delete('tko_iteration_attributes', where) 598*9c5db199SXin Li where['user_created'] = 0 599*9c5db199SXin Li self.delete('tko_test_attributes', where) 600*9c5db199SXin Li else: 601*9c5db199SXin Li self.insert('tko_tests', data, commit=commit) 602*9c5db199SXin Li test_idx = test.test_idx = self.get_last_autonumber_value() 603*9c5db199SXin Li data = {'test_idx': test_idx} 604*9c5db199SXin Li 605*9c5db199SXin Li for i in test.iterations: 606*9c5db199SXin Li data['iteration'] = i.index 607*9c5db199SXin Li for key, value in six.iteritems(i.attr_keyval): 608*9c5db199SXin Li data['attribute'] = key 609*9c5db199SXin Li data['value'] = value 610*9c5db199SXin Li self.insert('tko_iteration_attributes', data, 611*9c5db199SXin Li commit=commit) 612*9c5db199SXin Li for key, value in six.iteritems(i.perf_keyval): 613*9c5db199SXin Li data['attribute'] = key 614*9c5db199SXin Li if math.isnan(value) or math.isinf(value): 615*9c5db199SXin Li data['value'] = None 616*9c5db199SXin Li else: 617*9c5db199SXin Li data['value'] = value 618*9c5db199SXin Li self.insert('tko_iteration_result', data, 619*9c5db199SXin Li commit=commit) 620*9c5db199SXin Li 621*9c5db199SXin Li data = {'test_idx': test_idx} 622*9c5db199SXin Li 623*9c5db199SXin Li for key, value in six.iteritems(test.attributes): 624*9c5db199SXin Li data = {'test_idx': test_idx, 'attribute': key, 625*9c5db199SXin Li 'value': value} 626*9c5db199SXin Li try: 627*9c5db199SXin Li self.insert('tko_test_attributes', data, commit=commit) 628*9c5db199SXin Li except: 629*9c5db199SXin Li _log_error('Uploading attribute %r' % (data)) 630*9c5db199SXin Li raise 631*9c5db199SXin Li 632*9c5db199SXin Li if not is_update: 633*9c5db199SXin Li for label_index in test.labels: 634*9c5db199SXin Li data = {'test_id': test_idx, 'testlabel_id': label_index} 635*9c5db199SXin Li self.insert('tko_test_labels_tests', data, commit=commit) 636*9c5db199SXin Li 637*9c5db199SXin Li 638*9c5db199SXin Li def read_machine_map(self): 639*9c5db199SXin Li """Reads the machine map.""" 640*9c5db199SXin Li if self.machine_group or not self.machine_map: 641*9c5db199SXin Li return 642*9c5db199SXin Li for line in open(self.machine_map, 'r').readlines(): 643*9c5db199SXin Li (machine, group) = line.split() 644*9c5db199SXin Li self.machine_group[machine] = group 645*9c5db199SXin Li 646*9c5db199SXin Li 647*9c5db199SXin Li def machine_info_dict(self, job): 648*9c5db199SXin Li """Reads the machine information of a job. 649*9c5db199SXin Li 650*9c5db199SXin Li @param job: The job object. 651*9c5db199SXin Li 652*9c5db199SXin Li @return: The machine info dictionary. 653*9c5db199SXin Li """ 654*9c5db199SXin Li hostname = job.machine 655*9c5db199SXin Li group = job.machine_group 656*9c5db199SXin Li owner = job.machine_owner 657*9c5db199SXin Li 658*9c5db199SXin Li if not group: 659*9c5db199SXin Li self.read_machine_map() 660*9c5db199SXin Li group = self.machine_group.get(hostname, hostname) 661*9c5db199SXin Li if group == hostname and owner: 662*9c5db199SXin Li group = owner + '/' + hostname 663*9c5db199SXin Li 664*9c5db199SXin Li return {'hostname': hostname, 'machine_group': group, 'owner': owner} 665*9c5db199SXin Li 666*9c5db199SXin Li 667*9c5db199SXin Li def insert_or_update_machine(self, job, commit=None): 668*9c5db199SXin Li """Insert or updates machine information for the given job. 669*9c5db199SXin Li 670*9c5db199SXin Li Also updates the job object with new machine index, if any. 671*9c5db199SXin Li 672*9c5db199SXin Li @param job: tko.models.job object. 673*9c5db199SXin Li @param commit: Whether to commit the database transaction. 674*9c5db199SXin Li """ 675*9c5db199SXin Li job.machine_idx = self._lookup_machine(job.machine) 676*9c5db199SXin Li if not job.machine_idx: 677*9c5db199SXin Li job.machine_idx = self._insert_machine(job, commit=commit) 678*9c5db199SXin Li elif job.machine: 679*9c5db199SXin Li # Only try to update tko_machines record if machine is set. This 680*9c5db199SXin Li # prevents unnecessary db writes for suite jobs. 681*9c5db199SXin Li self._update_machine_information(job, commit=commit) 682*9c5db199SXin Li 683*9c5db199SXin Li 684*9c5db199SXin Li def _lookup_task_reference(self, job): 685*9c5db199SXin Li """Find the task_reference_id for a given job. Return None if not found. 686*9c5db199SXin Li 687*9c5db199SXin Li @param job: tko.models.job object. 688*9c5db199SXin Li """ 689*9c5db199SXin Li if job.job_idx is None: 690*9c5db199SXin Li return None 691*9c5db199SXin Li rows = self.select( 692*9c5db199SXin Li 'id', 'tko_task_references', {'tko_job_idx': job.job_idx}) 693*9c5db199SXin Li if not rows: 694*9c5db199SXin Li return None 695*9c5db199SXin Li if len(rows) > 1: 696*9c5db199SXin Li raise MySQLTooManyRows('Got %d tko_task_references for tko_job %d' 697*9c5db199SXin Li % (len(rows), job.job_idx)) 698*9c5db199SXin Li return rows[0][0] 699*9c5db199SXin Li 700*9c5db199SXin Li 701*9c5db199SXin Li def _insert_machine(self, job, commit = None): 702*9c5db199SXin Li """Inserts the job machine. 703*9c5db199SXin Li 704*9c5db199SXin Li @param job: The job object. 705*9c5db199SXin Li @param commit: If commit the transaction . 706*9c5db199SXin Li """ 707*9c5db199SXin Li machine_info = self.machine_info_dict(job) 708*9c5db199SXin Li self.insert('tko_machines', machine_info, commit=commit) 709*9c5db199SXin Li return self.get_last_autonumber_value() 710*9c5db199SXin Li 711*9c5db199SXin Li 712*9c5db199SXin Li def _update_machine_information(self, job, commit = None): 713*9c5db199SXin Li """Updates the job machine information. 714*9c5db199SXin Li 715*9c5db199SXin Li @param job: The job object. 716*9c5db199SXin Li @param commit: If commit the transaction . 717*9c5db199SXin Li """ 718*9c5db199SXin Li machine_info = self.machine_info_dict(job) 719*9c5db199SXin Li self.update('tko_machines', machine_info, 720*9c5db199SXin Li where={'hostname': machine_info['hostname']}, 721*9c5db199SXin Li commit=commit) 722*9c5db199SXin Li 723*9c5db199SXin Li 724*9c5db199SXin Li def _lookup_machine(self, hostname): 725*9c5db199SXin Li """Look up the machine information. 726*9c5db199SXin Li 727*9c5db199SXin Li @param hostname: The hostname as string. 728*9c5db199SXin Li """ 729*9c5db199SXin Li where = { 'hostname' : hostname } 730*9c5db199SXin Li rows = self.select('machine_idx', 'tko_machines', where) 731*9c5db199SXin Li if rows: 732*9c5db199SXin Li return rows[0][0] 733*9c5db199SXin Li else: 734*9c5db199SXin Li return None 735*9c5db199SXin Li 736*9c5db199SXin Li 737*9c5db199SXin Li def lookup_kernel(self, kernel): 738*9c5db199SXin Li """Look up the kernel. 739*9c5db199SXin Li 740*9c5db199SXin Li @param kernel: The kernel object. 741*9c5db199SXin Li """ 742*9c5db199SXin Li rows = self.select('kernel_idx', 'tko_kernels', 743*9c5db199SXin Li {'kernel_hash':kernel.kernel_hash}) 744*9c5db199SXin Li if rows: 745*9c5db199SXin Li return rows[0][0] 746*9c5db199SXin Li else: 747*9c5db199SXin Li return None 748*9c5db199SXin Li 749*9c5db199SXin Li 750*9c5db199SXin Li def insert_kernel(self, kernel, commit = None): 751*9c5db199SXin Li """Insert a kernel. 752*9c5db199SXin Li 753*9c5db199SXin Li @param kernel: The kernel object. 754*9c5db199SXin Li @param commit: If commit the transaction . 755*9c5db199SXin Li """ 756*9c5db199SXin Li kver = self.lookup_kernel(kernel) 757*9c5db199SXin Li if kver: 758*9c5db199SXin Li return kver 759*9c5db199SXin Li 760*9c5db199SXin Li # If this kernel has any significant patches, append their hash 761*9c5db199SXin Li # as diferentiator. 762*9c5db199SXin Li printable = kernel.base 763*9c5db199SXin Li patch_count = 0 764*9c5db199SXin Li for patch in kernel.patches: 765*9c5db199SXin Li match = re.match(r'.*(-mm[0-9]+|-git[0-9]+)\.(bz2|gz)$', 766*9c5db199SXin Li patch.reference) 767*9c5db199SXin Li if not match: 768*9c5db199SXin Li patch_count += 1 769*9c5db199SXin Li 770*9c5db199SXin Li self.insert('tko_kernels', 771*9c5db199SXin Li {'base':kernel.base, 772*9c5db199SXin Li 'kernel_hash':kernel.kernel_hash, 773*9c5db199SXin Li 'printable':printable}, 774*9c5db199SXin Li commit=commit) 775*9c5db199SXin Li kver = self.get_last_autonumber_value() 776*9c5db199SXin Li 777*9c5db199SXin Li if patch_count > 0: 778*9c5db199SXin Li printable += ' p%d' % (kver) 779*9c5db199SXin Li self.update('tko_kernels', 780*9c5db199SXin Li {'printable':printable}, 781*9c5db199SXin Li {'kernel_idx':kver}) 782*9c5db199SXin Li 783*9c5db199SXin Li for patch in kernel.patches: 784*9c5db199SXin Li self.insert_patch(kver, patch, commit=commit) 785*9c5db199SXin Li return kver 786*9c5db199SXin Li 787*9c5db199SXin Li 788*9c5db199SXin Li def insert_patch(self, kver, patch, commit = None): 789*9c5db199SXin Li """Insert a kernel patch. 790*9c5db199SXin Li 791*9c5db199SXin Li @param kver: The kernel version. 792*9c5db199SXin Li @param patch: The kernel patch object. 793*9c5db199SXin Li @param commit: If commit the transaction . 794*9c5db199SXin Li """ 795*9c5db199SXin Li print(patch.reference) 796*9c5db199SXin Li name = os.path.basename(patch.reference)[:80] 797*9c5db199SXin Li self.insert('tko_patches', 798*9c5db199SXin Li {'kernel_idx': kver, 799*9c5db199SXin Li 'name':name, 800*9c5db199SXin Li 'url':patch.reference, 801*9c5db199SXin Li 'hash':patch.hash}, 802*9c5db199SXin Li commit=commit) 803*9c5db199SXin Li 804*9c5db199SXin Li 805*9c5db199SXin Li def find_tests(self, job_idx): 806*9c5db199SXin Li """Find all tests by job index. 807*9c5db199SXin Li 808*9c5db199SXin Li @param job_idx: The job index. 809*9c5db199SXin Li @return: A list of tests. 810*9c5db199SXin Li """ 811*9c5db199SXin Li where = { 'job_idx':job_idx } 812*9c5db199SXin Li rows = self.select('test_idx', 'tko_tests', where) 813*9c5db199SXin Li if rows: 814*9c5db199SXin Li return [row[0] for row in rows] 815*9c5db199SXin Li else: 816*9c5db199SXin Li return [] 817*9c5db199SXin Li 818*9c5db199SXin Li 819*9c5db199SXin Li def find_job(self, tag): 820*9c5db199SXin Li """Find a job by tag. 821*9c5db199SXin Li 822*9c5db199SXin Li @param tag: The job tag name. 823*9c5db199SXin Li @return: The job object or None. 824*9c5db199SXin Li """ 825*9c5db199SXin Li rows = self.select('job_idx', 'tko_jobs', {'tag': tag}) 826*9c5db199SXin Li if rows: 827*9c5db199SXin Li return rows[0][0] 828*9c5db199SXin Li else: 829*9c5db199SXin Li return None 830*9c5db199SXin Li 831*9c5db199SXin Li 832*9c5db199SXin Lidef db(*args, **dargs): 833*9c5db199SXin Li """Creates an instance of the database class with the arguments 834*9c5db199SXin Li provided in args and dargs, using the database type specified by 835*9c5db199SXin Li the global configuration (defaulting to mysql). 836*9c5db199SXin Li 837*9c5db199SXin Li @param args: The db_type arguments. 838*9c5db199SXin Li @param dargs: The db_type named arguments. 839*9c5db199SXin Li 840*9c5db199SXin Li @return: An db object. 841*9c5db199SXin Li """ 842*9c5db199SXin Li return db_sql(*args, **dargs) 843