1#
2# BitBake Toaster Implementation
3#
4# Copyright (C) 2016 Intel Corporation
5#
6# SPDX-License-Identifier: GPL-2.0-only
7#
8
9from orm.models import Build, Task, Target, Package
10from django.db.models import Q, Sum
11
12import toastergui.tables as tables
13from toastergui.widgets import ToasterTable
14from toastergui.tablefilter import TableFilter
15from toastergui.tablefilter import TableFilterActionToggle
16
17
18class BuildTablesMixin(ToasterTable):
19    def get_context_data(self, **kwargs):
20        # We need to be explicit about which superclass we're calling here
21        # Otherwise the MRO gets in a right mess
22        context = ToasterTable.get_context_data(self, **kwargs)
23        context['build'] = Build.objects.get(pk=kwargs['build_id'])
24        return context
25
26
27class BuiltPackagesTableBase(tables.PackagesTable):
28    """ Table to display all the packages built in a build """
29    def __init__(self, *args, **kwargs):
30        super(BuiltPackagesTableBase, self).__init__(*args, **kwargs)
31        self.title = "Packages built"
32        self.default_orderby = "name"
33
34    def setup_queryset(self, *args, **kwargs):
35        build = Build.objects.get(pk=kwargs['build_id'])
36        self.static_context_extra['build'] = build
37        self.static_context_extra['target_name'] = None
38        self.queryset = build.package_set.all().exclude(recipe=None)
39        self.queryset = self.queryset.order_by(self.default_orderby)
40
41    def setup_columns(self, *args, **kwargs):
42        super(BuiltPackagesTableBase, self).setup_columns(*args, **kwargs)
43
44        def pkg_link_template(val):
45            """ return the template used for the link with the val as the
46            element value i.e. inside the <a></a>"""
47
48            return ('''
49                    <a href="
50                    {%% url "package_built_detail" extra.build.pk data.pk %%}
51                    ">%s</a>
52                    ''' % val)
53
54        def recipe_link_template(val):
55            return ('''
56                    {%% if data.recipe %%}
57                    <a href="
58                    {%% url "recipe" extra.build.pk data.recipe.pk %%}
59                    ">%(value)s</a>
60                    {%% else %%}
61                    %(value)s
62                    {%% endif %%}
63                    ''' % {'value': val})
64
65        add_pkg_link_to = 'name'
66        add_recipe_link_to = 'recipe__name'
67
68        # Add the recipe and pkg build links to the required columns
69        for column in self.columns:
70            # Convert to template field style accessors
71            tmplv = column['field_name'].replace('__', '.')
72            tmplv = "{{data.%s}}" % tmplv
73
74            if column['field_name'] is add_pkg_link_to:
75                # Don't overwrite an existing template
76                if column['static_data_template']:
77                    column['static_data_template'] =\
78                        pkg_link_template(column['static_data_template'])
79                else:
80                    column['static_data_template'] = pkg_link_template(tmplv)
81
82                column['static_data_name'] = column['field_name']
83
84            elif column['field_name'] is add_recipe_link_to:
85                # Don't overwrite an existing template
86                if column['static_data_template']:
87                    column['static_data_template'] =\
88                        recipe_link_template(column['static_data_template'])
89                else:
90                    column['static_data_template'] =\
91                        recipe_link_template(tmplv)
92                column['static_data_name'] = column['field_name']
93
94        self.add_column(title="Layer",
95                        field_name="recipe__layer_version__layer__name",
96                        hidden=True,
97                        orderable=True)
98
99        layer_branch_template = '''
100        {%if not data.recipe.layer_version.layer.local_source_dir %}
101        <span class="text-muted">{{data.recipe.layer_version.branch}}</span>
102        {% else %}
103        <span class="text-muted">Not applicable</span>
104        <span class="glyphicon glyphicon-question-sign get-help" data-original-title="" title="The source code of {{data.recipe.layer_version.layer.name}} is not in a Git repository, so there is no branch associated with it"> </span>
105        {% endif %}
106        '''
107
108        self.add_column(title="Layer branch",
109                        field_name="recipe__layer_version__branch",
110                        hidden=True,
111                        static_data_name="recipe__layer_version__branch",
112                        static_data_template=layer_branch_template,
113                        orderable=True)
114
115        git_rev_template = '''
116        {% if not data.recipe.layer_version.layer.local_source_dir %}
117        {% with vcs_ref=data.recipe.layer_version.commit %}
118        {% include 'snippets/gitrev_popover.html' %}
119        {% endwith %}
120        {% else %}
121        <span class="text-muted">Not applicable</span>
122        <span class="glyphicon glyphicon-question-sign get-help" data-original-title="" title="The source code of {{data.recipe.layer_version.layer.name}} is not in a Git repository, so there is no revision associated with it"> </span>
123        {% endif %}
124        '''
125
126        self.add_column(title="Layer commit",
127                        static_data_name='vcs_ref',
128                        static_data_template=git_rev_template,
129                        hidden=True)
130
131
132class BuiltPackagesTable(BuildTablesMixin, BuiltPackagesTableBase):
133    """ Show all the packages built for the selected build """
134    def __init__(self, *args, **kwargs):
135        super(BuiltPackagesTable, self).__init__(*args, **kwargs)
136        self.title = "Packages built"
137        self.default_orderby = "name"
138
139        self.empty_state =\
140            ('<strong>No packages were built.</strong> How did this happen? '
141             'Well, BitBake reuses as much stuff as possible. '
142             'If all of the packages needed were already built and available '
143             'in your build infrastructure, BitBake '
144             'will not rebuild any of them. This might be slightly confusing, '
145             'but it does make everything faster.')
146
147    def setup_columns(self, *args, **kwargs):
148        super(BuiltPackagesTable, self).setup_columns(*args, **kwargs)
149
150        def remove_dep_cols(columns):
151            for column in columns:
152                # We don't need these fields
153                if column['static_data_name'] in ['reverse_dependencies',
154                                                  'dependencies']:
155                    continue
156
157                yield column
158
159        self.columns = list(remove_dep_cols(self.columns))
160
161
162class InstalledPackagesTable(BuildTablesMixin, BuiltPackagesTableBase):
163    """ Show all packages installed in an image """
164    def __init__(self, *args, **kwargs):
165        super(InstalledPackagesTable, self).__init__(*args, **kwargs)
166        self.title = "Packages Included"
167        self.default_orderby = "name"
168
169    def make_package_list(self, target):
170        # The database design means that you get the intermediate objects and
171        # not package objects like you'd really want so we get them here
172        pkgs = target.target_installed_package_set.values_list('package',
173                                                               flat=True)
174        return Package.objects.filter(pk__in=pkgs)
175
176    def get_context_data(self, **kwargs):
177        context = super(InstalledPackagesTable,
178                        self).get_context_data(**kwargs)
179
180        target = Target.objects.get(pk=kwargs['target_id'])
181        packages = self.make_package_list(target)
182
183        context['packages_sum'] = packages.aggregate(
184            Sum('installed_size'))['installed_size__sum']
185
186        context['target'] = target
187        return context
188
189    def setup_queryset(self, *args, **kwargs):
190        build = Build.objects.get(pk=kwargs['build_id'])
191        self.static_context_extra['build'] = build
192
193        target = Target.objects.get(pk=kwargs['target_id'])
194        # We send these separately because in the case of image details table
195        # we don't have a target just the recipe name as the target
196        self.static_context_extra['target_name'] = target.target
197        self.static_context_extra['target_id'] = target.pk
198
199        self.static_context_extra['add_links'] = True
200
201        self.queryset = self.make_package_list(target)
202        self.queryset = self.queryset.order_by(self.default_orderby)
203
204    def setup_columns(self, *args, **kwargs):
205        super(InstalledPackagesTable, self).setup_columns(**kwargs)
206        self.add_column(title="Installed size",
207                        static_data_name="installed_size",
208                        static_data_template="{% load projecttags %}"
209                        "{{data.size|filtered_filesizeformat}}",
210                        orderable=True,
211                        hidden=True)
212
213        # Add the template to show installed name for installed packages
214        install_name_tmpl =\
215            ('<a href="{% url "package_included_detail" extra.build.pk'
216             ' extra.target_id data.pk %}">{{data.name}}</a>'
217             '{% if data.installed_name and data.installed_name !='
218             ' data.name %}'
219             '<span class="text-muted"> as {{data.installed_name}}</span>'
220             ' <span class="glyphicon glyphicon-question-sign get-help hover-help"'
221             ' title="{{data.name}} was renamed at packaging time and'
222             ' was installed in your image as {{data.installed_name}}'
223             '"></span>{% endif %} ')
224
225        for column in self.columns:
226            if column['static_data_name'] == 'name':
227                column['static_data_template'] = install_name_tmpl
228                break
229
230
231class BuiltRecipesTable(BuildTablesMixin):
232    """ Table to show the recipes that have been built in this build """
233
234    def __init__(self, *args, **kwargs):
235        super(BuiltRecipesTable, self).__init__(*args, **kwargs)
236        self.title = "Recipes built"
237        self.default_orderby = "name"
238
239    def setup_queryset(self, *args, **kwargs):
240        build = Build.objects.get(pk=kwargs['build_id'])
241        self.static_context_extra['build'] = build
242        self.queryset = build.get_recipes()
243        self.queryset = self.queryset.order_by(self.default_orderby)
244
245    def setup_columns(self, *args, **kwargs):
246        recipe_name_tmpl =\
247            '<a href="{% url "recipe" extra.build.pk data.pk %}">'\
248            '{{data.name}}'\
249            '</a>'
250
251        recipe_file_tmpl =\
252            '{{data.file_path}}'\
253            '{% if data.pathflags %}<i>({{data.pathflags}})</i>'\
254            '{% endif %}'
255
256        git_branch_template = '''
257        {% if data.layer_version.layer.local_source_dir %}
258        <span class="text-muted">Not applicable</span>
259        <span class="glyphicon glyphicon-question-sign get-help" data-original-title="" title="The source code of {{data.layer_version.layer.name}} is not in a Git repository, so there is no branch associated with it"> </span>
260        {% else %}
261        <span>{{data.layer_version.branch}}</span>
262        {% endif %}
263        '''
264
265        git_rev_template = '''
266        {% if data.layer_version.layer.local_source_dir %}
267        <span class="text-muted">Not applicable</span>
268        <span class="glyphicon glyphicon-question-sign get-help" data-original-title="" title="The source code of {{data.layer_version.layer.name}} is not in a Git repository, so there is no commit associated with it"> </span>
269        {% else %}
270        {% with vcs_ref=data.layer_version.commit %}
271        {% include 'snippets/gitrev_popover.html' %}
272        {% endwith %}
273        {% endif %}
274        '''
275
276        depends_on_tmpl = '''
277        {% with deps=data.r_dependencies_recipe.all %}
278        {% with count=deps|length %}
279        {% if count %}
280        <a class="btn btn-default" title="
281        <a href='{% url "recipe" extra.build.pk data.pk %}#dependencies'>
282        {{data.name}}</a> dependencies"
283        data-content="<ul class='list-unstyled'>
284        {% for dep in deps|dictsort:"depends_on.name"%}
285        <li><a href='{% url "recipe" extra.build.pk dep.depends_on.pk %}'>
286        {{dep.depends_on.name}}</a></li>
287        {% endfor %}
288        </ul>">
289         {{count}}
290        </a>
291        {% endif %}{% endwith %}{% endwith %}
292        '''
293
294        rev_depends_tmpl = '''
295        {% with revs=data.r_dependencies_depends.all %}
296        {% with count=revs|length %}
297        {% if count %}
298        <a class="btn btn-default"
299        title="
300        <a href='{% url "recipe" extra.build.pk data.pk %}#brought-in-by'>
301        {{data.name}}</a> reverse dependencies"
302        data-content="<ul class='list-unstyled'>
303        {% for dep in revs|dictsort:"recipe.name" %}
304        <li>
305        <a href='{% url "recipe" extra.build.pk dep.recipe.pk %}'>
306        {{dep.recipe.name}}
307        </a></li>
308        {% endfor %}
309        </ul>">
310        {{count}}
311        </a>
312        {% endif %}{% endwith %}{% endwith %}
313        '''
314
315        self.add_column(title="Recipe",
316                        field_name="name",
317                        static_data_name='name',
318                        orderable=True,
319                        hideable=False,
320                        static_data_template=recipe_name_tmpl)
321
322        self.add_column(title="Version",
323                        hideable=False,
324                        field_name="version")
325
326        self.add_column(title="Dependencies",
327                        static_data_name="dependencies",
328                        static_data_template=depends_on_tmpl)
329
330        self.add_column(title="Reverse dependencies",
331                        static_data_name="revdeps",
332                        static_data_template=rev_depends_tmpl,
333                        help_text='Recipe build-time reverse dependencies'
334                        ' (i.e. the recipes that depend on this recipe)')
335
336        self.add_column(title="Recipe file",
337                        field_name="file_path",
338                        static_data_name="file_path",
339                        static_data_template=recipe_file_tmpl,
340                        hidden=True)
341
342        self.add_column(title="Section",
343                        field_name="section",
344                        orderable=True,
345                        hidden=True)
346
347        self.add_column(title="License",
348                        field_name="license",
349                        help_text='Multiple license names separated by the'
350                        ' pipe character indicates a choice between licenses.'
351                        ' Multiple license names separated by the ampersand'
352                        ' character indicates multiple licenses exist that'
353                        ' cover different parts of the source',
354                        orderable=True)
355
356        self.add_column(title="Layer",
357                        field_name="layer_version__layer__name",
358                        orderable=True)
359
360        self.add_column(title="Layer branch",
361                        field_name="layer_version__branch",
362                        static_data_name="layer_version__branch",
363                        static_data_template=git_branch_template,
364                        orderable=True,
365                        hidden=True)
366
367        self.add_column(title="Layer commit",
368                        static_data_name="commit",
369                        static_data_template=git_rev_template,
370                        hidden=True)
371
372
373class BuildTasksTable(BuildTablesMixin):
374    """ Table to show the tasks that run in this build """
375
376    def __init__(self, *args, **kwargs):
377        super(BuildTasksTable, self).__init__(*args, **kwargs)
378        self.title = "Tasks"
379        self.default_orderby = "order"
380
381        # Toggle these columns on off for Time/CPU usage/Disk I/O tables
382        self.toggle_columns = {}
383
384    def setup_queryset(self, *args, **kwargs):
385        build = Build.objects.get(pk=kwargs['build_id'])
386        self.static_context_extra['build'] = build
387        self.queryset = build.task_build.filter(~Q(order=None))
388        self.queryset = self.queryset.order_by(self.default_orderby)
389
390    def setup_filters(self, *args, **kwargs):
391        # Execution outcome types filter
392        executed_outcome = TableFilter(name="execution_outcome",
393                                       title="Filter Tasks by 'Executed")
394
395        exec_outcome_action_exec = TableFilterActionToggle(
396            "executed",
397            "Executed Tasks",
398            Q(task_executed=True))
399
400        exec_outcome_action_not_exec = TableFilterActionToggle(
401            "not_executed",
402            "Not Executed Tasks",
403            Q(task_executed=False))
404
405        executed_outcome.add_action(exec_outcome_action_exec)
406        executed_outcome.add_action(exec_outcome_action_not_exec)
407
408        # Task outcome types filter
409        task_outcome = TableFilter(name="task_outcome",
410                                   title="Filter Task by 'Outcome'")
411
412        for outcome_enum, title in Task.TASK_OUTCOME:
413            if outcome_enum is Task.OUTCOME_NA:
414                continue
415            action = TableFilterActionToggle(
416                title.replace(" ", "_").lower(),
417                "%s Tasks" % title,
418                Q(outcome=outcome_enum))
419
420            task_outcome.add_action(action)
421
422        # SSTATE outcome types filter
423        sstate_outcome = TableFilter(name="sstate_outcome",
424                                     title="Filter Task by 'Cache attempt'")
425
426        for sstate_result_enum, title in Task.SSTATE_RESULT:
427            action = TableFilterActionToggle(
428                title.replace(" ", "_").lower(),
429                "Tasks with '%s' attempts" % title,
430                Q(sstate_result=sstate_result_enum))
431
432            sstate_outcome.add_action(action)
433
434        self.add_filter(sstate_outcome)
435        self.add_filter(executed_outcome)
436        self.add_filter(task_outcome)
437
438    def setup_columns(self, *args, **kwargs):
439        self.toggle_columns['order'] = len(self.columns)
440
441        recipe_name_tmpl =\
442            '<a href="{% url "recipe" extra.build.pk data.recipe.pk %}">'\
443            '{{data.recipe.name}}'\
444            '</a>'
445
446        def task_link_tmpl(val):
447            return ('<a name="task-{{data.order}}"'
448                    'href="{%% url "task" extra.build.pk data.pk %%}">'
449                    '%s'
450                    '</a>') % str(val)
451
452        self.add_column(title="Order",
453                        static_data_name="order",
454                        static_data_template='{{data.order}}',
455                        hideable=False,
456                        orderable=True)
457
458        self.add_column(title="Task",
459                        static_data_name="task_name",
460                        static_data_template=task_link_tmpl(
461                            "{{data.task_name}}"),
462                        hideable=False,
463                        orderable=True)
464
465        self.add_column(title="Recipe",
466                        static_data_name='recipe__name',
467                        static_data_template=recipe_name_tmpl,
468                        hideable=False,
469                        orderable=True)
470
471        self.add_column(title="Recipe version",
472                        field_name='recipe__version',
473                        hidden=True)
474
475        self.add_column(title="Executed",
476                        static_data_name="task_executed",
477                        static_data_template='{{data.get_executed_display}}',
478                        filter_name='execution_outcome',
479                        orderable=True)
480
481        self.static_context_extra['OUTCOME_FAILED'] = Task.OUTCOME_FAILED
482        outcome_tmpl = '{{data.outcome_text}}'
483        outcome_tmpl = ('%s '
484                        '{%% if data.outcome = extra.OUTCOME_FAILED %%}'
485                        '<a href="{%% url "build_artifact" extra.build.pk '
486                        '          "tasklogfile" data.pk %%}">'
487                        ' <span class="glyphicon glyphicon-download-alt'
488                        ' get-help" title="Download task log file"></span>'
489                        '</a> {%% endif %%}'
490                        '<span class="glyphicon glyphicon-question-sign'
491                        ' get-help hover-help" style="visibility: hidden;" '
492                        'title="{{data.get_outcome_help}}"></span>'
493                        ) % outcome_tmpl
494
495        self.add_column(title="Outcome",
496                        static_data_name="outcome",
497                        static_data_template=outcome_tmpl,
498                        filter_name="task_outcome",
499                        orderable=True)
500
501        self.toggle_columns['sstate_result'] = len(self.columns)
502
503        self.add_column(title="Cache attempt",
504                        static_data_name="sstate_result",
505                        static_data_template='{{data.sstate_text}}',
506                        filter_name="sstate_outcome",
507                        orderable=True)
508
509        self.toggle_columns['elapsed_time'] = len(self.columns)
510
511        self.add_column(
512            title="Time (secs)",
513            static_data_name="elapsed_time",
514            static_data_template='{% load projecttags %}{% load humanize %}'
515            '{{data.elapsed_time|format_none_and_zero|floatformat:2}}',
516            orderable=True,
517            hidden=True)
518
519        self.toggle_columns['cpu_time_sys'] = len(self.columns)
520
521        self.add_column(
522            title="System CPU time (secs)",
523            static_data_name="cpu_time_system",
524            static_data_template='{% load projecttags %}{% load humanize %}'
525            '{{data.cpu_time_system|format_none_and_zero|floatformat:2}}',
526            hidden=True,
527            orderable=True)
528
529        self.toggle_columns['cpu_time_user'] = len(self.columns)
530
531        self.add_column(
532            title="User CPU time (secs)",
533            static_data_name="cpu_time_user",
534            static_data_template='{% load projecttags %}{% load humanize %}'
535            '{{data.cpu_time_user|format_none_and_zero|floatformat:2}}',
536            hidden=True,
537            orderable=True)
538
539        self.toggle_columns['disk_io'] = len(self.columns)
540
541        self.add_column(
542            title="Disk I/O (ms)",
543            static_data_name="disk_io",
544            static_data_template='{% load projecttags %}{% load humanize %}'
545            '{{data.disk_io|format_none_and_zero|filtered_filesizeformat}}',
546            hidden=True,
547            orderable=True)
548
549
550class BuildTimeTable(BuildTasksTable):
551    """ Same as tasks table but the Time column is default displayed"""
552
553    def __init__(self, *args, **kwargs):
554        super(BuildTimeTable, self).__init__(*args, **kwargs)
555        self.default_orderby = "-elapsed_time"
556
557    def setup_columns(self, *args, **kwargs):
558        super(BuildTimeTable, self).setup_columns(**kwargs)
559
560        self.columns[self.toggle_columns['order']]['hidden'] = True
561        self.columns[self.toggle_columns['order']]['hideable'] = True
562        self.columns[self.toggle_columns['sstate_result']]['hidden'] = True
563        self.columns[self.toggle_columns['elapsed_time']]['hidden'] = False
564
565
566class BuildCPUTimeTable(BuildTasksTable):
567    """ Same as tasks table but the CPU usage columns are default displayed"""
568
569    def __init__(self, *args, **kwargs):
570        super(BuildCPUTimeTable, self).__init__(*args, **kwargs)
571        self.default_orderby = "-cpu_time_system"
572
573    def setup_columns(self, *args, **kwargs):
574        super(BuildCPUTimeTable, self).setup_columns(**kwargs)
575
576        self.columns[self.toggle_columns['order']]['hidden'] = True
577        self.columns[self.toggle_columns['order']]['hideable'] = True
578        self.columns[self.toggle_columns['sstate_result']]['hidden'] = True
579        self.columns[self.toggle_columns['cpu_time_sys']]['hidden'] = False
580        self.columns[self.toggle_columns['cpu_time_user']]['hidden'] = False
581
582
583class BuildIOTable(BuildTasksTable):
584    """ Same as tasks table but the Disk IO column is default displayed"""
585
586    def __init__(self, *args, **kwargs):
587        super(BuildIOTable, self).__init__(*args, **kwargs)
588        self.default_orderby = "-disk_io"
589
590    def setup_columns(self, *args, **kwargs):
591        super(BuildIOTable, self).setup_columns(**kwargs)
592
593        self.columns[self.toggle_columns['order']]['hidden'] = True
594        self.columns[self.toggle_columns['order']]['hideable'] = True
595        self.columns[self.toggle_columns['sstate_result']]['hidden'] = True
596        self.columns[self.toggle_columns['disk_io']]['hidden'] = False
597