blob: 327059d00de4f9076c62c916b67d96b0cbcce458 [file] [log] [blame]
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001#
Patrick Williamsc0f7c042017-02-23 20:41:17 -06002# BitBake Toaster Implementation
3#
4# Copyright (C) 2016 Intel Corporation
5#
Brad Bishopc342db32019-05-15 21:57:59 -04006# SPDX-License-Identifier: GPL-2.0-only
Patrick Williamsc0f7c042017-02-23 20:41:17 -06007#
Patrick Williamsc0f7c042017-02-23 20:41:17 -06008
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
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500561 self.columns[self.toggle_columns['order']]['hideable'] = True
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600562 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
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500577 self.columns[self.toggle_columns['order']]['hideable'] = True
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600578 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
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500594 self.columns[self.toggle_columns['order']]['hideable'] = True
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600595 self.columns[self.toggle_columns['sstate_result']]['hidden'] = True
596 self.columns[self.toggle_columns['disk_io']]['hidden'] = False