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