aboutsummaryrefslogtreecommitdiff
path: root/app/utils/bisect/boot.py
blob: 819626b89942cccd2d283c9d8701100e7dbdd3ed (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""All boot bisect operations."""

try:
    import simplejson as json
except ImportError:
    import json

import bson
import bson.json_util
import datetime
import pymongo
import types

import models
import models.bisect as mbisect
import utils
import utils.db
import utils.bisect.common as bcommon

BOOT_SEARCH_FIELDS = [
    models.ARCHITECTURE_KEY,
    models.BOARD_KEY,
    models.CREATED_KEY,
    models.DEFCONFIG_FULL_KEY,
    models.DEFCONFIG_ID_KEY,
    models.DEFCONFIG_KEY,
    models.ID_KEY,
    models.JOB_ID_KEY,
    models.JOB_KEY,
    models.KERNEL_KEY,
    models.LAB_NAME_KEY,
    models.STATUS_KEY
]

BOOT_SORT = [(models.CREATED_KEY, pymongo.DESCENDING)]


def execute_boot_bisection(doc_id, db_options, fields=None):
    """Perform a bisect-like on the provided boot report.

    It searches all the previous boot reports starting from the provided one
    until it finds one whose boot passed. After that, it looks for all the
    defconfig reports and combines the value into a single data structure.

    :param doc_id: The boot document ID.
    :type doc_id: str
    :param db_options: The mongodb database connection parameters.
    :type db_options: dict
    :param fields: A `fields` data structure with the fields to return or
    exclude. Default to None.
    :type fields: list or dict
    :return A numeric value for the result status and a list dictionaries.
    """
    database = utils.db.get_db_connection(db_options)
    result = []
    code = 200

    obj_id = bson.objectid.ObjectId(doc_id)
    start_doc = utils.db.find_one(
        database[models.BOOT_COLLECTION], [obj_id], fields=BOOT_SEARCH_FIELDS
    )

    if all([start_doc, isinstance(start_doc, types.DictionaryType)]):
        start_doc_get = start_doc.get

        if start_doc_get(models.STATUS_KEY) == models.PASS_STATUS:
            code = 400
            result = None
        else:
            bisect_doc = _find_boot_bisect_data(
                obj_id, start_doc, database, db_options)

            return_code, saved_id = utils.db.save(
                database, bisect_doc, manipulate=True)
            if return_code == 201:
                bisect_doc.id = saved_id
            else:
                utils.LOG.error("Error saving bisect data %s", doc_id)

            bisect_doc = bcommon.update_doc_fields(bisect_doc, fields)
            result = [
                json.loads(
                    json.dumps(
                        bisect_doc,
                        default=bson.json_util.default,
                        ensure_ascii=False,
                        separators=(",", ":")
                    )
                )
            ]
    else:
        code = 404
        result = None

    return code, result


# pylint: disable=too-many-locals
def _find_boot_bisect_data(obj_id, start_doc, database, db_options):
    """Execute the real bisect logic.

    This is where the BisectDocument is created and returned.

    :param obj_id: The `bson.objectid.ObjectId` of the starting point.
    :type obj_id: bson.objectid.ObjectId
    :param start_doc: The starting document.
    :type start_doc: dictionary
    :param database: The connection to the database.
    :param db_options: The options for the database connection.
    :type db_options: dictionary
    :return A BisectDocument instance.
    """
    start_doc_get = start_doc.get

    board = start_doc_get(models.BOARD_KEY)
    job = start_doc_get(models.JOB_KEY)
    defconfig = start_doc_get(models.DEFCONFIG_KEY)
    defconfig_full = start_doc_get(
        models.DEFCONFIG_FULL_KEY) or defconfig
    created_on = start_doc_get(models.CREATED_KEY)
    arch = start_doc_get(
        models.ARCHITECTURE_KEY) or models.ARM_ARCHITECTURE_KEY
    lab_name = start_doc_get(models.LAB_NAME_KEY)

    bisect_doc = mbisect.BootBisectDocument(obj_id)
    bisect_doc.boot_id = obj_id
    bisect_doc.version = "1.0"
    bisect_doc.job = job
    bisect_doc.job_id = start_doc_get(models.JOB_ID_KEY, None)
    bisect_doc.defconfig_id = start_doc_get(
        models.DEFCONFIG_ID_KEY, None)
    bisect_doc.created_on = datetime.datetime.now(tz=bson.tz_util.utc)
    bisect_doc.board = board

    spec = {
        models.LAB_NAME_KEY: lab_name,
        models.BOARD_KEY: board,
        models.DEFCONFIG_KEY: defconfig,
        models.DEFCONFIG_FULL_KEY: defconfig_full,
        models.JOB_KEY: start_doc_get(models.JOB_KEY),
        models.ARCHITECTURE_KEY: arch,
        models.CREATED_KEY: {"$lt": created_on}
    }

    # The function to apply to each boot document to find its defconfig
    # one and combine the values.
    func = bcommon.combine_defconfig_values

    bad_doc = func(start_doc, db_options)
    bad_doc_get = bad_doc.get

    bisect_doc.bad_commit_date = bad_doc_get(
        models.BISECT_DEFCONFIG_CREATED_KEY)
    bisect_doc.bad_commit = bad_doc_get(models.GIT_COMMIT_KEY)
    bisect_doc.bad_commit_url = bad_doc_get(models.GIT_URL_KEY)

    all_valid_docs = [bad_doc]

    # Search through all the previous boot reports, until one that
    # passed is found, and combine them with their defconfig document.
    all_prev_docs = utils.db.find(
        database[models.BOOT_COLLECTION],
        0,
        0,
        spec=spec,
        fields=BOOT_SEARCH_FIELDS,
        sort=BOOT_SORT
    )

    if all_prev_docs:
        all_valid_docs.extend(
            [
                func(doc, db_options)
                for doc in bcommon.get_docs_until_pass(all_prev_docs)
            ]
        )
        # The last doc should be the good one, in case it is, add the
        # values to the bisect_doc.
        good_doc = all_valid_docs[-1]
        if (good_doc[models.BISECT_BOOT_STATUS_KEY] ==
                models.PASS_STATUS):
            good_doc_get = good_doc.get
            bisect_doc.good_commit = good_doc_get(
                models.GIT_COMMIT_KEY)
            bisect_doc.good_commit_url = good_doc_get(
                models.GIT_URL_KEY)
            bisect_doc.good_commit_date = good_doc_get(
                models.BISECT_DEFCONFIG_CREATED_KEY)

    # Store everything in the bisect_data list of the bisect_doc.
    bisect_doc.bisect_data = all_valid_docs
    return bisect_doc


# pylint: disable=invalid-name
def execute_boot_bisection_compared_to(
        doc_id, compare_to, db_options, fields=None):
    """Execute a bisect for one tree compared to another one.

    :param doc_id: The ID of the boot report we want compared.
    :type doc_id: string
    :param compare_to: The tree name to compare against.
    :type compare_to: string
    :param db_options: The options for the database connection.
    :type db_options: dictionary
    :param fields: A `fields` data structure with the fields to return or
    exclude. Default to None.
    :type fields: list or dict
    :return A numeric value for the result status and a list dictionaries.
    """
    database = utils.db.get_db_connection(db_options)
    result = []
    code = 200

    obj_id = bson.objectid.ObjectId(doc_id)
    start_doc = utils.db.find_one2(
        database[models.BOOT_COLLECTION], obj_id, fields=BOOT_SEARCH_FIELDS
    )

    if all([start_doc, isinstance(start_doc, types.DictionaryType)]):
        start_doc_get = start_doc.get

        if start_doc_get(models.STATUS_KEY) == models.PASS_STATUS:
            code = 400
            result = None
        else:
            # TODO: we need to know the baseline tree commit in order not to
            # search too much in the past.

            # Search for a previous normal bisect. If we find it, use the good
            # commit date as the maximum date to search in the comparison tree
            # and retrieve at max the number of commit available in the bisect
            # data list. If we do not have the previous bisect, return max 10
            # documents since we do not know which is the last valid commit
            # we are based on.
            end_date = None
            limit = 10

            prev_bisect = utils.db.find_one2(
                database[models.BISECT_COLLECTION],
                {models.BOOT_ID_KEY: obj_id})

            if prev_bisect:
                b_get = prev_bisect.get
                good_comit_date = b_get(models.BISECT_GOOD_COMMIT_DATE, None)
                bisect_data = b_get(models.BISECT_DATA_KEY, None)

                if good_comit_date:
                    end_date = good_comit_date
                if bisect_data:
                    limit = len(bisect_data)
                # If we don't have the good commit, but we have a list of
                # failed commit, pick the last one - since they are ordered by
                # creation date - and use its boot creation date.
                if not end_date and bisect_data:
                    last = bisect_data[-1]
                    end_date = last.get(models.BISECT_BOOT_CREATED_KEY, None)

            board = start_doc_get(models.BOARD_KEY)
            job = start_doc_get(models.JOB_KEY)
            defconfig = start_doc_get(models.DEFCONFIG_KEY)
            defconfig_full = start_doc_get(
                models.DEFCONFIG_FULL_KEY) or defconfig
            created_on = start_doc_get(models.CREATED_KEY)
            arch = start_doc_get(
                models.ARCHITECTURE_KEY) or models.ARM_ARCHITECTURE_KEY
            lab_name = start_doc_get(models.LAB_NAME_KEY)

            bisect_doc = mbisect.BootBisectDocument(obj_id)
            bisect_doc.compare_to = compare_to
            bisect_doc.version = "1.0"
            bisect_doc.job = job
            bisect_doc.job_id = start_doc_get(models.JOB_ID_KEY, None)
            bisect_doc.defconfig_id = start_doc_get(
                models.DEFCONFIG_ID_KEY, None)
            bisect_doc.boot_id = obj_id
            bisect_doc.created_on = datetime.datetime.now(tz=bson.tz_util.utc)
            bisect_doc.board = board

            if end_date:
                date_range = {
                    "$lt": created_on,
                    "$gte": end_date
                }
            else:
                date_range = {"$lt": created_on}

            spec = {
                models.LAB_NAME_KEY: lab_name,
                models.BOARD_KEY: board,
                models.DEFCONFIG_KEY: defconfig,
                models.DEFCONFIG_FULL_KEY: defconfig_full,
                models.JOB_KEY: compare_to,
                models.ARCHITECTURE_KEY: arch,
                models.CREATED_KEY: date_range
            }
            prev_docs = utils.db.find(
                database[models.BOOT_COLLECTION],
                limit,
                0,
                spec=spec,
                fields=BOOT_SEARCH_FIELDS,
                sort=BOOT_SORT)

            all_valid_docs = []
            if prev_docs:
                # The function to apply to each boot document to find its
                # defconfig one and combine the values.
                func = bcommon.combine_defconfig_values

                all_valid_docs.extend(
                    [
                        func(doc, db_options) for doc in prev_docs
                    ]
                )

            bisect_doc.bisect_data = all_valid_docs
            return_code, saved_id = utils.db.save(
                database, bisect_doc, manipulate=True)
            if return_code == 201:
                bisect_doc.id = saved_id
            else:
                utils.LOG.error("Error saving compared-bisect data %s", doc_id)

            bisect_doc = bcommon.update_doc_fields(bisect_doc, fields)
            result = [
                json.loads(
                    json.dumps(
                        bisect_doc,
                        default=bson.json_util.default,
                        ensure_ascii=False,
                        separators=(",", ":")
                    )
                )
            ]
    else:
        code = 404
        result = None

    return code, result