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