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