xref: /aosp_15_r20/external/autotest/tko/db.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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