xref: /openbmc/openbmc/poky/bitbake/lib/toaster/toastergui/views.py (revision c9537f57ab488bf5d90132917b0184e2527970a5)
1#
2# BitBake Toaster Implementation
3#
4# Copyright (C) 2013        Intel Corporation
5#
6# SPDX-License-Identifier: GPL-2.0-only
7#
8
9import ast
10import re
11import subprocess
12import sys
13
14import bb.cooker
15from bb.ui import toasterui
16from bb.ui import eventreplay
17
18from django.db.models import F, Q, Sum
19from django.db import IntegrityError
20from django.shortcuts import render, redirect, get_object_or_404, HttpResponseRedirect
21from django.utils.http import urlencode
22from orm.models import Build, Target, Task, Layer, Layer_Version, Recipe
23from orm.models import LogMessage, Variable, Package_Dependency, Package
24from orm.models import Task_Dependency, Package_File
25from orm.models import Target_Installed_Package, Target_File
26from orm.models import TargetKernelFile, TargetSDKFile, Target_Image_File
27from orm.models import BitbakeVersion, CustomImageRecipe, EventLogsImports
28
29from django.urls import reverse, resolve
30from django.contrib import messages
31
32from django.core.exceptions import ObjectDoesNotExist
33from django.core.files.storage import FileSystemStorage
34from django.core.files.uploadedfile import InMemoryUploadedFile, TemporaryUploadedFile
35from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
36from django.http import HttpResponseNotFound, JsonResponse
37from django.utils import timezone
38from django.views.generic import TemplateView
39from datetime import timedelta, datetime
40from toastergui.templatetags.projecttags import json as jsonfilter
41from decimal import Decimal
42import json
43import os
44from os.path import dirname
45import mimetypes
46
47from toastergui.forms import LoadFileForm
48
49from collections import namedtuple
50
51import logging
52
53from toastermain.logs import log_view_mixin
54
55logger = logging.getLogger("toaster")
56
57# Project creation and managed build enable
58project_enable = ('1' == os.environ.get('TOASTER_BUILDSERVER'))
59is_project_specific = ('1' == os.environ.get('TOASTER_PROJECTSPECIFIC'))
60import_page = False
61
62class MimeTypeFinder(object):
63    # setting this to False enables additional non-standard mimetypes
64    # to be included in the guess
65    _strict = False
66
67    # returns the mimetype for a file path as a string,
68    # or 'application/octet-stream' if the type couldn't be guessed
69    @classmethod
70    def get_mimetype(self, path):
71        guess = mimetypes.guess_type(path, self._strict)
72        guessed_type = guess[0]
73        if guessed_type is None:
74            guessed_type = 'application/octet-stream'
75        return guessed_type
76
77# single point to add global values into the context before rendering
78@log_view_mixin
79def toaster_render(request, page, context):
80    context['project_enable'] = project_enable
81    context['project_specific'] = is_project_specific
82    return render(request, page, context)
83
84
85# all new sessions should come through the landing page;
86# determine in which mode we are running in, and redirect appropriately
87def landing(request):
88    # in build mode, we redirect to the command-line builds page
89    # if there are any builds for the default (cli builds) project
90    default_project = Project.objects.get_or_create_default_project()
91    default_project_builds = Build.objects.filter(project = default_project)
92
93    # we only redirect to projects page if there is a user-generated project
94    num_builds = Build.objects.all().count()
95    user_projects = Project.objects.filter(is_default = False)
96    has_user_project = user_projects.count() > 0
97
98    if num_builds == 0 and has_user_project:
99        return redirect(reverse('all-projects'), permanent = False)
100
101    if num_builds > 0:
102        return redirect(reverse('all-builds'), permanent = False)
103
104    context = {'lvs_nos' : Layer_Version.objects.all().count()}
105
106    return toaster_render(request, 'landing.html', context)
107
108def objtojson(obj):
109    from django.db.models.query import QuerySet
110    from django.db.models import Model
111
112    if isinstance(obj, datetime):
113        return obj.isoformat()
114    elif isinstance(obj, timedelta):
115        return obj.total_seconds()
116    elif isinstance(obj, QuerySet) or isinstance(obj, set):
117        return list(obj)
118    elif isinstance(obj, Decimal):
119        return str(obj)
120    elif type(obj).__name__ == "RelatedManager":
121        return [x.pk for x in obj.all()]
122    elif hasattr( obj, '__dict__') and isinstance(obj, Model):
123        d = obj.__dict__
124        nd = dict(d)
125        for di in d.keys():
126            if di.startswith("_"):
127                del nd[di]
128            elif isinstance(d[di], Model):
129                nd[di] = d[di].pk
130            elif isinstance(d[di], int) and hasattr(obj, "get_%s_display" % di):
131                nd[di] = getattr(obj, "get_%s_display" % di)()
132        return nd
133    elif isinstance( obj, type(lambda x:x)):
134        import inspect
135        return inspect.getsourcelines(obj)[0]
136    else:
137        raise TypeError("Unserializable object %s (%s) of type %s" % ( obj, dir(obj), type(obj)))
138
139
140def _lv_to_dict(prj, x = None):
141    if x is None:
142        def wrapper(x):
143            return _lv_to_dict(prj, x)
144        return wrapper
145
146    return {"id": x.pk,
147            "name": x.layer.name,
148            "tooltip": "%s | %s" % (x.layer.vcs_url,x.get_vcs_reference()),
149            "detail": "(%s" % x.layer.vcs_url + (")" if x.release is None else " | "+x.get_vcs_reference()+")"),
150            "giturl": x.layer.vcs_url,
151            "layerdetailurl" : reverse('layerdetails', args=(prj.id,x.pk)),
152            "revision" : x.get_vcs_reference(),
153           }
154
155
156def _build_page_range(paginator, index = 1):
157    try:
158        page = paginator.page(index)
159    except PageNotAnInteger:
160        page = paginator.page(1)
161    except  EmptyPage:
162        page = paginator.page(paginator.num_pages)
163
164
165    page.page_range = [page.number]
166    crt_range = 0
167    for i in range(1,5):
168        if (page.number + i) <= paginator.num_pages:
169            page.page_range = page.page_range + [ page.number + i]
170            crt_range +=1
171        if (page.number - i) > 0:
172            page.page_range =  [page.number -i] + page.page_range
173            crt_range +=1
174        if crt_range == 4:
175            break
176    return page
177
178
179def _verify_parameters(g, mandatory_parameters):
180    miss = []
181    for mp in mandatory_parameters:
182        if not mp in g:
183            miss.append(mp)
184    if len(miss):
185        return miss
186    return None
187
188def _redirect_parameters(view, g, mandatory_parameters, *args, **kwargs):
189    try:
190        from urllib import unquote, urlencode
191    except ImportError:
192        from urllib.parse import unquote, urlencode
193    url = reverse(view, kwargs=kwargs)
194    params = {}
195    for i in g:
196        params[i] = g[i]
197    for i in mandatory_parameters:
198        if not i in params:
199            params[i] = unquote(str(mandatory_parameters[i]))
200
201    return redirect(url + "?%s" % urlencode(params), permanent = False, **kwargs)
202
203class RedirectException(Exception):
204    def __init__(self, view, g, mandatory_parameters, *args, **kwargs):
205        super(RedirectException, self).__init__()
206        self.view = view
207        self.g = g
208        self.mandatory_parameters = mandatory_parameters
209        self.oargs  = args
210        self.okwargs = kwargs
211
212    def get_redirect_response(self):
213        return _redirect_parameters(self.view, self.g, self.mandatory_parameters, self.oargs, **self.okwargs)
214
215FIELD_SEPARATOR = ":"
216AND_VALUE_SEPARATOR = "!"
217OR_VALUE_SEPARATOR = "|"
218DESCENDING = "-"
219
220def __get_q_for_val(name, value):
221    if "OR" in value or "AND" in value:
222        result = None
223        for x in value.split("OR"):
224             x = __get_q_for_val(name, x)
225             result = result | x if result else x
226        return result
227    if "AND" in value:
228        result = None
229        for x in value.split("AND"):
230            x = __get_q_for_val(name, x)
231            result = result & x if result else x
232        return result
233    if value.startswith("NOT"):
234        value = value[3:]
235        if value == 'None':
236            value = None
237        kwargs = { name : value }
238        return ~Q(**kwargs)
239    else:
240        if value == 'None':
241            value = None
242        kwargs = { name : value }
243        return Q(**kwargs)
244
245def _get_filtering_query(filter_string):
246
247    search_terms = filter_string.split(FIELD_SEPARATOR)
248    and_keys = search_terms[0].split(AND_VALUE_SEPARATOR)
249    and_values = search_terms[1].split(AND_VALUE_SEPARATOR)
250
251    and_query = None
252    for kv in zip(and_keys, and_values):
253        or_keys = kv[0].split(OR_VALUE_SEPARATOR)
254        or_values = kv[1].split(OR_VALUE_SEPARATOR)
255        query = None
256        for key, val in zip(or_keys, or_values):
257            x = __get_q_for_val(key, val)
258            query = query | x if query else x
259
260        and_query = and_query & query if and_query else query
261
262    return and_query
263
264def _get_toggle_order(request, orderkey, toggle_reverse = False):
265    if toggle_reverse:
266        return "%s:+" % orderkey if request.GET.get('orderby', "") == "%s:-" % orderkey else "%s:-" % orderkey
267    else:
268        return "%s:-" % orderkey if request.GET.get('orderby', "") == "%s:+" % orderkey else "%s:+" % orderkey
269
270def _get_toggle_order_icon(request, orderkey):
271    if request.GET.get('orderby', "") == "%s:+"%orderkey:
272        return "down"
273    elif request.GET.get('orderby', "") == "%s:-"%orderkey:
274        return "up"
275    else:
276        return None
277
278# we check that the input comes in a valid form that we can recognize
279def _validate_input(field_input, model):
280
281    invalid = None
282
283    if field_input:
284        field_input_list = field_input.split(FIELD_SEPARATOR)
285
286        # Check we have only one colon
287        if len(field_input_list) != 2:
288            invalid = "We have an invalid number of separators: " + field_input + " -> " + str(field_input_list)
289            return None, invalid
290
291        # Check we have an equal number of terms both sides of the colon
292        if len(field_input_list[0].split(AND_VALUE_SEPARATOR)) != len(field_input_list[1].split(AND_VALUE_SEPARATOR)):
293            invalid = "Not all arg names got values"
294            return None, invalid + str(field_input_list)
295
296        # Check we are looking for a valid field
297        valid_fields = [f.name for f in model._meta.get_fields()]
298        for field in field_input_list[0].split(AND_VALUE_SEPARATOR):
299            if True in [field.startswith(x) for x in valid_fields]:
300                break
301        else:
302           return None, (field, valid_fields)
303
304    return field_input, invalid
305
306# uses search_allowed_fields in orm/models.py to create a search query
307# for these fields with the supplied input text
308def _get_search_results(search_term, queryset, model):
309    search_object = None
310    for st in search_term.split(" "):
311        queries = None
312        for field in model.search_allowed_fields:
313            query = Q(**{field + '__icontains': st})
314            queries = queries | query if queries else query
315
316        search_object = search_object & queries if search_object else queries
317    queryset = queryset.filter(search_object)
318
319    return queryset
320
321
322# function to extract the search/filter/ordering parameters from the request
323# it uses the request and the model to validate input for the filter and orderby values
324def _search_tuple(request, model):
325    ordering_string, invalid = _validate_input(request.GET.get('orderby', ''), model)
326    if invalid:
327        raise BaseException("Invalid ordering model:" + str(model) + str(invalid))
328
329    filter_string, invalid = _validate_input(request.GET.get('filter', ''), model)
330    if invalid:
331        raise BaseException("Invalid filter " + str(invalid))
332
333    search_term = request.GET.get('search', '')
334    return (filter_string, search_term, ordering_string)
335
336
337# returns a lazy-evaluated queryset for a filter/search/order combination
338def _get_queryset(model, queryset, filter_string, search_term, ordering_string, ordering_secondary=''):
339    if filter_string:
340        filter_query = _get_filtering_query(filter_string)
341        queryset = queryset.filter(filter_query)
342    else:
343        queryset = queryset.all()
344
345    if search_term:
346        queryset = _get_search_results(search_term, queryset, model)
347
348    if ordering_string:
349        column, order = ordering_string.split(':')
350        if column == re.sub('-','',ordering_secondary):
351            ordering_secondary=''
352        if order.lower() == DESCENDING:
353            column = '-' + column
354        if ordering_secondary:
355            queryset = queryset.order_by(column, ordering_secondary)
356        else:
357            queryset = queryset.order_by(column)
358
359    # insure only distinct records (e.g. from multiple search hits) are returned
360    return queryset.distinct()
361
362# returns the value of entries per page and the name of the applied sorting field.
363# if the value is given explicitly as a GET parameter it will be the first selected,
364# otherwise the cookie value will be used.
365def _get_parameters_values(request, default_count, default_order):
366    current_url = resolve(request.path_info).url_name
367    pagesize = request.GET.get('count', request.session.get('%s_count' % current_url, default_count))
368    orderby = request.GET.get('orderby', request.session.get('%s_orderby' % current_url, default_order))
369    return (pagesize, orderby)
370
371
372# set cookies for parameters. this is usefull in case parameters are set
373# manually from the GET values of the link
374def _set_parameters_values(pagesize, orderby, request):
375    current_url = resolve(request.path_info).url_name
376    request.session['%s_count' % current_url] = pagesize
377    request.session['%s_orderby' % current_url] =orderby
378
379# date range: normalize GUI's dd/mm/yyyy to date object
380def _normalize_input_date(date_str,default):
381    date_str=re.sub('/', '-', date_str)
382    # accept dd/mm/yyyy to d/m/yy
383    try:
384        date_in = datetime.strptime(date_str, "%d-%m-%Y")
385    except ValueError:
386        # courtesy try with two digit year
387        try:
388            date_in = datetime.strptime(date_str, "%d-%m-%y")
389        except ValueError:
390            return default
391    date_in = date_in.replace(tzinfo=default.tzinfo)
392    return date_in
393
394# convert and normalize any received date range filter, for example:
395# "completed_on__gte!completed_on__lt:01/03/2015!02/03/2015_daterange" to
396# "completed_on__gte!completed_on__lt:2015-03-01!2015-03-02"
397def _modify_date_range_filter(filter_string):
398    # was the date range radio button selected?
399    if 0 >  filter_string.find('_daterange'):
400        return filter_string,''
401    # normalize GUI dates to database format
402    filter_string = filter_string.replace('_daterange','').replace(':','!');
403    filter_list = filter_string.split('!');
404    if 4 != len(filter_list):
405        return filter_string
406    today = timezone.localtime(timezone.now())
407    date_id = filter_list[1]
408    date_from = _normalize_input_date(filter_list[2],today)
409    date_to = _normalize_input_date(filter_list[3],today)
410    # swap dates if manually set dates are out of order
411    if  date_to < date_from:
412        date_to,date_from = date_from,date_to
413    # convert to strings, make 'date_to' inclusive by moving to begining of next day
414    date_from_str = date_from.strftime("%Y-%m-%d")
415    date_to_str = (date_to+timedelta(days=1)).strftime("%Y-%m-%d")
416    filter_string=filter_list[0]+'!'+filter_list[1]+':'+date_from_str+'!'+date_to_str
417    daterange_selected = re.sub('__.*','', date_id)
418    return filter_string,daterange_selected
419
420def _add_daterange_context(queryset_all, request, daterange_list):
421    # calculate the exact begining of local today and yesterday
422    today_begin = timezone.localtime(timezone.now())
423    yesterday_begin = today_begin - timedelta(days=1)
424    # add daterange persistent
425    context_date = {}
426    context_date['last_date_from'] = request.GET.get('last_date_from',timezone.localtime(timezone.now()).strftime("%d/%m/%Y"))
427    context_date['last_date_to'  ] = request.GET.get('last_date_to'  ,context_date['last_date_from'])
428    # calculate the date ranges, avoid second sort for 'created'
429    # fetch the respective max range from the database
430    context_date['daterange_filter']=''
431    for key in daterange_list:
432        queryset_key = queryset_all.order_by(key)
433        try:
434            context_date['dateMin_'+key]=timezone.localtime(getattr(queryset_key.first(),key)).strftime("%d/%m/%Y")
435        except AttributeError:
436            context_date['dateMin_'+key]=timezone.localtime(timezone.now())
437        try:
438            context_date['dateMax_'+key]=timezone.localtime(getattr(queryset_key.last(),key)).strftime("%d/%m/%Y")
439        except AttributeError:
440            context_date['dateMax_'+key]=timezone.localtime(timezone.now())
441    return context_date,today_begin,yesterday_begin
442
443
444##
445# build dashboard for a single build, coming in as argument
446# Each build may contain multiple targets and each target
447# may generate multiple image files. display them all.
448#
449def builddashboard( request, build_id ):
450    template = "builddashboard.html"
451    if Build.objects.filter( pk=build_id ).count( ) == 0 :
452        return redirect( builds )
453    build = Build.objects.get( pk = build_id );
454    layerVersionId = Layer_Version.objects.filter( build = build_id );
455    recipeCount = Recipe.objects.filter( layer_version__id__in = layerVersionId ).count( );
456    tgts = Target.objects.filter( build_id = build_id ).order_by( 'target' );
457
458    # set up custom target list with computed package and image data
459    targets = []
460    ntargets = 0
461
462    # True if at least one target for this build has an SDK artifact
463    # or image file
464    has_artifacts = False
465
466    for t in tgts:
467        elem = {}
468        elem['target'] = t
469
470        target_has_images = False
471        image_files = []
472
473        npkg = 0
474        pkgsz = 0
475        package = None
476        # Chunk the query to avoid "too many SQL variables" error
477        package_set = t.target_installed_package_set.all()
478        package_set_len = len(package_set)
479        for ps_start in range(0,package_set_len,500):
480            ps_stop = min(ps_start+500,package_set_len)
481            for package in Package.objects.filter(id__in = [x.package_id for x in package_set[ps_start:ps_stop]]):
482                pkgsz = pkgsz + package.size
483                if package.installed_name:
484                    npkg = npkg + 1
485        elem['npkg'] = npkg
486        elem['pkgsz'] = pkgsz
487        ti = Target_Image_File.objects.filter(target_id = t.id)
488        for i in ti:
489            ndx = i.file_name.rfind('/')
490            if ndx < 0:
491                ndx = 0;
492            f = i.file_name[ndx + 1:]
493            image_files.append({
494                'id': i.id,
495                'path': f,
496                'size': i.file_size,
497                'suffix': i.suffix
498            })
499        if len(image_files) > 0:
500            target_has_images = True
501        elem['targetHasImages'] = target_has_images
502
503        elem['imageFiles'] = image_files
504        elem['target_kernel_artifacts'] = t.targetkernelfile_set.all()
505
506        target_sdk_files = t.targetsdkfile_set.all()
507        target_sdk_artifacts_count = target_sdk_files.count()
508        elem['target_sdk_artifacts_count'] = target_sdk_artifacts_count
509        elem['target_sdk_artifacts'] = target_sdk_files
510
511        if target_has_images or target_sdk_artifacts_count > 0:
512            has_artifacts = True
513
514        targets.append(elem)
515
516    ##
517    # how many packages in this build - ignore anonymous ones
518    #
519
520    packageCount = 0
521    packages = Package.objects.filter( build_id = build_id )
522    for p in packages:
523        if ( p.installed_name ):
524            packageCount = packageCount + 1
525
526    logmessages = list(LogMessage.objects.filter( build = build_id ))
527
528    context = {
529            'build'           : build,
530            'project'         : build.project,
531            'hasArtifacts'    : has_artifacts,
532            'ntargets'        : ntargets,
533            'targets'         : targets,
534            'recipecount'     : recipeCount,
535            'packagecount'    : packageCount,
536            'logmessages'     : logmessages,
537    }
538    return toaster_render( request, template, context )
539
540
541
542def generateCoveredList2( revlist = None ):
543    if not revlist:
544        revlist = []
545    covered_list =  [ x for x in revlist if x.outcome == Task.OUTCOME_COVERED ]
546    while len(covered_list):
547        revlist =  [ x for x in revlist if x.outcome != Task.OUTCOME_COVERED ]
548        if len(revlist) > 0:
549            return revlist
550
551        newlist = _find_task_revdep_list(covered_list)
552
553        revlist = list(set(revlist + newlist))
554        covered_list =  [ x for x in revlist if x.outcome == Task.OUTCOME_COVERED ]
555    return revlist
556
557def task( request, build_id, task_id ):
558    template = "task.html"
559    tasks_list = Task.objects.filter( pk=task_id )
560    if tasks_list.count( ) == 0:
561        return redirect( builds )
562    task_object = tasks_list[ 0 ];
563    dependencies = sorted(
564        _find_task_dep( task_object ),
565        key=lambda t:'%s_%s %s'%(t.recipe.name, t.recipe.version, t.task_name))
566    reverse_dependencies = sorted(
567        _find_task_revdep( task_object ),
568        key=lambda t:'%s_%s %s'%( t.recipe.name, t.recipe.version, t.task_name ))
569    coveredBy = '';
570    if ( task_object.outcome == Task.OUTCOME_COVERED ):
571#        _list = generateCoveredList( task )
572        coveredBy = sorted(generateCoveredList2( _find_task_revdep( task_object ) ), key = lambda x: x.recipe.name)
573    log_head = ''
574    log_body = ''
575    if task_object.outcome == task_object.OUTCOME_FAILED:
576        pass
577
578    uri_list= [ ]
579    variables = Variable.objects.filter(build=build_id)
580    v=variables.filter(variable_name='SSTATE_DIR')
581    if v.count() > 0:
582        uri_list.append(v[0].variable_value)
583    v=variables.filter(variable_name='SSTATE_MIRRORS')
584    if (v.count() > 0):
585        for mirror in v[0].variable_value.split('\\n'):
586            s=re.sub('.* ','',mirror.strip(' \t\n\r'))
587            if len(s):
588                uri_list.append(s)
589
590    context = {
591            'build'           : Build.objects.filter( pk = build_id )[ 0 ],
592            'object'          : task_object,
593            'task'            : task_object,
594            'covered_by'      : coveredBy,
595            'deps'            : dependencies,
596            'rdeps'           : reverse_dependencies,
597            'log_head'        : log_head,
598            'log_body'        : log_body,
599            'showing_matches' : False,
600            'uri_list'        : uri_list,
601            'task_in_tasks_table_pg':  int(task_object.order / 25) + 1
602    }
603    if request.GET.get( 'show_matches', "" ):
604        context[ 'showing_matches' ] = True
605        context[ 'matching_tasks' ] = Task.objects.filter(
606            sstate_checksum=task_object.sstate_checksum ).filter(
607            build__completed_on__lt=task_object.build.completed_on).exclude(
608            order__isnull=True).exclude(outcome=Task.OUTCOME_NA).order_by('-build__completed_on')
609
610    return toaster_render( request, template, context )
611
612def recipe(request, build_id, recipe_id, active_tab="1"):
613    template = "recipe.html"
614    if Recipe.objects.filter(pk=recipe_id).count() == 0 :
615        return redirect(builds)
616
617    recipe_object = Recipe.objects.get(pk=recipe_id)
618    layer_version = Layer_Version.objects.get(pk=recipe_object.layer_version_id)
619    layer  = Layer.objects.get(pk=layer_version.layer_id)
620    tasks_list  = Task.objects.filter(recipe_id = recipe_id, build_id = build_id).exclude(order__isnull=True).exclude(task_name__endswith='_setscene').exclude(outcome=Task.OUTCOME_NA)
621    package_count = Package.objects.filter(recipe_id = recipe_id).filter(build_id = build_id).filter(size__gte=0).count()
622
623    if active_tab != '1' and active_tab != '3' and active_tab != '4' :
624        active_tab = '1'
625    tab_states = {'1': '', '3': '', '4': ''}
626    tab_states[active_tab] = 'active'
627
628    context = {
629            'build'   : Build.objects.get(pk=build_id),
630            'object'  : recipe_object,
631            'layer_version' : layer_version,
632            'layer'   : layer,
633            'tasks'   : tasks_list,
634            'package_count' : package_count,
635            'tab_states' : tab_states,
636    }
637    return toaster_render(request, template, context)
638
639def recipe_packages(request, build_id, recipe_id):
640    template = "recipe_packages.html"
641    if Recipe.objects.filter(pk=recipe_id).count() == 0 :
642        return redirect(builds)
643
644    (pagesize, orderby) = _get_parameters_values(request, 10, 'name:+')
645    mandatory_parameters = { 'count': pagesize,  'page' : 1, 'orderby': orderby }
646    retval = _verify_parameters( request.GET, mandatory_parameters )
647    if retval:
648        return _redirect_parameters( 'recipe_packages', request.GET, mandatory_parameters, build_id = build_id, recipe_id = recipe_id)
649    (filter_string, search_term, ordering_string) = _search_tuple(request, Package)
650
651    recipe_object = Recipe.objects.get(pk=recipe_id)
652    queryset = Package.objects.filter(recipe_id = recipe_id).filter(build_id = build_id).filter(size__gte=0)
653    package_count = queryset.count()
654    queryset = _get_queryset(Package, queryset, filter_string, search_term, ordering_string, 'name')
655
656    packages = _build_page_range(Paginator(queryset, pagesize),request.GET.get('page', 1))
657
658    context = {
659            'build'   : Build.objects.get(pk=build_id),
660            'recipe'  : recipe_object,
661            'objects'  : packages,
662            'object_count' : package_count,
663            'tablecols':[
664                {
665                    'name':'Package',
666                    'orderfield': _get_toggle_order(request,"name"),
667                    'ordericon': _get_toggle_order_icon(request,"name"),
668                    'orderkey': "name",
669                },
670                {
671                    'name':'Version',
672                },
673                {
674                    'name':'Size',
675                    'orderfield': _get_toggle_order(request,"size", True),
676                    'ordericon': _get_toggle_order_icon(request,"size"),
677                    'orderkey': 'size',
678                    'dclass': 'sizecol span2',
679                },
680           ]
681       }
682    response = toaster_render(request, template, context)
683    _set_parameters_values(pagesize, orderby, request)
684    return response
685
686from django.http import HttpResponse
687@log_view_mixin
688def xhr_dirinfo(request, build_id, target_id):
689    top = request.GET.get('start', '/')
690    return HttpResponse(_get_dir_entries(build_id, target_id, top), content_type = "application/json")
691
692from django.utils.functional import Promise
693from django.utils.encoding import force_str
694class LazyEncoder(json.JSONEncoder):
695    def default(self, obj):
696        if isinstance(obj, Promise):
697            return force_str(obj)
698        return super(LazyEncoder, self).default(obj)
699
700from toastergui.templatetags.projecttags import filtered_filesizeformat
701def _get_dir_entries(build_id, target_id, start):
702    node_str = {
703        Target_File.ITYPE_REGULAR   : '-',
704        Target_File.ITYPE_DIRECTORY : 'd',
705        Target_File.ITYPE_SYMLINK   : 'l',
706        Target_File.ITYPE_SOCKET    : 's',
707        Target_File.ITYPE_FIFO      : 'p',
708        Target_File.ITYPE_CHARACTER : 'c',
709        Target_File.ITYPE_BLOCK     : 'b',
710    }
711    response = []
712    objects  = Target_File.objects.filter(target__exact=target_id, directory__path=start)
713    target_packages = Target_Installed_Package.objects.filter(target__exact=target_id).values_list('package_id', flat=True)
714    for o in objects:
715        # exclude root inode '/'
716        if o.path == '/':
717            continue
718        try:
719            entry = {}
720            entry['parent'] = start
721            entry['name'] = os.path.basename(o.path)
722            entry['fullpath'] = o.path
723
724            # set defaults, not all dentries have packages
725            entry['installed_package'] = None
726            entry['package_id'] = None
727            entry['package'] = None
728            entry['link_to'] = None
729            if o.inodetype == Target_File.ITYPE_DIRECTORY:
730                entry['isdir'] = 1
731                # is there content in directory
732                entry['childcount'] = Target_File.objects.filter(target__exact=target_id, directory__path=o.path).all().count()
733            else:
734                entry['isdir'] = 0
735
736                # resolve the file to get the package from the resolved file
737                resolved_id = o.sym_target_id
738                resolved_path = o.path
739                if target_packages.count():
740                    while resolved_id != "" and resolved_id is not None:
741                        tf = Target_File.objects.get(pk=resolved_id)
742                        resolved_path = tf.path
743                        resolved_id = tf.sym_target_id
744
745                    thisfile=Package_File.objects.all().filter(path__exact=resolved_path, package_id__in=target_packages)
746                    if thisfile.count():
747                        p = Package.objects.get(pk=thisfile[0].package_id)
748                        entry['installed_package'] = p.installed_name
749                        entry['package_id'] = str(p.id)
750                        entry['package'] = p.name
751                # don't use resolved path from above, show immediate link-to
752                if o.sym_target_id != "" and o.sym_target_id is not None:
753                    entry['link_to'] = Target_File.objects.get(pk=o.sym_target_id).path
754            entry['size'] = filtered_filesizeformat(o.size)
755            if entry['link_to'] is not None:
756                entry['permission'] = node_str[o.inodetype] + o.permission
757            else:
758                entry['permission'] = node_str[o.inodetype] + o.permission
759            entry['owner'] = o.owner
760            entry['group'] = o.group
761            response.append(entry)
762
763        except Exception as e:
764            print("Exception ", e)
765            traceback.print_exc()
766
767    # sort by directories first, then by name
768    rsorted = sorted(response, key=lambda entry :  entry['name'])
769    rsorted = sorted(rsorted, key=lambda entry :  entry['isdir'], reverse=True)
770    return json.dumps(rsorted, cls=LazyEncoder).replace('</', '<\\/')
771
772def dirinfo(request, build_id, target_id, file_path=None):
773    template = "dirinfo.html"
774    objects = _get_dir_entries(build_id, target_id, '/')
775    packages_sum = Package.objects.filter(id__in=Target_Installed_Package.objects.filter(target_id=target_id).values('package_id')).aggregate(Sum('installed_size'))
776    dir_list = None
777    if file_path is not None:
778        """
779        Link from the included package detail file list page and is
780        requesting opening the dir info to a specific file path.
781        Provide the list of directories to expand and the full path to
782        highlight in the page.
783        """
784        # Aassume target's path separator matches host's, that is, os.sep
785        sep = os.sep
786        dir_list = []
787        head = file_path
788        while head != sep:
789            (head, tail) = os.path.split(head)
790            if head != sep:
791                dir_list.insert(0, head)
792
793    build = Build.objects.get(pk=build_id)
794
795    context = { 'build': build,
796                'project': build.project,
797                'target': Target.objects.get(pk=target_id),
798                'packages_sum': packages_sum['installed_size__sum'],
799                'objects': objects,
800                'dir_list': dir_list,
801                'file_path': file_path,
802              }
803    return toaster_render(request, template, context)
804
805def _find_task_dep(task_object):
806    tdeps = Task_Dependency.objects.filter(task=task_object).filter(depends_on__order__gt=0)
807    tdeps = tdeps.exclude(depends_on__outcome=Task.OUTCOME_NA).select_related("depends_on")
808    return [x.depends_on for x in tdeps]
809
810def _find_task_revdep(task_object):
811    tdeps = Task_Dependency.objects.filter(depends_on=task_object).filter(task__order__gt=0)
812    tdeps = tdeps.exclude(task__outcome = Task.OUTCOME_NA).select_related("task", "task__recipe", "task__build")
813
814    # exclude self-dependencies to prevent infinite dependency loop
815    # in generateCoveredList2()
816    tdeps = tdeps.exclude(task=task_object)
817
818    return [tdep.task for tdep in tdeps]
819
820def _find_task_revdep_list(tasklist):
821    tdeps = Task_Dependency.objects.filter(depends_on__in=tasklist).filter(task__order__gt=0)
822    tdeps = tdeps.exclude(task__outcome=Task.OUTCOME_NA).select_related("task", "task__recipe", "task__build")
823
824    # exclude self-dependencies to prevent infinite dependency loop
825    # in generateCoveredList2()
826    tdeps = tdeps.exclude(task=F('depends_on'))
827
828    return [tdep.task for tdep in tdeps]
829
830def _find_task_provider(task_object):
831    task_revdeps = _find_task_revdep(task_object)
832    for tr in task_revdeps:
833        if tr.outcome != Task.OUTCOME_COVERED:
834            return tr
835    for tr in task_revdeps:
836        trc = _find_task_provider(tr)
837        if trc is not None:
838            return trc
839    return None
840
841def configuration(request, build_id):
842    template = 'configuration.html'
843
844    var_names = ('BB_VERSION', 'BUILD_SYS', 'NATIVELSBSTRING', 'TARGET_SYS',
845                 'MACHINE', 'DISTRO', 'DISTRO_VERSION', 'TUNE_FEATURES', 'TARGET_FPU')
846    context = dict(Variable.objects.filter(build=build_id, variable_name__in=var_names)\
847                                           .values_list('variable_name', 'variable_value'))
848    build = Build.objects.get(pk=build_id)
849    context.update({'objectname': 'configuration',
850                    'object_search_display':'variables',
851                    'filter_search_display':'variables',
852                    'build': build,
853                    'project': build.project,
854                    'targets': Target.objects.filter(build=build_id)})
855    return toaster_render(request, template, context)
856
857
858def configvars(request, build_id):
859    template = 'configvars.html'
860    (pagesize, orderby) = _get_parameters_values(request, 100, 'variable_name:+')
861    mandatory_parameters = { 'count': pagesize,  'page' : 1, 'orderby' : orderby, 'filter' : 'description__regex:.+' }
862    retval = _verify_parameters( request.GET, mandatory_parameters )
863    (filter_string, search_term, ordering_string) = _search_tuple(request, Variable)
864    if retval:
865        # if new search, clear the default filter
866        if search_term and len(search_term):
867            mandatory_parameters['filter']=''
868        return _redirect_parameters( 'configvars', request.GET, mandatory_parameters, build_id = build_id)
869
870    queryset = Variable.objects.filter(build=build_id).exclude(variable_name__istartswith='B_').exclude(variable_name__istartswith='do_')
871    queryset_with_search =  _get_queryset(Variable, queryset, None, search_term, ordering_string, 'variable_name').exclude(variable_value='',vhistory__file_name__isnull=True)
872    queryset = _get_queryset(Variable, queryset, filter_string, search_term, ordering_string, 'variable_name')
873    # remove records where the value is empty AND there are no history files
874    queryset = queryset.exclude(variable_value='',vhistory__file_name__isnull=True)
875
876    variables = _build_page_range(Paginator(queryset, pagesize), request.GET.get('page', 1))
877
878    # show all matching files (not just the last one)
879    file_filter= search_term + ":"
880    if filter_string.find('/conf/') > 0:
881        file_filter += 'conf/(local|bblayers).conf'
882    if filter_string.find('conf/machine/') > 0:
883        file_filter += 'conf/machine/'
884    if filter_string.find('conf/distro/') > 0:
885        file_filter += 'conf/distro/'
886    if filter_string.find('/bitbake.conf') > 0:
887        file_filter += '/bitbake.conf'
888    build_dir=re.sub("/tmp/log/.*","",Build.objects.get(pk=build_id).cooker_log_path)
889
890    build = Build.objects.get(pk=build_id)
891
892    context = {
893                'objectname': 'configvars',
894                'object_search_display':'BitBake variables',
895                'filter_search_display':'variables',
896                'file_filter': file_filter,
897                'build': build,
898                'project': build.project,
899                'objects' : variables,
900                'total_count':queryset_with_search.count(),
901                'default_orderby' : 'variable_name:+',
902                'search_term':search_term,
903            # Specifies the display of columns for the table, appearance in "Edit columns" box, toggling default show/hide, and specifying filters for columns
904                'tablecols' : [
905                {'name': 'Variable',
906                 'qhelp': "BitBake is a generic task executor that considers a list of tasks with dependencies and handles metadata that consists of variables in a certain format that get passed to the tasks",
907                 'orderfield': _get_toggle_order(request, "variable_name"),
908                 'ordericon':_get_toggle_order_icon(request, "variable_name"),
909                },
910                {'name': 'Value',
911                 'qhelp': "The value assigned to the variable",
912                },
913                {'name': 'Set in file',
914                 'qhelp': "The last configuration file that touched the variable value",
915                 'clclass': 'file', 'hidden' : 0,
916                 'orderkey' : 'vhistory__file_name',
917                 'filter' : {
918                    'class' : 'vhistory__file_name',
919                    'label': 'Show:',
920                    'options' : [
921                               ('Local configuration variables', 'vhistory__file_name__contains:'+build_dir+'/conf/',queryset_with_search.filter(vhistory__file_name__contains=build_dir+'/conf/').count(), 'Select this filter to see variables set by the <code>local.conf</code> and <code>bblayers.conf</code> configuration files inside the <code>/build/conf/</code> directory'),
922                               ('Machine configuration variables', 'vhistory__file_name__contains:conf/machine/',queryset_with_search.filter(vhistory__file_name__contains='conf/machine').count(), 'Select this filter to see variables set by the configuration file(s) inside your layers <code>/conf/machine/</code> directory'),
923                               ('Distro configuration variables', 'vhistory__file_name__contains:conf/distro/',queryset_with_search.filter(vhistory__file_name__contains='conf/distro').count(), 'Select this filter to see variables set by the configuration file(s) inside your layers <code>/conf/distro/</code> directory'),
924                               ('Layer configuration variables', 'vhistory__file_name__contains:conf/layer.conf',queryset_with_search.filter(vhistory__file_name__contains='conf/layer.conf').count(), 'Select this filter to see variables set by the <code>layer.conf</code> configuration file inside your layers'),
925                               ('bitbake.conf variables', 'vhistory__file_name__contains:/bitbake.conf',queryset_with_search.filter(vhistory__file_name__contains='/bitbake.conf').count(), 'Select this filter to see variables set by the <code>bitbake.conf</code> configuration file'),
926                               ]
927                             },
928                },
929                {'name': 'Description',
930                 'qhelp': "A brief explanation of the variable",
931                 'clclass': 'description', 'hidden' : 0,
932                 'dclass': "span4",
933                 'filter' : {
934                    'class' : 'description',
935                    'label': 'Show:',
936                    'options' : [
937                               ('Variables with description', 'description__regex:.+', queryset_with_search.filter(description__regex='.+').count(), 'We provide descriptions for the most common BitBake variables. The list of descriptions lives in <code>meta/conf/documentation.conf</code>'),
938                               ]
939                            },
940                },
941                ],
942            }
943
944    response = toaster_render(request, template, context)
945    _set_parameters_values(pagesize, orderby, request)
946    return response
947
948def bfile(request, build_id, package_id):
949    template = 'bfile.html'
950    files = Package_File.objects.filter(package = package_id)
951    build = Build.objects.get(pk=build_id)
952    context = {
953        'build': build,
954        'project': build.project,
955        'objects' : files
956    }
957    return toaster_render(request, template, context)
958
959
960# A set of dependency types valid for both included and built package views
961OTHER_DEPENDS_BASE = [
962    Package_Dependency.TYPE_RSUGGESTS,
963    Package_Dependency.TYPE_RPROVIDES,
964    Package_Dependency.TYPE_RREPLACES,
965    Package_Dependency.TYPE_RCONFLICTS,
966    ]
967
968# value for invalid row id
969INVALID_KEY = -1
970
971"""
972Given a package id, target_id retrieves two sets of this image and package's
973dependencies.  The return value is a dictionary consisting of two other
974lists: a list of 'runtime' dependencies, that is, having RDEPENDS
975values in source package's recipe, and a list of other dependencies, that is
976the list of possible recipe variables as found in OTHER_DEPENDS_BASE plus
977the RRECOMMENDS or TRECOMMENDS value.
978The lists are built in the sort order specified for the package runtime
979dependency views.
980"""
981def _get_package_dependencies(package_id, target_id = INVALID_KEY):
982    runtime_deps = []
983    other_deps = []
984    other_depends_types = OTHER_DEPENDS_BASE
985
986    if target_id != INVALID_KEY :
987        rdepends_type = Package_Dependency.TYPE_TRDEPENDS
988        other_depends_types +=  [Package_Dependency.TYPE_TRECOMMENDS]
989    else :
990        rdepends_type = Package_Dependency.TYPE_RDEPENDS
991        other_depends_types += [Package_Dependency.TYPE_RRECOMMENDS]
992
993    package = Package.objects.get(pk=package_id)
994    if target_id != INVALID_KEY :
995        alldeps = package.package_dependencies_source.filter(target_id__exact = target_id)
996    else :
997        alldeps = package.package_dependencies_source.all()
998    for idep in alldeps:
999        dep_package = Package.objects.get(pk=idep.depends_on_id)
1000        dep_entry = Package_Dependency.DEPENDS_DICT[idep.dep_type]
1001        if dep_package.version == '' :
1002            version = ''
1003        else :
1004            version = dep_package.version + "-" + dep_package.revision
1005        installed = False
1006        if target_id != INVALID_KEY :
1007            if Target_Installed_Package.objects.filter(target_id__exact = target_id, package_id__exact = dep_package.id).count() > 0:
1008                installed = True
1009        dep =   {
1010                'name' : dep_package.name,
1011                'version' : version,
1012                'size' : dep_package.size,
1013                'dep_type' : idep.dep_type,
1014                'dep_type_display' : dep_entry[0].capitalize(),
1015                'dep_type_help' : dep_entry[1] % (dep_package.name, package.name),
1016                'depends_on_id' : dep_package.id,
1017                'installed' : installed,
1018                }
1019
1020        if target_id != INVALID_KEY:
1021                dep['alias'] = _get_package_alias(dep_package)
1022
1023        if idep.dep_type == rdepends_type :
1024            runtime_deps.append(dep)
1025        elif idep.dep_type in other_depends_types :
1026            other_deps.append(dep)
1027
1028    rdep_sorted = sorted(runtime_deps, key=lambda k: k['name'])
1029    odep_sorted = sorted(
1030            sorted(other_deps, key=lambda k: k['name']),
1031            key=lambda k: k['dep_type'])
1032    retvalues = {'runtime_deps' : rdep_sorted, 'other_deps' : odep_sorted}
1033    return retvalues
1034
1035# Return the count of packages dependent on package for this target_id image
1036def _get_package_reverse_dep_count(package, target_id):
1037    return package.package_dependencies_target.filter(target_id__exact=target_id, dep_type__exact = Package_Dependency.TYPE_TRDEPENDS).count()
1038
1039# Return the count of the packages that this package_id is dependent on.
1040# Use one of the two RDEPENDS types, either TRDEPENDS if the package was
1041# installed, or else RDEPENDS if only built.
1042def _get_package_dependency_count(package, target_id, is_installed):
1043    if is_installed :
1044        return package.package_dependencies_source.filter(target_id__exact = target_id,
1045            dep_type__exact = Package_Dependency.TYPE_TRDEPENDS).count()
1046    else :
1047        return package.package_dependencies_source.filter(dep_type__exact = Package_Dependency.TYPE_RDEPENDS).count()
1048
1049def _get_package_alias(package):
1050    alias = package.installed_name
1051    if alias is not None and alias != '' and alias != package.name:
1052        return alias
1053    else:
1054        return ''
1055
1056def _get_fullpackagespec(package):
1057    r = package.name
1058    version_good = package.version is not None and  package.version != ''
1059    revision_good = package.revision is not None and package.revision != ''
1060    if version_good or revision_good:
1061        r += '_'
1062        if version_good:
1063            r += package.version
1064            if revision_good:
1065                r += '-'
1066        if revision_good:
1067            r += package.revision
1068    return r
1069
1070def package_built_detail(request, build_id, package_id):
1071    template = "package_built_detail.html"
1072    if Build.objects.filter(pk=build_id).count() == 0 :
1073        return redirect(builds)
1074
1075    # follow convention for pagination w/ search although not used for this view
1076    queryset = Package_File.objects.filter(package_id__exact=package_id)
1077    (pagesize, orderby) = _get_parameters_values(request, 25, 'path:+')
1078    mandatory_parameters = { 'count': pagesize,  'page' : 1, 'orderby' : orderby }
1079    retval = _verify_parameters( request.GET, mandatory_parameters )
1080    if retval:
1081        return _redirect_parameters( 'package_built_detail', request.GET, mandatory_parameters, build_id = build_id, package_id = package_id)
1082
1083    (filter_string, search_term, ordering_string) = _search_tuple(request, Package_File)
1084    paths = _get_queryset(Package_File, queryset, filter_string, search_term, ordering_string, 'path')
1085
1086    package = Package.objects.get(pk=package_id)
1087    package.fullpackagespec = _get_fullpackagespec(package)
1088    context = {
1089            'build' : Build.objects.get(pk=build_id),
1090            'package' : package,
1091            'dependency_count' : _get_package_dependency_count(package, -1, False),
1092            'objects' : paths,
1093            'tablecols':[
1094                {
1095                    'name':'File',
1096                    'orderfield': _get_toggle_order(request, "path"),
1097                    'ordericon':_get_toggle_order_icon(request, "path"),
1098                },
1099                {
1100                    'name':'Size',
1101                    'orderfield': _get_toggle_order(request, "size", True),
1102                    'ordericon':_get_toggle_order_icon(request, "size"),
1103                    'dclass': 'sizecol span2',
1104                },
1105            ]
1106    }
1107    if paths.all().count() < 2:
1108        context['disable_sort'] = True;
1109
1110    response = toaster_render(request, template, context)
1111    _set_parameters_values(pagesize, orderby, request)
1112    return response
1113
1114def package_built_dependencies(request, build_id, package_id):
1115    template = "package_built_dependencies.html"
1116    if Build.objects.filter(pk=build_id).count() == 0 :
1117         return redirect(builds)
1118
1119    package = Package.objects.get(pk=package_id)
1120    package.fullpackagespec = _get_fullpackagespec(package)
1121    dependencies = _get_package_dependencies(package_id)
1122    context = {
1123            'build' : Build.objects.get(pk=build_id),
1124            'package' : package,
1125            'runtime_deps' : dependencies['runtime_deps'],
1126            'other_deps' :   dependencies['other_deps'],
1127            'dependency_count' : _get_package_dependency_count(package, -1,  False)
1128    }
1129    return toaster_render(request, template, context)
1130
1131
1132def package_included_detail(request, build_id, target_id, package_id):
1133    template = "package_included_detail.html"
1134    if Build.objects.filter(pk=build_id).count() == 0 :
1135        return redirect(builds)
1136
1137    # follow convention for pagination w/ search although not used for this view
1138    (pagesize, orderby) = _get_parameters_values(request, 25, 'path:+')
1139    mandatory_parameters = { 'count': pagesize,  'page' : 1, 'orderby' : orderby }
1140    retval = _verify_parameters( request.GET, mandatory_parameters )
1141    if retval:
1142        return _redirect_parameters( 'package_included_detail', request.GET, mandatory_parameters, build_id = build_id, target_id = target_id, package_id = package_id)
1143    (filter_string, search_term, ordering_string) = _search_tuple(request, Package_File)
1144
1145    queryset = Package_File.objects.filter(package_id__exact=package_id)
1146    paths = _get_queryset(Package_File, queryset, filter_string, search_term, ordering_string, 'path')
1147
1148    package = Package.objects.get(pk=package_id)
1149    package.fullpackagespec = _get_fullpackagespec(package)
1150    package.alias = _get_package_alias(package)
1151    target = Target.objects.get(pk=target_id)
1152    context = {
1153            'build' : Build.objects.get(pk=build_id),
1154            'target'  : target,
1155            'package' : package,
1156            'reverse_count' : _get_package_reverse_dep_count(package, target_id),
1157            'dependency_count' : _get_package_dependency_count(package, target_id, True),
1158            'objects': paths,
1159            'tablecols':[
1160                {
1161                    'name':'File',
1162                    'orderfield': _get_toggle_order(request, "path"),
1163                    'ordericon':_get_toggle_order_icon(request, "path"),
1164                },
1165                {
1166                    'name':'Size',
1167                    'orderfield': _get_toggle_order(request, "size", True),
1168                    'ordericon':_get_toggle_order_icon(request, "size"),
1169                    'dclass': 'sizecol span2',
1170                },
1171            ]
1172    }
1173    if paths.all().count() < 2:
1174        context['disable_sort'] = True
1175    response = toaster_render(request, template, context)
1176    _set_parameters_values(pagesize, orderby, request)
1177    return response
1178
1179def package_included_dependencies(request, build_id, target_id, package_id):
1180    template = "package_included_dependencies.html"
1181    if Build.objects.filter(pk=build_id).count() == 0 :
1182        return redirect(builds)
1183
1184    package = Package.objects.get(pk=package_id)
1185    package.fullpackagespec = _get_fullpackagespec(package)
1186    package.alias = _get_package_alias(package)
1187    target = Target.objects.get(pk=target_id)
1188
1189    dependencies = _get_package_dependencies(package_id, target_id)
1190    context = {
1191            'build' : Build.objects.get(pk=build_id),
1192            'package' : package,
1193            'target' : target,
1194            'runtime_deps' : dependencies['runtime_deps'],
1195            'other_deps' :   dependencies['other_deps'],
1196            'reverse_count' : _get_package_reverse_dep_count(package, target_id),
1197            'dependency_count' : _get_package_dependency_count(package, target_id, True)
1198    }
1199    return toaster_render(request, template, context)
1200
1201def package_included_reverse_dependencies(request, build_id, target_id, package_id):
1202    template = "package_included_reverse_dependencies.html"
1203    if Build.objects.filter(pk=build_id).count() == 0 :
1204        return redirect(builds)
1205
1206    (pagesize, orderby) = _get_parameters_values(request, 25, 'package__name:+')
1207    mandatory_parameters = { 'count': pagesize,  'page' : 1, 'orderby': orderby }
1208    retval = _verify_parameters( request.GET, mandatory_parameters )
1209    if retval:
1210        return _redirect_parameters( 'package_included_reverse_dependencies', request.GET, mandatory_parameters, build_id = build_id, target_id = target_id, package_id = package_id)
1211    (filter_string, search_term, ordering_string) = _search_tuple(request, Package_File)
1212
1213    queryset = Package_Dependency.objects.select_related('depends_on').filter(depends_on=package_id, target_id=target_id, dep_type=Package_Dependency.TYPE_TRDEPENDS)
1214    objects = _get_queryset(Package_Dependency, queryset, filter_string, search_term, ordering_string, 'package__name')
1215
1216    package = Package.objects.get(pk=package_id)
1217    package.fullpackagespec = _get_fullpackagespec(package)
1218    package.alias = _get_package_alias(package)
1219    target = Target.objects.get(pk=target_id)
1220    for o in objects:
1221        if o.package.version != '':
1222            o.package.version += '-' + o.package.revision
1223        o.alias = _get_package_alias(o.package)
1224    context = {
1225            'build' : Build.objects.get(pk=build_id),
1226            'package' : package,
1227            'target' : target,
1228            'objects' : objects,
1229            'reverse_count' : _get_package_reverse_dep_count(package, target_id),
1230            'dependency_count' : _get_package_dependency_count(package, target_id, True),
1231            'tablecols':[
1232                {
1233                    'name':'Package',
1234                    'orderfield': _get_toggle_order(request, "package__name"),
1235                    'ordericon': _get_toggle_order_icon(request, "package__name"),
1236                },
1237                {
1238                    'name':'Version',
1239                },
1240                {
1241                    'name':'Size',
1242                    'orderfield': _get_toggle_order(request, "package__size", True),
1243                    'ordericon': _get_toggle_order_icon(request, "package__size"),
1244                    'dclass': 'sizecol span2',
1245                },
1246            ]
1247    }
1248    if objects.all().count() < 2:
1249        context['disable_sort'] = True
1250    response = toaster_render(request, template, context)
1251    _set_parameters_values(pagesize, orderby, request)
1252    return response
1253
1254def image_information_dir(request, build_id, target_id, packagefile_id):
1255    # stubbed for now
1256    return redirect(builds)
1257    # the context processor that supplies data used across all the pages
1258
1259# a context processor which runs on every request; this provides the
1260# projects and non_cli_projects (i.e. projects created by the user)
1261# variables referred to in templates, which used to determine the
1262# visibility of UI elements like the "New build" button
1263def managedcontextprocessor(request):
1264    projects = Project.objects.all()
1265    ret = {
1266        "projects": projects,
1267        "non_cli_projects": projects.exclude(is_default=True),
1268        "DEBUG" : toastermain.settings.DEBUG,
1269        "TOASTER_BRANCH": toastermain.settings.TOASTER_BRANCH,
1270        "TOASTER_REVISION" : toastermain.settings.TOASTER_REVISION,
1271    }
1272    return ret
1273
1274# REST-based API calls to return build/building status to external Toaster
1275# managers and aggregators via JSON
1276
1277def _json_build_status(build_id,extend):
1278    build_stat = None
1279    try:
1280        build = Build.objects.get( pk = build_id )
1281        build_stat = {}
1282        build_stat['id'] = build.id
1283        build_stat['name'] = build.build_name
1284        build_stat['machine'] = build.machine
1285        build_stat['distro'] = build.distro
1286        build_stat['start'] = build.started_on
1287        # look up target name
1288        target= Target.objects.get( build = build )
1289        if target:
1290            if target.task:
1291                build_stat['target'] = '%s:%s' % (target.target,target.task)
1292            else:
1293                build_stat['target'] = '%s' % (target.target)
1294        else:
1295            build_stat['target'] = ''
1296        # look up project name
1297        project = Project.objects.get( build = build )
1298        if project:
1299            build_stat['project'] = project.name
1300        else:
1301            build_stat['project'] = ''
1302        if Build.IN_PROGRESS == build.outcome:
1303            now = timezone.now()
1304            timediff = now - build.started_on
1305            build_stat['seconds']='%.3f' % timediff.total_seconds()
1306            build_stat['clone']='%d:%d' % (build.repos_cloned,build.repos_to_clone)
1307            build_stat['parse']='%d:%d' % (build.recipes_parsed,build.recipes_to_parse)
1308            tf = Task.objects.filter(build = build)
1309            tfc = tf.count()
1310            if tfc > 0:
1311                tfd = tf.exclude(order__isnull=True).count()
1312            else:
1313                tfd = 0
1314            build_stat['task']='%d:%d' % (tfd,tfc)
1315        else:
1316            build_stat['outcome'] = build.get_outcome_text()
1317            timediff = build.completed_on - build.started_on
1318            build_stat['seconds']='%.3f' % timediff.total_seconds()
1319            build_stat['stop'] = build.completed_on
1320            messages = LogMessage.objects.all().filter(build = build)
1321            errors = len(messages.filter(level=LogMessage.ERROR) |
1322                 messages.filter(level=LogMessage.EXCEPTION) |
1323                 messages.filter(level=LogMessage.CRITICAL))
1324            build_stat['errors'] = errors
1325            warnings = len(messages.filter(level=LogMessage.WARNING))
1326            build_stat['warnings'] = warnings
1327        if extend:
1328            build_stat['cooker_log'] = build.cooker_log_path
1329    except Exception as e:
1330        build_state = str(e)
1331    return build_stat
1332
1333def json_builds(request):
1334    build_table = []
1335    builds = []
1336    try:
1337        builds = Build.objects.exclude(outcome=Build.IN_PROGRESS).order_by("-started_on")
1338        for build in builds:
1339            build_table.append(_json_build_status(build.id,False))
1340    except Exception as e:
1341        build_table = str(e)
1342    return JsonResponse({'builds' : build_table, 'count' : len(builds)})
1343
1344def json_building(request):
1345    build_table = []
1346    builds = []
1347    try:
1348        builds = Build.objects.filter(outcome=Build.IN_PROGRESS).order_by("-started_on")
1349        for build in builds:
1350            build_table.append(_json_build_status(build.id,False))
1351    except Exception as e:
1352        build_table = str(e)
1353    return JsonResponse({'building' : build_table, 'count' : len(builds)})
1354
1355def json_build(request,build_id):
1356    return JsonResponse({'build' : _json_build_status(build_id,True)})
1357
1358
1359import toastermain.settings
1360
1361from orm.models import Project, ProjectLayer, ProjectVariable
1362from bldcontrol.models import  BuildEnvironment
1363
1364# we have a set of functions if we're in managed mode, or
1365# a default "page not available" simple functions for interactive mode
1366
1367if True:
1368    from django.contrib.auth.models import User
1369    from django.contrib.auth import authenticate, login
1370
1371    from orm.models import LayerSource, ToasterSetting, Release
1372
1373    import traceback
1374
1375    class BadParameterException(Exception):
1376        ''' The exception raised on invalid POST requests '''
1377        pass
1378
1379    # new project
1380    def newproject(request):
1381        if not project_enable:
1382            return redirect( landing )
1383
1384        template = "newproject.html"
1385        context = {
1386            'email': request.user.email if request.user.is_authenticated else '',
1387            'username': request.user.username if request.user.is_authenticated else '',
1388            'releases': Release.objects.order_by("description"),
1389        }
1390
1391        try:
1392            context['defaultbranch'] = ToasterSetting.objects.get(name = "DEFAULT_RELEASE").value
1393        except ToasterSetting.DoesNotExist:
1394            pass
1395
1396        if request.method == "GET":
1397            # render new project page
1398            return toaster_render(request, template, context)
1399        elif request.method == "POST":
1400            mandatory_fields = ['projectname', 'ptype']
1401            try:
1402                ptype = request.POST.get('ptype')
1403                if ptype == "import":
1404                    mandatory_fields.append('importdir')
1405                else:
1406                    mandatory_fields.append('projectversion')
1407                # make sure we have values for all mandatory_fields
1408                missing = [field for field in mandatory_fields if len(request.POST.get(field, '')) == 0]
1409                if missing:
1410                    # set alert for missing fields
1411                    raise BadParameterException("Fields missing: %s" % ", ".join(missing))
1412
1413                if not request.user.is_authenticated:
1414                    user = authenticate(username = request.POST.get('username', '_anonuser'), password = 'nopass')
1415                    if user is None:
1416                        user = User.objects.create_user(username = request.POST.get('username', '_anonuser'), email = request.POST.get('email', ''), password = "nopass")
1417
1418                        user = authenticate(username = user.username, password = 'nopass')
1419                    login(request, user)
1420
1421                #  save the project
1422                if ptype == "import":
1423                    if not os.path.isdir('%s/conf' % request.POST['importdir']):
1424                        raise BadParameterException("Bad path or missing 'conf' directory (%s)" % request.POST['importdir'])
1425                    from django.core import management
1426                    management.call_command('buildimport', '--command=import', '--name=%s' % request.POST['projectname'], '--path=%s' % request.POST['importdir'])
1427                    prj = Project.objects.get(name = request.POST['projectname'])
1428                    prj.merged_attr = True
1429                    prj.save()
1430                else:
1431                    release = Release.objects.get(pk = request.POST.get('projectversion', None ))
1432                    prj = Project.objects.create_project(name = request.POST['projectname'], release = release)
1433                    prj.user_id = request.user.pk
1434                    if 'mergeattr' == request.POST.get('mergeattr', ''):
1435                        prj.merged_attr = True
1436                    prj.save()
1437
1438                return redirect(reverse(project, args=(prj.pk,)) + "?notify=new-project")
1439
1440            except (IntegrityError, BadParameterException) as e:
1441                # fill in page with previously submitted values
1442                for field in mandatory_fields:
1443                    context.__setitem__(field, request.POST.get(field, "-- missing"))
1444                if isinstance(e, IntegrityError) and "username" in str(e):
1445                    context['alert'] = "Your chosen username is already used"
1446                else:
1447                    context['alert'] = str(e)
1448                return toaster_render(request, template, context)
1449
1450        raise Exception("Invalid HTTP method for this page")
1451
1452    # new project
1453    def newproject_specific(request, pid):
1454        if not project_enable:
1455            return redirect( landing )
1456
1457        project = Project.objects.get(pk=pid)
1458        template = "newproject_specific.html"
1459        context = {
1460            'email': request.user.email if request.user.is_authenticated else '',
1461            'username': request.user.username if request.user.is_authenticated else '',
1462            'releases': Release.objects.order_by("description"),
1463            'projectname': project.name,
1464            'project_pk': project.pk,
1465        }
1466
1467        # WORKAROUND: if we already know release, redirect 'newproject_specific' to 'project_specific'
1468        if '1' == project.get_variable('INTERNAL_PROJECT_SPECIFIC_SKIPRELEASE'):
1469            return redirect(reverse(project_specific, args=(project.pk,)))
1470
1471        try:
1472            context['defaultbranch'] = ToasterSetting.objects.get(name = "DEFAULT_RELEASE").value
1473        except ToasterSetting.DoesNotExist:
1474            pass
1475
1476        if request.method == "GET":
1477            # render new project page
1478            return toaster_render(request, template, context)
1479        elif request.method == "POST":
1480            mandatory_fields = ['projectname', 'ptype']
1481            try:
1482                ptype = request.POST.get('ptype')
1483                if ptype == "build":
1484                    mandatory_fields.append('projectversion')
1485                # make sure we have values for all mandatory_fields
1486                missing = [field for field in mandatory_fields if len(request.POST.get(field, '')) == 0]
1487                if missing:
1488                    # set alert for missing fields
1489                    raise BadParameterException("Fields missing: %s" % ", ".join(missing))
1490
1491                if not request.user.is_authenticated:
1492                    user = authenticate(username = request.POST.get('username', '_anonuser'), password = 'nopass')
1493                    if user is None:
1494                        user = User.objects.create_user(username = request.POST.get('username', '_anonuser'), email = request.POST.get('email', ''), password = "nopass")
1495
1496                        user = authenticate(username = user.username, password = 'nopass')
1497                    login(request, user)
1498
1499                #  save the project
1500                if ptype == "analysis":
1501                    release = None
1502                else:
1503                    release = Release.objects.get(pk = request.POST.get('projectversion', None ))
1504
1505                prj = Project.objects.create_project(name = request.POST['projectname'], release = release, existing_project = project)
1506                prj.user_id = request.user.pk
1507                prj.save()
1508                return redirect(reverse(project_specific, args=(prj.pk,)) + "?notify=new-project")
1509
1510            except (IntegrityError, BadParameterException) as e:
1511                # fill in page with previously submitted values
1512                for field in mandatory_fields:
1513                    context.__setitem__(field, request.POST.get(field, "-- missing"))
1514                if isinstance(e, IntegrityError) and "username" in str(e):
1515                    context['alert'] = "Your chosen username is already used"
1516                else:
1517                    context['alert'] = str(e)
1518                return toaster_render(request, template, context)
1519
1520        raise Exception("Invalid HTTP method for this page")
1521
1522    # Shows the edit project page
1523    def project(request, pid):
1524        project = Project.objects.get(pk=pid)
1525
1526        if '1' == os.environ.get('TOASTER_PROJECTSPECIFIC'):
1527            if request.GET:
1528                #Example:request.GET=<QueryDict: {'setMachine': ['qemuarm']}>
1529                params = urlencode(request.GET).replace('%5B%27','').replace('%27%5D','')
1530                return redirect("%s?%s" % (reverse(project_specific, args=(project.pk,)),params))
1531            else:
1532                return redirect(reverse(project_specific, args=(project.pk,)))
1533        context = {"project": project}
1534        return toaster_render(request, "project.html", context)
1535
1536    # Shows the edit project-specific page
1537    def project_specific(request, pid):
1538        project = Project.objects.get(pk=pid)
1539
1540        # Are we refreshing from a successful project specific update clone?
1541        if Project.PROJECT_SPECIFIC_CLONING_SUCCESS == project.get_variable(Project.PROJECT_SPECIFIC_STATUS):
1542            return redirect(reverse(landing_specific,args=(project.pk,)))
1543
1544        context = {
1545            "project": project,
1546            "is_new" : project.get_variable(Project.PROJECT_SPECIFIC_ISNEW),
1547            "default_image_recipe" : project.get_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE),
1548            "mru" : Build.objects.all().filter(project=project,outcome=Build.IN_PROGRESS),
1549            }
1550        if project.build_set.filter(outcome=Build.IN_PROGRESS).count() > 0:
1551            context['build_in_progress_none_completed'] = True
1552        else:
1553            context['build_in_progress_none_completed'] = False
1554        return toaster_render(request, "project.html", context)
1555
1556    # perform the final actions for the project specific page
1557    def project_specific_finalize(cmnd, pid):
1558        project = Project.objects.get(pk=pid)
1559        callback = project.get_variable(Project.PROJECT_SPECIFIC_CALLBACK)
1560        if "update" == cmnd:
1561            # Delete all '_PROJECT_PREPARE_' builds
1562            for b in Build.objects.all().filter(project=project):
1563                delete_build = False
1564                for t in b.target_set.all():
1565                    if '_PROJECT_PREPARE_' == t.target:
1566                        delete_build = True
1567                if delete_build:
1568                    from django.core import management
1569                    management.call_command('builddelete', str(b.id), interactive=False)
1570            # perform callback at this last moment if defined, in case Toaster gets shutdown next
1571            default_target = project.get_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE)
1572            if callback:
1573                callback = callback.replace("<IMAGE>",default_target)
1574        if "cancel" == cmnd:
1575            if callback:
1576                callback = callback.replace("<IMAGE>","none")
1577                callback = callback.replace("--update","--cancel")
1578        # perform callback at this last moment if defined, in case this Toaster gets shutdown next
1579        ret = ''
1580        if callback:
1581            ret = os.system('bash -c "%s"' % callback)
1582            project.set_variable(Project.PROJECT_SPECIFIC_CALLBACK,'')
1583        # Delete the temp project specific variables
1584        project.set_variable(Project.PROJECT_SPECIFIC_ISNEW,'')
1585        project.set_variable(Project.PROJECT_SPECIFIC_STATUS,Project.PROJECT_SPECIFIC_NONE)
1586        # WORKAROUND: Release this workaround flag
1587        project.set_variable('INTERNAL_PROJECT_SPECIFIC_SKIPRELEASE','')
1588
1589    # Shows the final landing page for project specific update
1590    def landing_specific(request, pid):
1591        project_specific_finalize("update", pid)
1592        context = {
1593            "install_dir": os.environ['TOASTER_DIR'],
1594        }
1595        return toaster_render(request, "landing_specific.html", context)
1596
1597    # Shows the related landing-specific page
1598    def landing_specific_cancel(request, pid):
1599        project_specific_finalize("cancel", pid)
1600        context = {
1601            "install_dir": os.environ['TOASTER_DIR'],
1602            "status": "cancel",
1603        }
1604        return toaster_render(request, "landing_specific.html", context)
1605
1606    def jsunittests(request):
1607        """ Provides a page for the js unit tests """
1608        bbv = BitbakeVersion.objects.filter(branch="master").first()
1609        release = Release.objects.filter(bitbake_version=bbv).first()
1610
1611        name = "_js_unit_test_prj_"
1612
1613        # If there is an existing project by this name delete it.
1614        # We don't want Lots of duplicates cluttering up the projects.
1615        Project.objects.filter(name=name).delete()
1616
1617        new_project = Project.objects.create_project(name=name,
1618                                                     release=release)
1619        # Add a layer
1620        layer = new_project.get_all_compatible_layer_versions().first()
1621
1622        ProjectLayer.objects.get_or_create(layercommit=layer,
1623                                           project=new_project)
1624
1625        # make sure we have a machine set for this project
1626        ProjectVariable.objects.get_or_create(project=new_project,
1627                                              name="MACHINE",
1628                                              value="qemux86-64")
1629        context = {'project': new_project}
1630        return toaster_render(request, "js-unit-tests.html", context)
1631
1632    from django.views.decorators.csrf import csrf_exempt
1633    @csrf_exempt
1634    @log_view_mixin
1635    def xhr_testreleasechange(request, pid):
1636        def response(data):
1637            return HttpResponse(jsonfilter(data),
1638                                content_type="application/json")
1639
1640        """ returns layer versions that would be deleted on the new
1641        release__pk """
1642        try:
1643            prj = Project.objects.get(pk = pid)
1644            new_release_id = request.GET['new_release_id']
1645
1646            # If we're already on this project do nothing
1647            if prj.release.pk == int(new_release_id):
1648                return reponse({"error": "ok", "rows": []})
1649
1650            retval = []
1651
1652            for project in prj.projectlayer_set.all():
1653                release = Release.objects.get(pk = new_release_id)
1654
1655                layer_versions = prj.get_all_compatible_layer_versions()
1656                layer_versions = layer_versions.filter(release = release)
1657                layer_versions = layer_versions.filter(layer__name = project.layercommit.layer.name)
1658
1659                # there is no layer_version with the new release id,
1660                # and the same name
1661                if layer_versions.count() < 1:
1662                    retval.append(project)
1663
1664            return response({"error":"ok",
1665                             "rows": [_lv_to_dict(prj) for y in [x.layercommit for x in retval]]
1666                            })
1667
1668        except Exception as e:
1669            return response({"error": str(e) })
1670
1671    @log_view_mixin
1672    def xhr_configvaredit(request, pid):
1673        try:
1674            prj = Project.objects.get(id = pid)
1675            # There are cases where user can add variables which hold values
1676            # like http://, file:/// etc. In such case a simple split(":")
1677            # would fail. One example is SSTATE_MIRRORS variable. So we use
1678            # max_split var to handle them.
1679            max_split = 1
1680            # add conf variables
1681            if 'configvarAdd' in request.POST:
1682                t=request.POST['configvarAdd'].strip()
1683                if ":" in t:
1684                    variable, value = t.split(":", max_split)
1685                else:
1686                    variable = t
1687                    value = ""
1688
1689                pt, created = ProjectVariable.objects.get_or_create(project = prj, name = variable, value = value)
1690            # change conf variables
1691            if 'configvarChange' in request.POST:
1692                t=request.POST['configvarChange'].strip()
1693                if ":" in t:
1694                    variable, value = t.split(":", max_split)
1695                else:
1696                    variable = t
1697                    value = ""
1698
1699                pt, created = ProjectVariable.objects.get_or_create(project = prj, name = variable)
1700                pt.value=value
1701                pt.save()
1702            # remove conf variables
1703            if 'configvarDel' in request.POST:
1704                t=request.POST['configvarDel'].strip()
1705                pt = ProjectVariable.objects.get(pk = int(t)).delete()
1706
1707            # return all project settings, filter out disallowed and elsewhere-managed variables
1708            vars_managed,vars_fstypes,vars_disallowed = get_project_configvars_context()
1709            configvars_query = ProjectVariable.objects.filter(project_id = pid).all()
1710            for var in vars_managed:
1711                configvars_query = configvars_query.exclude(name = var)
1712            for var in vars_disallowed:
1713                configvars_query = configvars_query.exclude(name = var)
1714
1715            return_data = {
1716                "error": "ok",
1717                'configvars': [(x.name, x.value, x.pk) for x in configvars_query]
1718               }
1719            try:
1720                return_data['distro'] = ProjectVariable.objects.get(project = prj, name = "DISTRO").value,
1721            except ProjectVariable.DoesNotExist:
1722                pass
1723            try:
1724                return_data['dl_dir'] = ProjectVariable.objects.get(project = prj, name = "DL_DIR").value,
1725            except ProjectVariable.DoesNotExist:
1726                pass
1727            try:
1728                return_data['fstypes'] = ProjectVariable.objects.get(project = prj, name = "IMAGE_FSTYPES").value,
1729            except ProjectVariable.DoesNotExist:
1730                pass
1731            try:
1732                return_data['image_install:append'] = ProjectVariable.objects.get(project = prj, name = "IMAGE_INSTALL:append").value,
1733            except ProjectVariable.DoesNotExist:
1734                pass
1735            try:
1736                return_data['package_classes'] = ProjectVariable.objects.get(project = prj, name = "PACKAGE_CLASSES").value,
1737            except ProjectVariable.DoesNotExist:
1738                pass
1739            try:
1740                return_data['sstate_dir'] = ProjectVariable.objects.get(project = prj, name = "SSTATE_DIR").value,
1741            except ProjectVariable.DoesNotExist:
1742                pass
1743
1744            return HttpResponse(json.dumps( return_data ), content_type = "application/json")
1745
1746        except Exception as e:
1747            return HttpResponse(json.dumps({"error":str(e) + "\n" + traceback.format_exc()}), content_type = "application/json")
1748
1749
1750    @log_view_mixin
1751    def customrecipe_download(request, pid, recipe_id):
1752        recipe = get_object_or_404(CustomImageRecipe, pk=recipe_id)
1753
1754        file_data = recipe.generate_recipe_file_contents()
1755
1756        response = HttpResponse(file_data, content_type='text/plain')
1757        response['Content-Disposition'] = \
1758                'attachment; filename="%s_%s.bb"' % (recipe.name,
1759                                                     recipe.version)
1760
1761        return response
1762
1763    def importlayer(request, pid):
1764        template = "importlayer.html"
1765        context = {
1766            'project': Project.objects.get(id=pid),
1767        }
1768        return toaster_render(request, template, context)
1769
1770    def layerdetails(request, pid, layerid):
1771        project = Project.objects.get(pk=pid)
1772        layer_version = Layer_Version.objects.get(pk=layerid)
1773
1774        project_layers = ProjectLayer.objects.filter(
1775            project=project).values_list("layercommit_id",
1776                                         flat=True)
1777
1778        context = {
1779            'project': project,
1780            'layer_source': LayerSource.types_dict(),
1781            'layerversion': layer_version,
1782            'layerdeps': {
1783                "list": [
1784                    {
1785                        "id": dep.id,
1786                        "name": dep.layer.name,
1787                        "layerdetailurl": reverse('layerdetails',
1788                                                  args=(pid, dep.pk)),
1789                        "vcs_url": dep.layer.vcs_url,
1790                        "vcs_reference": dep.get_vcs_reference()
1791                    }
1792                    for dep in layer_version.get_alldeps(project.id)]
1793            },
1794            'projectlayers': list(project_layers)
1795        }
1796
1797        return toaster_render(request, 'layerdetails.html', context)
1798
1799
1800    def get_project_configvars_context():
1801        # Vars managed outside of this view
1802        vars_managed = {
1803            'MACHINE', 'BBLAYERS'
1804        }
1805
1806        vars_disallowed  = {
1807            'PARALLEL_MAKE','BB_NUMBER_THREADS',
1808            'BB_DISKMON_DIRS','BB_NUMBER_THREADS','CVS_PROXY_HOST','CVS_PROXY_PORT',
1809            'PARALLEL_MAKE','TMPDIR',
1810            'all_proxy','ftp_proxy','http_proxy ','https_proxy'
1811            }
1812
1813        vars_fstypes = Target_Image_File.SUFFIXES
1814
1815        return(vars_managed,sorted(vars_fstypes),vars_disallowed)
1816
1817    def projectconf(request, pid):
1818
1819        try:
1820            prj = Project.objects.get(id = pid)
1821        except Project.DoesNotExist:
1822            return HttpResponseNotFound("<h1>Project id " + pid + " is unavailable</h1>")
1823
1824        # remove disallowed and externally managed varaibles from this list
1825        vars_managed,vars_fstypes,vars_disallowed = get_project_configvars_context()
1826        configvars = ProjectVariable.objects.filter(project_id = pid).all()
1827        for var in vars_managed:
1828            configvars = configvars.exclude(name = var)
1829        for var in vars_disallowed:
1830            configvars = configvars.exclude(name = var)
1831
1832        context = {
1833            'project':          prj,
1834            'configvars':       configvars,
1835            'vars_managed':     vars_managed,
1836            'vars_fstypes':     vars_fstypes,
1837            'vars_disallowed':  vars_disallowed,
1838        }
1839
1840        try:
1841            context['distro'] =  ProjectVariable.objects.get(project = prj, name = "DISTRO").value
1842            context['distro_defined'] = "1"
1843        except ProjectVariable.DoesNotExist:
1844            pass
1845        try:
1846            if ProjectVariable.objects.get(project = prj, name = "DL_DIR").value == "${TOPDIR}/../downloads":
1847                be = BuildEnvironment.objects.get(pk = str(1))
1848                dl_dir = os.path.join(dirname(be.builddir), "downloads")
1849                context['dl_dir'] =  dl_dir
1850                pv, created = ProjectVariable.objects.get_or_create(project = prj, name = "DL_DIR")
1851                pv.value = dl_dir
1852                pv.save()
1853            else:
1854                context['dl_dir'] = ProjectVariable.objects.get(project = prj, name = "DL_DIR").value
1855            context['dl_dir_defined'] = "1"
1856        except (ProjectVariable.DoesNotExist, BuildEnvironment.DoesNotExist):
1857            pass
1858        try:
1859            context['fstypes'] =  ProjectVariable.objects.get(project = prj, name = "IMAGE_FSTYPES").value
1860            context['fstypes_defined'] = "1"
1861        except ProjectVariable.DoesNotExist:
1862            pass
1863        try:
1864            context['image_install:append'] =  ProjectVariable.objects.get(project = prj, name = "IMAGE_INSTALL:append").value
1865            context['image_install_append_defined'] = "1"
1866        except ProjectVariable.DoesNotExist:
1867            pass
1868        try:
1869            context['package_classes'] =  ProjectVariable.objects.get(project = prj, name = "PACKAGE_CLASSES").value
1870            context['package_classes_defined'] = "1"
1871        except ProjectVariable.DoesNotExist:
1872            pass
1873        try:
1874            if ProjectVariable.objects.get(project = prj, name = "SSTATE_DIR").value == "${TOPDIR}/../sstate-cache":
1875                be = BuildEnvironment.objects.get(pk = str(1))
1876                sstate_dir = os.path.join(dirname(be.builddir), "sstate-cache")
1877                context['sstate_dir'] = sstate_dir
1878                pv, created = ProjectVariable.objects.get_or_create(project = prj, name = "SSTATE_DIR")
1879                pv.value = sstate_dir
1880                pv.save()
1881            else:
1882                context['sstate_dir'] = ProjectVariable.objects.get(project = prj, name = "SSTATE_DIR").value
1883            context['sstate_dir_defined'] = "1"
1884        except (ProjectVariable.DoesNotExist, BuildEnvironment.DoesNotExist):
1885            pass
1886
1887        return toaster_render(request, "projectconf.html", context)
1888
1889    def _file_names_for_artifact(build, artifact_type, artifact_id):
1890        """
1891        Return a tuple (file path, file name for the download response) for an
1892        artifact of type artifact_type with ID artifact_id for build; if
1893        artifact type is not supported, returns (None, None)
1894        """
1895        file_name = None
1896        response_file_name = None
1897
1898        if artifact_type == "cookerlog":
1899            file_name = build.cooker_log_path
1900            response_file_name = "cooker.log"
1901
1902        elif artifact_type == "imagefile":
1903            file_name = Target_Image_File.objects.get(target__build = build, pk = artifact_id).file_name
1904
1905        elif artifact_type == "targetkernelartifact":
1906            target = TargetKernelFile.objects.get(pk=artifact_id)
1907            file_name = target.file_name
1908
1909        elif artifact_type == "targetsdkartifact":
1910            target = TargetSDKFile.objects.get(pk=artifact_id)
1911            file_name = target.file_name
1912
1913        elif artifact_type == "licensemanifest":
1914            file_name = Target.objects.get(build = build, pk = artifact_id).license_manifest_path
1915
1916        elif artifact_type == "packagemanifest":
1917            file_name = Target.objects.get(build = build, pk = artifact_id).package_manifest_path
1918
1919        elif artifact_type == "tasklogfile":
1920            file_name = Task.objects.get(build = build, pk = artifact_id).logfile
1921
1922        elif artifact_type == "logmessagefile":
1923            file_name = LogMessage.objects.get(build = build, pk = artifact_id).pathname
1924
1925        if file_name and not response_file_name:
1926            response_file_name = os.path.basename(file_name)
1927
1928        return (file_name, response_file_name)
1929
1930    def build_artifact(request, build_id, artifact_type, artifact_id):
1931        """
1932        View which returns a build artifact file as a response
1933        """
1934        file_name = None
1935        response_file_name = None
1936
1937        try:
1938            build = Build.objects.get(pk = build_id)
1939            file_name, response_file_name = _file_names_for_artifact(
1940                build, artifact_type, artifact_id
1941            )
1942
1943            if file_name and response_file_name:
1944                fsock = open(file_name, "rb")
1945                content_type = MimeTypeFinder.get_mimetype(file_name)
1946
1947                response = HttpResponse(fsock, content_type = content_type)
1948
1949                disposition = "attachment; filename=" + response_file_name
1950                response["Content-Disposition"] = disposition
1951
1952                return response
1953            else:
1954                return toaster_render(request, "unavailable_artifact.html")
1955        except (ObjectDoesNotExist, IOError):
1956            return toaster_render(request, "unavailable_artifact.html")
1957
1958
1959class CommandLineBuilds(TemplateView):
1960    model = EventLogsImports
1961    template_name = 'command_line_builds.html'
1962
1963    def get_context_data(self, **kwargs):
1964        context = super(CommandLineBuilds, self).get_context_data(**kwargs)
1965        #get value from BB_DEFAULT_EVENTLOG defined in bitbake.conf
1966        eventlog = subprocess.check_output(['bitbake-getvar', 'BB_DEFAULT_EVENTLOG', '--value'])
1967        if eventlog:
1968            logs_dir = os.path.dirname(eventlog.decode().strip('\n'))
1969            files = os.listdir(logs_dir)
1970            imported_files = EventLogsImports.objects.all()
1971            files_list = []
1972
1973            # Filter files that end with ".json"
1974            event_files = []
1975            for file in files:
1976                if file.endswith(".json"):
1977                    # because BB_DEFAULT_EVENTLOG is a directory, we need to check if the file is a valid eventlog
1978                    with open("{}/{}".format(logs_dir, file)) as efile:
1979                        content = efile.read()
1980                        if 'allvariables' in content:
1981                            event_files.append(file)
1982
1983            #build dict for template using db data
1984            for event_file in event_files:
1985                if imported_files.filter(name=event_file):
1986                    files_list.append({
1987                        'name': event_file,
1988                        'imported': True,
1989                        'build_id': imported_files.filter(name=event_file)[0].build_id,
1990                        'size': os.path.getsize("{}/{}".format(logs_dir, event_file))
1991                    })
1992                else:
1993                    files_list.append({
1994                        'name': event_file,
1995                        'imported': False,
1996                        'build_id': None,
1997                        'size': os.path.getsize("{}/{}".format(logs_dir, event_file))
1998                    })
1999                    context['import_all'] = True
2000
2001            context['files'] = files_list
2002            context['dir'] = logs_dir
2003        else:
2004            context['files'] = []
2005            context['dir'] = ''
2006
2007        # enable session variable
2008        if not self.request.session.get('file'):
2009            self.request.session['file'] = ""
2010
2011        context['form'] = LoadFileForm()
2012        context['project_enable'] = project_enable
2013        return context
2014
2015    def post(self, request, **kwargs):
2016        logs_dir = request.POST.get('dir')
2017        all_files =  request.POST.get('all')
2018
2019        # check if a build is already in progress
2020        if Build.objects.filter(outcome=Build.IN_PROGRESS):
2021            messages.add_message(
2022                self.request,
2023                messages.ERROR,
2024                "A build is already in progress. Please wait for it to complete before starting a new build."
2025            )
2026            return JsonResponse({'response': 'building'})
2027        imported_files = EventLogsImports.objects.all()
2028        try:
2029            if all_files == 'true':
2030                # use of session variable to deactivate icon for builds in progress
2031                request.session['all_builds'] = True
2032                request.session.modified = True
2033                request.session.save()
2034
2035                files = ast.literal_eval(request.POST.get('file'))
2036                for file in files:
2037                    if imported_files.filter(name=file.get('name')).exists():
2038                        imported_files.filter(name=file.get('name'))[0].imported = True
2039                    else:
2040                        with open("{}/{}".format(logs_dir, file.get('name'))) as eventfile:
2041                            # load variables from the first line
2042                            variables = None
2043                            while line := eventfile.readline().strip():
2044                                try:
2045                                    variables = json.loads(line)['allvariables']
2046                                    break
2047                                except (KeyError, json.JSONDecodeError):
2048                                    continue
2049                            if not variables:
2050                                raise Exception("File content missing  build variables")
2051                            eventfile.seek(0)
2052                            params = namedtuple('ConfigParams', ['observe_only'])(True)
2053                            player = eventreplay.EventPlayer(eventfile, variables)
2054
2055                            toasterui.main(player, player, params)
2056                        event_log_import = EventLogsImports.objects.create(name=file.get('name'), imported=True)
2057                        event_log_import.build_id = Build.objects.last().id
2058                        event_log_import.save()
2059            else:
2060                if self.request.FILES.get('eventlog_file'):
2061                    file = self.request.FILES['eventlog_file']
2062                else:
2063                    file = request.POST.get('file')
2064                    # use of session variable to deactivate icon for build in progress
2065                    request.session['file'] = file
2066                    request.session['all_builds'] = False
2067                    request.session.modified = True
2068                    request.session.save()
2069
2070                if imported_files.filter(name=file).exists():
2071                    imported_files.filter(name=file)[0].imported = True
2072                else:
2073                    if isinstance(file, InMemoryUploadedFile) or isinstance(file, TemporaryUploadedFile):
2074                        variables = None
2075                        while line := file.readline().strip():
2076                            try:
2077                                variables = json.loads(line)['allvariables']
2078                                break
2079                            except (KeyError, json.JSONDecodeError):
2080                                continue
2081                        if not variables:
2082                            raise Exception("File content missing  build variables")
2083                        file.seek(0)
2084                        params = namedtuple('ConfigParams', ['observe_only'])(True)
2085                        player = eventreplay.EventPlayer(file, variables)
2086                        if not os.path.exists('{}/{}'.format(logs_dir, file.name)):
2087                            fs = FileSystemStorage(location=logs_dir)
2088                            fs.save(file.name, file)
2089                        toasterui.main(player, player, params)
2090                    else:
2091                        with open("{}/{}".format(logs_dir, file)) as eventfile:
2092                            # load variables from the first line
2093                            variables = None
2094                            while line := eventfile.readline().strip():
2095                                try:
2096                                    variables = json.loads(line)['allvariables']
2097                                    break
2098                                except (KeyError, json.JSONDecodeError):
2099                                    continue
2100                            if not variables:
2101                                raise Exception("File content missing  build variables")
2102                            eventfile.seek(0)
2103                            params = namedtuple('ConfigParams', ['observe_only'])(True)
2104                            player = eventreplay.EventPlayer(eventfile, variables)
2105                            toasterui.main(player, player, params)
2106                    event_log_import = EventLogsImports.objects.create(name=file, imported=True)
2107                    event_log_import.build_id = Build.objects.last().id
2108                    event_log_import.save()
2109                    request.session['file'] = ""
2110        except Exception:
2111            messages.add_message(
2112                self.request,
2113                messages.ERROR,
2114                "The file content is not in the correct format. Update file content or upload a different file."
2115            )
2116            return HttpResponseRedirect("/toastergui/cmdline/")
2117        return HttpResponseRedirect('/toastergui/builds/')
2118