1#
2# BitBake Toaster Implementation
3#
4# Copyright (C) 2013        Intel Corporation
5#
6# SPDX-License-Identifier: GPL-2.0-only
7#
8
9from __future__ import unicode_literals
10
11from django.db import models, IntegrityError, DataError
12from django.db.models import F, Q, Sum, Count
13from django.utils import timezone
14from django.utils.encoding import force_bytes
15
16from django.urls import reverse
17
18from django.core import validators
19from django.conf import settings
20import django.db.models.signals
21
22import sys
23import os
24import re
25import itertools
26from signal import SIGUSR1
27
28
29import logging
30logger = logging.getLogger("toaster")
31
32if 'sqlite' in settings.DATABASES['default']['ENGINE']:
33    from django.db import transaction, OperationalError
34    from time import sleep
35
36    _base_save = models.Model.save
37    def save(self, *args, **kwargs):
38        while True:
39            try:
40                with transaction.atomic():
41                    return _base_save(self, *args, **kwargs)
42            except OperationalError as err:
43                if 'database is locked' in str(err):
44                    logger.warning("%s, model: %s, args: %s, kwargs: %s",
45                                   err, self.__class__, args, kwargs)
46                    sleep(0.5)
47                    continue
48                raise
49
50    models.Model.save = save
51
52    # HACK: Monkey patch Django to fix 'database is locked' issue
53
54    from django.db.models.query import QuerySet
55    _base_insert = QuerySet._insert
56    def _insert(self,  *args, **kwargs):
57        with transaction.atomic(using=self.db, savepoint=False):
58            return _base_insert(self, *args, **kwargs)
59    QuerySet._insert = _insert
60
61    def _create_object_from_params(self, lookup, params):
62        """
63        Tries to create an object using passed params.
64        Used by get_or_create and update_or_create
65        """
66        try:
67            obj = self.create(**params)
68            return obj, True
69        except (IntegrityError, DataError):
70            exc_info = sys.exc_info()
71            try:
72                return self.get(**lookup), False
73            except self.model.DoesNotExist:
74                pass
75            six.reraise(*exc_info)
76
77    QuerySet._create_object_from_params = _create_object_from_params
78
79    # end of HACK
80
81class GitURLValidator(validators.URLValidator):
82    import re
83    regex = re.compile(
84        r'^(?:ssh|git|http|ftp)s?://'  # http:// or https://
85        r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|'  # domain...
86        r'localhost|'  # localhost...
87        r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|'  # ...or ipv4
88        r'\[?[A-F0-9]*:[A-F0-9:]+\]?)'  # ...or ipv6
89        r'(?::\d+)?'  # optional port
90        r'(?:/?|[/?]\S+)$', re.IGNORECASE)
91
92def GitURLField(**kwargs):
93    r = models.URLField(**kwargs)
94    for i in range(len(r.validators)):
95        if isinstance(r.validators[i], validators.URLValidator):
96            r.validators[i] = GitURLValidator()
97    return r
98
99
100class ToasterSetting(models.Model):
101    name = models.CharField(max_length=63)
102    helptext = models.TextField()
103    value = models.CharField(max_length=255)
104
105    def __unicode__(self):
106        return "Setting %s = %s" % (self.name, self.value)
107
108
109class ProjectManager(models.Manager):
110    def create_project(self, name, release, existing_project=None, imported=False):
111        if existing_project and (release is not None):
112            prj = existing_project
113            prj.bitbake_version = release.bitbake_version
114            prj.release = release
115            # Delete the previous ProjectLayer mappings
116            for pl in ProjectLayer.objects.filter(project=prj):
117                pl.delete()
118        elif release is not None:
119            prj = self.model(name=name,
120                             bitbake_version=release.bitbake_version,
121                             release=release)
122        else:
123            prj = self.model(name=name,
124                             bitbake_version=None,
125                             release=None)
126        prj.save()
127
128        for defaultconf in ToasterSetting.objects.filter(
129                name__startswith="DEFCONF_"):
130            name = defaultconf.name[8:]
131            pv,create = ProjectVariable.objects.get_or_create(project=prj,name=name)
132            pv.value = defaultconf.value
133            pv.save()
134
135        if release is None:
136            return prj
137        if not imported:
138            for rdl in release.releasedefaultlayer_set.all():
139                lv = Layer_Version.objects.filter(
140                    layer__name=rdl.layer_name,
141                    release=release).first()
142
143                if lv:
144                    ProjectLayer.objects.create(project=prj,
145                                                layercommit=lv,
146                                                optional=False)
147                else:
148                    logger.warning("Default project layer %s not found" %
149                                rdl.layer_name)
150
151        return prj
152
153    # return single object with is_default = True
154    def get_or_create_default_project(self):
155        projects = super(ProjectManager, self).filter(is_default=True)
156
157        if len(projects) > 1:
158            raise Exception('Inconsistent project data: multiple ' +
159                            'default projects (i.e. with is_default=True)')
160        elif len(projects) < 1:
161            options = {
162                'name': 'Command line builds',
163                'short_description':
164                'Project for builds started outside Toaster',
165                'is_default': True
166            }
167            project = Project.objects.create(**options)
168            project.save()
169
170            return project
171        else:
172            return projects[0]
173
174
175class Project(models.Model):
176    search_allowed_fields = ['name', 'short_description', 'release__name',
177                             'release__branch_name']
178    name = models.CharField(max_length=100)
179    short_description = models.CharField(max_length=50, blank=True)
180    bitbake_version = models.ForeignKey('BitbakeVersion', on_delete=models.CASCADE, null=True)
181    release = models.ForeignKey("Release", on_delete=models.CASCADE, null=True)
182    created = models.DateTimeField(auto_now_add=True)
183    updated = models.DateTimeField(auto_now=True)
184    # This is a horrible hack; since Toaster has no "User" model available when
185    # running in interactive mode, we can't reference the field here directly
186    # Instead, we keep a possible null reference to the User id,
187    # as not to force
188    # hard links to possibly missing models
189    user_id = models.IntegerField(null=True)
190    objects = ProjectManager()
191
192    # build directory override (e.g. imported)
193    builddir = models.TextField()
194    # merge the Toaster configure attributes directly into the standard conf files
195    merged_attr = models.BooleanField(default=False)
196
197    # set to True for the project which is the default container
198    # for builds initiated by the command line etc.
199    is_default= models.BooleanField(default=False)
200
201    def __unicode__(self):
202        return "%s (Release %s, BBV %s)" % (self.name, self.release, self.bitbake_version)
203
204    def get_current_machine_name(self):
205        try:
206            return self.projectvariable_set.get(name="MACHINE").value
207        except (ProjectVariable.DoesNotExist,IndexError):
208            return None;
209
210    def get_number_of_builds(self):
211        """Return the number of builds which have ended"""
212
213        return self.build_set.exclude(
214            Q(outcome=Build.IN_PROGRESS) |
215            Q(outcome=Build.CANCELLED)
216        ).count()
217
218    def get_last_build_id(self):
219        try:
220            return Build.objects.filter( project = self.id ).order_by('-completed_on')[0].id
221        except (Build.DoesNotExist,IndexError):
222            return( -1 )
223
224    def get_last_outcome(self):
225        build_id = self.get_last_build_id()
226        if (-1 == build_id):
227            return( "" )
228        try:
229            return Build.objects.filter( id = build_id )[ 0 ].outcome
230        except (Build.DoesNotExist,IndexError):
231            return( "not_found" )
232
233    def get_last_target(self):
234        build_id = self.get_last_build_id()
235        if (-1 == build_id):
236            return( "" )
237        try:
238            return Target.objects.filter(build = build_id)[0].target
239        except (Target.DoesNotExist,IndexError):
240            return( "not_found" )
241
242    def get_last_errors(self):
243        build_id = self.get_last_build_id()
244        if (-1 == build_id):
245            return( 0 )
246        try:
247            return Build.objects.filter(id = build_id)[ 0 ].errors.count()
248        except (Build.DoesNotExist,IndexError):
249            return( "not_found" )
250
251    def get_last_warnings(self):
252        build_id = self.get_last_build_id()
253        if (-1 == build_id):
254            return( 0 )
255        try:
256            return Build.objects.filter(id = build_id)[ 0 ].warnings.count()
257        except (Build.DoesNotExist,IndexError):
258            return( "not_found" )
259
260    def get_last_build_extensions(self):
261        """
262        Get list of file name extensions for images produced by the most
263        recent build
264        """
265        last_build = Build.objects.get(pk = self.get_last_build_id())
266        return last_build.get_image_file_extensions()
267
268    def get_last_imgfiles(self):
269        build_id = self.get_last_build_id()
270        if (-1 == build_id):
271            return( "" )
272        try:
273            return Variable.objects.filter(build = build_id, variable_name = "IMAGE_FSTYPES")[ 0 ].variable_value
274        except (Variable.DoesNotExist,IndexError):
275            return( "not_found" )
276
277    def get_all_compatible_layer_versions(self):
278        """ Returns Queryset of all Layer_Versions which are compatible with
279        this project"""
280        queryset = None
281
282        # guard on release, as it can be null
283        if self.release:
284            queryset = Layer_Version.objects.filter(
285                (Q(release=self.release) &
286                 Q(build=None) &
287                 Q(project=None)) |
288                 Q(project=self))
289        else:
290            queryset = Layer_Version.objects.none()
291
292        return queryset
293
294    def get_project_layer_versions(self, pk=False):
295        """ Returns the Layer_Versions currently added to this project """
296        layer_versions = self.projectlayer_set.all().values_list('layercommit',
297                                                                 flat=True)
298
299        if pk is False:
300            return Layer_Version.objects.filter(pk__in=layer_versions)
301        else:
302            return layer_versions
303
304
305    def get_default_image_recipe(self):
306        try:
307            return self.projectvariable_set.get(name="DEFAULT_IMAGE").value
308        except (ProjectVariable.DoesNotExist,IndexError):
309            return None;
310
311    def get_is_new(self):
312        return self.get_variable(Project.PROJECT_SPECIFIC_ISNEW)
313
314    def get_available_machines(self):
315        """ Returns QuerySet of all Machines which are provided by the
316        Layers currently added to the Project """
317        queryset = Machine.objects.filter(
318            layer_version__in=self.get_project_layer_versions())
319
320        return queryset
321
322    def get_all_compatible_machines(self):
323        """ Returns QuerySet of all the compatible machines available to the
324        project including ones from Layers not currently added """
325        queryset = Machine.objects.filter(
326            layer_version__in=self.get_all_compatible_layer_versions())
327
328        return queryset
329
330    def get_available_distros(self):
331        """ Returns QuerySet of all Distros which are provided by the
332        Layers currently added to the Project """
333        queryset = Distro.objects.filter(
334            layer_version__in=self.get_project_layer_versions())
335
336        return queryset
337
338    def get_all_compatible_distros(self):
339        """ Returns QuerySet of all the compatible Wind River distros available to the
340        project including ones from Layers not currently added """
341        queryset = Distro.objects.filter(
342            layer_version__in=self.get_all_compatible_layer_versions())
343
344        return queryset
345
346    def get_available_recipes(self):
347        """ Returns QuerySet of all the recipes that are provided by layers
348        added to this project """
349        queryset = Recipe.objects.filter(
350            layer_version__in=self.get_project_layer_versions())
351
352        return queryset
353
354    def get_all_compatible_recipes(self):
355        """ Returns QuerySet of all the compatible Recipes available to the
356        project including ones from Layers not currently added """
357        queryset = Recipe.objects.filter(
358            layer_version__in=self.get_all_compatible_layer_versions()).exclude(name__exact='')
359
360        return queryset
361
362    # Project Specific status management
363    PROJECT_SPECIFIC_STATUS = 'INTERNAL_PROJECT_SPECIFIC_STATUS'
364    PROJECT_SPECIFIC_CALLBACK = 'INTERNAL_PROJECT_SPECIFIC_CALLBACK'
365    PROJECT_SPECIFIC_ISNEW = 'INTERNAL_PROJECT_SPECIFIC_ISNEW'
366    PROJECT_SPECIFIC_DEFAULTIMAGE = 'PROJECT_SPECIFIC_DEFAULTIMAGE'
367    PROJECT_SPECIFIC_NONE = ''
368    PROJECT_SPECIFIC_NEW = '1'
369    PROJECT_SPECIFIC_EDIT = '2'
370    PROJECT_SPECIFIC_CLONING = '3'
371    PROJECT_SPECIFIC_CLONING_SUCCESS = '4'
372    PROJECT_SPECIFIC_CLONING_FAIL = '5'
373
374    def get_variable(self,variable,default_value = ''):
375        try:
376            return self.projectvariable_set.get(name=variable).value
377        except (ProjectVariable.DoesNotExist,IndexError):
378            return default_value
379
380    def set_variable(self,variable,value):
381        pv,create = ProjectVariable.objects.get_or_create(project = self, name = variable)
382        pv.value = value
383        pv.save()
384
385    def get_default_image(self):
386        return self.get_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE)
387
388    def schedule_build(self):
389
390        from bldcontrol.models import BuildRequest, BRTarget, BRLayer
391        from bldcontrol.models import BRBitbake, BRVariable
392
393        try:
394            now = timezone.now()
395            build = Build.objects.create(project=self,
396                                         completed_on=now,
397                                         started_on=now)
398
399            br = BuildRequest.objects.create(project=self,
400                                             state=BuildRequest.REQ_QUEUED,
401                                             build=build)
402            BRBitbake.objects.create(req=br,
403                                     giturl=self.bitbake_version.giturl,
404                                     commit=self.bitbake_version.branch,
405                                     dirpath=self.bitbake_version.dirpath)
406
407            for t in self.projecttarget_set.all():
408                BRTarget.objects.create(req=br, target=t.target, task=t.task)
409                Target.objects.create(build=br.build, target=t.target,
410                                      task=t.task)
411                # If we're about to build a custom image recipe make sure
412                # that layer is currently in the project before we create the
413                # BRLayer objects
414                customrecipe = CustomImageRecipe.objects.filter(
415                    name=t.target,
416                    project=self).first()
417                if customrecipe:
418                    ProjectLayer.objects.get_or_create(
419                        project=self,
420                        layercommit=customrecipe.layer_version,
421                        optional=False)
422
423            for l in self.projectlayer_set.all().order_by("pk"):
424                commit = l.layercommit.get_vcs_reference()
425                logger.debug("Adding layer to build %s" %
426                             l.layercommit.layer.name)
427                BRLayer.objects.create(
428                    req=br,
429                    name=l.layercommit.layer.name,
430                    giturl=l.layercommit.layer.vcs_url,
431                    commit=commit,
432                    dirpath=l.layercommit.dirpath,
433                    layer_version=l.layercommit,
434                    local_source_dir=l.layercommit.layer.local_source_dir
435                )
436
437            for v in self.projectvariable_set.all():
438                BRVariable.objects.create(req=br, name=v.name, value=v.value)
439
440            try:
441                br.build.machine = self.projectvariable_set.get(
442                    name='MACHINE').value
443                br.build.save()
444            except ProjectVariable.DoesNotExist:
445                pass
446
447            br.save()
448            signal_runbuilds()
449
450        except Exception:
451            # revert the build request creation since we're not done cleanly
452            br.delete()
453            raise
454        return br
455
456class Build(models.Model):
457    SUCCEEDED = 0
458    FAILED = 1
459    IN_PROGRESS = 2
460    CANCELLED = 3
461
462    BUILD_OUTCOME = (
463        (SUCCEEDED, 'Succeeded'),
464        (FAILED, 'Failed'),
465        (IN_PROGRESS, 'In Progress'),
466        (CANCELLED, 'Cancelled'),
467    )
468
469    search_allowed_fields = ['machine', 'cooker_log_path', "target__target", "target__target_image_file__file_name"]
470
471    project = models.ForeignKey(Project, on_delete=models.CASCADE)            # must have a project
472    machine = models.CharField(max_length=100)
473    distro = models.CharField(max_length=100)
474    distro_version = models.CharField(max_length=100)
475    started_on = models.DateTimeField()
476    completed_on = models.DateTimeField()
477    outcome = models.IntegerField(choices=BUILD_OUTCOME, default=IN_PROGRESS)
478    cooker_log_path = models.CharField(max_length=500)
479    build_name = models.CharField(max_length=100, default='')
480    bitbake_version = models.CharField(max_length=50)
481
482    # number of recipes to parse for this build
483    recipes_to_parse = models.IntegerField(default=1)
484
485    # number of recipes parsed so far for this build
486    recipes_parsed = models.IntegerField(default=1)
487
488    # number of repos to clone for this build
489    repos_to_clone = models.IntegerField(default=1)
490
491    # number of repos cloned so far for this build (default off)
492    repos_cloned = models.IntegerField(default=1)
493
494    # Hint on current progress item
495    progress_item = models.CharField(max_length=40)
496
497    @staticmethod
498    def get_recent(project=None):
499        """
500        Return recent builds as a list; if project is set, only return
501        builds for that project
502        """
503
504        builds = Build.objects.all()
505
506        if project:
507            builds = builds.filter(project=project)
508
509        finished_criteria = \
510                Q(outcome=Build.SUCCEEDED) | \
511                Q(outcome=Build.FAILED) | \
512                Q(outcome=Build.CANCELLED)
513
514        recent_builds = list(itertools.chain(
515            builds.filter(outcome=Build.IN_PROGRESS).order_by("-started_on"),
516            builds.filter(finished_criteria).order_by("-completed_on")[:3]
517        ))
518
519        # add percentage done property to each build; this is used
520        # to show build progress in mrb_section.html
521        for build in recent_builds:
522            build.percentDone = build.completeper()
523            build.outcomeText = build.get_outcome_text()
524
525        return recent_builds
526
527    def started(self):
528        """
529        As build variables are only added for a build when its BuildStarted event
530        is received, a build with no build variables is counted as
531        "in preparation" and not properly started yet. This method
532        will return False if a build has no build variables (it never properly
533        started), or True otherwise.
534
535        Note that this is a temporary workaround for the fact that we don't
536        have a fine-grained state variable on a build which would allow us
537        to record "in progress" (BuildStarted received) vs. "in preparation".
538        """
539        variables = Variable.objects.filter(build=self)
540        return len(variables) > 0
541
542    def completeper(self):
543        tf = Task.objects.filter(build = self)
544        tfc = tf.count()
545        if tfc > 0:
546            completeper = tf.exclude(outcome=Task.OUTCOME_NA).count()*100 // tfc
547        else:
548            completeper = 0
549        return completeper
550
551    def eta(self):
552        eta = timezone.now()
553        completeper = self.completeper()
554        if self.completeper() > 0:
555            eta += ((eta - self.started_on)*(100-completeper))/completeper
556        return eta
557
558    def has_images(self):
559        """
560        Returns True if at least one of the targets for this build has an
561        image file associated with it, False otherwise
562        """
563        targets = Target.objects.filter(build_id=self.id)
564        has_images = False
565        for target in targets:
566            if target.has_images():
567                has_images = True
568                break
569        return has_images
570
571    def has_image_recipes(self):
572        """
573        Returns True if a build has any targets which were built from
574        image recipes.
575        """
576        image_recipes = self.get_image_recipes()
577        return len(image_recipes) > 0
578
579    def get_image_file_extensions(self):
580        """
581        Get string of file name extensions for images produced by this build;
582        note that this is the actual list of extensions stored on Target objects
583        for this build, and not the value of IMAGE_FSTYPES.
584
585        Returns comma-separated string, e.g. "vmdk, ext4"
586        """
587        extensions = []
588
589        targets = Target.objects.filter(build_id = self.id)
590        for target in targets:
591            if not target.is_image:
592                continue
593
594            target_image_files = Target_Image_File.objects.filter(
595                target_id=target.id)
596
597            for target_image_file in target_image_files:
598                extensions.append(target_image_file.suffix)
599
600        extensions = list(set(extensions))
601        extensions.sort()
602
603        return ', '.join(extensions)
604
605    def get_image_fstypes(self):
606        """
607        Get the IMAGE_FSTYPES variable value for this build as a de-duplicated
608        list of image file suffixes.
609        """
610        image_fstypes = Variable.objects.get(
611            build=self, variable_name='IMAGE_FSTYPES').variable_value
612        return list(set(re.split(r' {1,}', image_fstypes)))
613
614    def get_sorted_target_list(self):
615        tgts = Target.objects.filter(build_id = self.id).order_by( 'target' );
616        return( tgts );
617
618    def get_recipes(self):
619        """
620        Get the recipes related to this build;
621        note that the related layer versions and layers are also prefetched
622        by this query, as this queryset can be sorted by these objects in the
623        build recipes view; prefetching them here removes the need
624        for another query in that view
625        """
626        layer_versions = Layer_Version.objects.filter(build=self)
627        criteria = Q(layer_version__id__in=layer_versions)
628        return Recipe.objects.filter(criteria) \
629                             .select_related('layer_version', 'layer_version__layer')
630
631    def get_image_recipes(self):
632        """
633        Returns a list of image Recipes (custom and built-in) related to this
634        build, sorted by name; note that this has to be done in two steps, as
635        there's no way to get all the custom image recipes and image recipes
636        in one query
637        """
638        custom_image_recipes = self.get_custom_image_recipes()
639        custom_image_recipe_names = custom_image_recipes.values_list('name', flat=True)
640
641        not_custom_image_recipes = ~Q(name__in=custom_image_recipe_names) & \
642                                   Q(is_image=True)
643
644        built_image_recipes = self.get_recipes().filter(not_custom_image_recipes)
645
646        # append to the custom image recipes and sort
647        customisable_image_recipes = list(
648            itertools.chain(custom_image_recipes, built_image_recipes)
649        )
650
651        return sorted(customisable_image_recipes, key=lambda recipe: recipe.name)
652
653    def get_custom_image_recipes(self):
654        """
655        Returns a queryset of CustomImageRecipes related to this build,
656        sorted by name
657        """
658        built_recipe_names = self.get_recipes().values_list('name', flat=True)
659        criteria = Q(name__in=built_recipe_names) & Q(project=self.project)
660        queryset = CustomImageRecipe.objects.filter(criteria).order_by('name')
661        return queryset
662
663    def get_outcome_text(self):
664        return Build.BUILD_OUTCOME[int(self.outcome)][1]
665
666    @property
667    def failed_tasks(self):
668        """ Get failed tasks for the build """
669        tasks = self.task_build.all()
670        return tasks.filter(order__gt=0, outcome=Task.OUTCOME_FAILED)
671
672    @property
673    def errors(self):
674        return (self.logmessage_set.filter(level=LogMessage.ERROR) |
675                self.logmessage_set.filter(level=LogMessage.EXCEPTION) |
676                self.logmessage_set.filter(level=LogMessage.CRITICAL))
677
678    @property
679    def warnings(self):
680        return self.logmessage_set.filter(level=LogMessage.WARNING)
681
682    @property
683    def timespent(self):
684        return self.completed_on - self.started_on
685
686    @property
687    def timespent_seconds(self):
688        return self.timespent.total_seconds()
689
690    @property
691    def target_labels(self):
692        """
693        Sorted (a-z) "target1:task, target2, target3" etc. string for all
694        targets in this build
695        """
696        targets = self.target_set.all()
697        target_labels = [target.target +
698                         (':' + target.task if target.task else '')
699                         for target in targets]
700        target_labels.sort()
701
702        return target_labels
703
704    def get_buildrequest(self):
705        buildrequest = None
706        if hasattr(self, 'buildrequest'):
707            buildrequest = self.buildrequest
708        return buildrequest
709
710    def is_queued(self):
711        from bldcontrol.models import BuildRequest
712        buildrequest = self.get_buildrequest()
713        if buildrequest:
714            return buildrequest.state == BuildRequest.REQ_QUEUED
715        else:
716            return False
717
718    def is_cancelling(self):
719        from bldcontrol.models import BuildRequest
720        buildrequest = self.get_buildrequest()
721        if buildrequest:
722            return self.outcome == Build.IN_PROGRESS and \
723                buildrequest.state == BuildRequest.REQ_CANCELLING
724        else:
725            return False
726
727    def is_cloning(self):
728        """
729        True if the build is still cloning repos
730        """
731        return self.outcome == Build.IN_PROGRESS and \
732            self.repos_cloned < self.repos_to_clone
733
734    def is_parsing(self):
735        """
736        True if the build is still parsing recipes
737        """
738        return self.outcome == Build.IN_PROGRESS and \
739            self.recipes_parsed < self.recipes_to_parse
740
741    def is_starting(self):
742        """
743        True if the build has no completed tasks yet and is still just starting
744        tasks.
745
746        Note that the mechanism for testing whether a Task is "done" is whether
747        its outcome field is set, as per the completeper() method.
748        """
749        return self.outcome == Build.IN_PROGRESS and \
750            self.task_build.exclude(outcome=Task.OUTCOME_NA).count() == 0
751
752
753    def get_state(self):
754        """
755        Get the state of the build; one of 'Succeeded', 'Failed', 'In Progress',
756        'Cancelled' (Build outcomes); or 'Queued', 'Cancelling' (states
757        dependent on the BuildRequest state).
758
759        This works around the fact that we have BuildRequest states as well
760        as Build states, but really we just want to know the state of the build.
761        """
762        if self.is_cancelling():
763            return 'Cancelling';
764        elif self.is_queued():
765            return 'Queued'
766        elif self.is_cloning():
767            return 'Cloning'
768        elif self.is_parsing():
769            return 'Parsing'
770        elif self.is_starting():
771            return 'Starting'
772        else:
773            return self.get_outcome_text()
774
775    def __str__(self):
776        return "%d %s %s" % (self.id, self.project, ",".join([t.target for t in self.target_set.all()]))
777
778class ProjectTarget(models.Model):
779    project = models.ForeignKey(Project, on_delete=models.CASCADE)
780    target = models.CharField(max_length=100)
781    task = models.CharField(max_length=100, null=True)
782
783class Target(models.Model):
784    search_allowed_fields = ['target', 'file_name']
785    build = models.ForeignKey(Build, on_delete=models.CASCADE)
786    target = models.CharField(max_length=100)
787    task = models.CharField(max_length=100, null=True)
788    is_image = models.BooleanField(default = False)
789    image_size = models.IntegerField(default=0)
790    license_manifest_path = models.CharField(max_length=500, null=True)
791    package_manifest_path = models.CharField(max_length=500, null=True)
792
793    def package_count(self):
794        return Target_Installed_Package.objects.filter(target_id__exact=self.id).count()
795
796    def __unicode__(self):
797        return self.target
798
799    def get_similar_targets(self):
800        """
801        Get target sfor the same machine, task and target name
802        (e.g. 'core-image-minimal') from a successful build for this project
803        (but excluding this target).
804
805        Note that we only look for targets built by this project because
806        projects can have different configurations from each other, and put
807        their artifacts in different directories.
808
809        The possibility of error when retrieving candidate targets
810        is minimised by the fact that bitbake will rebuild artifacts if MACHINE
811        (or various other variables) change. In this case, there is no need to
812        clone artifacts from another target, as those artifacts will have
813        been re-generated for this target anyway.
814        """
815        query = ~Q(pk=self.pk) & \
816            Q(target=self.target) & \
817            Q(build__machine=self.build.machine) & \
818            Q(build__outcome=Build.SUCCEEDED) & \
819            Q(build__project=self.build.project)
820
821        return Target.objects.filter(query)
822
823    def get_similar_target_with_image_files(self):
824        """
825        Get the most recent similar target with Target_Image_Files associated
826        with it, for the purpose of cloning those files onto this target.
827        """
828        similar_target = None
829
830        candidates = self.get_similar_targets()
831        if candidates.count() == 0:
832            return similar_target
833
834        task_subquery = Q(task=self.task)
835
836        # we can look for a 'build' task if this task is a 'populate_sdk_ext'
837        # task, as the latter also creates images; and vice versa; note that
838        # 'build' targets can have their task set to '';
839        # also note that 'populate_sdk' does not produce image files
840        image_tasks = [
841            '', # aka 'build'
842            'build',
843            'image',
844            'populate_sdk_ext'
845        ]
846        if self.task in image_tasks:
847            task_subquery = Q(task__in=image_tasks)
848
849        # annotate with the count of files, to exclude any targets which
850        # don't have associated files
851        candidates = candidates.annotate(num_files=Count('target_image_file'))
852
853        query = task_subquery & Q(num_files__gt=0)
854
855        candidates = candidates.filter(query)
856
857        if candidates.count() > 0:
858            candidates.order_by('build__completed_on')
859            similar_target = candidates.last()
860
861        return similar_target
862
863    def get_similar_target_with_sdk_files(self):
864        """
865        Get the most recent similar target with TargetSDKFiles associated
866        with it, for the purpose of cloning those files onto this target.
867        """
868        similar_target = None
869
870        candidates = self.get_similar_targets()
871        if candidates.count() == 0:
872            return similar_target
873
874        # annotate with the count of files, to exclude any targets which
875        # don't have associated files
876        candidates = candidates.annotate(num_files=Count('targetsdkfile'))
877
878        query = Q(task=self.task) & Q(num_files__gt=0)
879
880        candidates = candidates.filter(query)
881
882        if candidates.count() > 0:
883            candidates.order_by('build__completed_on')
884            similar_target = candidates.last()
885
886        return similar_target
887
888    def clone_image_artifacts_from(self, target):
889        """
890        Make clones of the Target_Image_Files and TargetKernelFile objects
891        associated with Target target, then associate them with this target.
892
893        Note that for Target_Image_Files, we only want files from the previous
894        build whose suffix matches one of the suffixes defined in this
895        target's build's IMAGE_FSTYPES configuration variable. This prevents the
896        Target_Image_File object for an ext4 image being associated with a
897        target for a project which didn't produce an ext4 image (for example).
898
899        Also sets the license_manifest_path and package_manifest_path
900        of this target to the same path as that of target being cloned from, as
901        the manifests are also build artifacts but are treated differently.
902        """
903
904        image_fstypes = self.build.get_image_fstypes()
905
906        # filter out any image files whose suffixes aren't in the
907        # IMAGE_FSTYPES suffixes variable for this target's build
908        image_files = [target_image_file \
909            for target_image_file in target.target_image_file_set.all() \
910            if target_image_file.suffix in image_fstypes]
911
912        for image_file in image_files:
913            image_file.pk = None
914            image_file.target = self
915            image_file.save()
916
917        kernel_files = target.targetkernelfile_set.all()
918        for kernel_file in kernel_files:
919            kernel_file.pk = None
920            kernel_file.target = self
921            kernel_file.save()
922
923        self.license_manifest_path = target.license_manifest_path
924        self.package_manifest_path = target.package_manifest_path
925        self.save()
926
927    def clone_sdk_artifacts_from(self, target):
928        """
929        Clone TargetSDKFile objects from target and associate them with this
930        target.
931        """
932        sdk_files = target.targetsdkfile_set.all()
933        for sdk_file in sdk_files:
934            sdk_file.pk = None
935            sdk_file.target = self
936            sdk_file.save()
937
938    def has_images(self):
939        """
940        Returns True if this target has one or more image files attached to it.
941        """
942        return self.target_image_file_set.all().count() > 0
943
944# kernel artifacts for a target: bzImage and modules*
945class TargetKernelFile(models.Model):
946    target = models.ForeignKey(Target, on_delete=models.CASCADE)
947    file_name = models.FilePathField()
948    file_size = models.IntegerField()
949
950    @property
951    def basename(self):
952        return os.path.basename(self.file_name)
953
954# SDK artifacts for a target: sh and manifest files
955class TargetSDKFile(models.Model):
956    target = models.ForeignKey(Target, on_delete=models.CASCADE)
957    file_name = models.FilePathField()
958    file_size = models.IntegerField()
959
960    @property
961    def basename(self):
962        return os.path.basename(self.file_name)
963
964class Target_Image_File(models.Model):
965    # valid suffixes for image files produced by a build
966    SUFFIXES = {
967        'btrfs', 'container', 'cpio', 'cpio.gz', 'cpio.lz4', 'cpio.lzma',
968        'cpio.xz', 'cramfs', 'ext2', 'ext2.bz2', 'ext2.gz', 'ext2.lzma',
969        'ext3', 'ext3.gz', 'ext4', 'ext4.gz', 'f2fs', 'hddimg', 'iso', 'jffs2',
970        'jffs2.sum', 'multiubi', 'squashfs', 'squashfs-lz4', 'squashfs-lzo',
971        'squashfs-xz', 'tar', 'tar.bz2', 'tar.gz', 'tar.lz4', 'tar.xz', 'ubi',
972        'ubifs', 'wic', 'wic.bz2', 'wic.gz', 'wic.lzma'
973    }
974
975    target = models.ForeignKey(Target, on_delete=models.CASCADE)
976    file_name = models.FilePathField(max_length=254)
977    file_size = models.IntegerField()
978
979    @property
980    def suffix(self):
981        """
982        Suffix for image file, minus leading "."
983        """
984        for suffix in Target_Image_File.SUFFIXES:
985            if self.file_name.endswith(suffix):
986                return suffix
987
988        filename, suffix = os.path.splitext(self.file_name)
989        suffix = suffix.lstrip('.')
990        return suffix
991
992class Target_File(models.Model):
993    ITYPE_REGULAR = 1
994    ITYPE_DIRECTORY = 2
995    ITYPE_SYMLINK = 3
996    ITYPE_SOCKET = 4
997    ITYPE_FIFO = 5
998    ITYPE_CHARACTER = 6
999    ITYPE_BLOCK = 7
1000    ITYPES = ( (ITYPE_REGULAR ,'regular'),
1001        ( ITYPE_DIRECTORY ,'directory'),
1002        ( ITYPE_SYMLINK ,'symlink'),
1003        ( ITYPE_SOCKET ,'socket'),
1004        ( ITYPE_FIFO ,'fifo'),
1005        ( ITYPE_CHARACTER ,'character'),
1006        ( ITYPE_BLOCK ,'block'),
1007        )
1008
1009    target = models.ForeignKey(Target, on_delete=models.CASCADE)
1010    path = models.FilePathField()
1011    size = models.IntegerField()
1012    inodetype = models.IntegerField(choices = ITYPES)
1013    permission = models.CharField(max_length=16)
1014    owner = models.CharField(max_length=128)
1015    group = models.CharField(max_length=128)
1016    directory = models.ForeignKey('Target_File', on_delete=models.CASCADE, related_name="directory_set", null=True)
1017    sym_target = models.ForeignKey('Target_File', on_delete=models.CASCADE, related_name="symlink_set", null=True)
1018
1019
1020class Task(models.Model):
1021
1022    SSTATE_NA = 0
1023    SSTATE_MISS = 1
1024    SSTATE_FAILED = 2
1025    SSTATE_RESTORED = 3
1026
1027    SSTATE_RESULT = (
1028        (SSTATE_NA, 'Not Applicable'), # For rest of tasks, but they still need checking.
1029        (SSTATE_MISS, 'File not in cache'), # the sstate object was not found
1030        (SSTATE_FAILED, 'Failed'), # there was a pkg, but the script failed
1031        (SSTATE_RESTORED, 'Succeeded'), # successfully restored
1032    )
1033
1034    CODING_NA = 0
1035    CODING_PYTHON = 2
1036    CODING_SHELL = 3
1037
1038    TASK_CODING = (
1039        (CODING_NA, 'N/A'),
1040        (CODING_PYTHON, 'Python'),
1041        (CODING_SHELL, 'Shell'),
1042    )
1043
1044    OUTCOME_NA = -1
1045    OUTCOME_SUCCESS = 0
1046    OUTCOME_COVERED = 1
1047    OUTCOME_CACHED = 2
1048    OUTCOME_PREBUILT = 3
1049    OUTCOME_FAILED = 4
1050    OUTCOME_EMPTY = 5
1051
1052    TASK_OUTCOME = (
1053        (OUTCOME_NA, 'Not Available'),
1054        (OUTCOME_SUCCESS, 'Succeeded'),
1055        (OUTCOME_COVERED, 'Covered'),
1056        (OUTCOME_CACHED, 'Cached'),
1057        (OUTCOME_PREBUILT, 'Prebuilt'),
1058        (OUTCOME_FAILED, 'Failed'),
1059        (OUTCOME_EMPTY, 'Empty'),
1060    )
1061
1062    TASK_OUTCOME_HELP = (
1063        (OUTCOME_SUCCESS, 'This task successfully completed'),
1064        (OUTCOME_COVERED, 'This task did not run because its output is provided by another task'),
1065        (OUTCOME_CACHED, 'This task restored output from the sstate-cache directory or mirrors'),
1066        (OUTCOME_PREBUILT, 'This task did not run because its outcome was reused from a previous build'),
1067        (OUTCOME_FAILED, 'This task did not complete'),
1068        (OUTCOME_EMPTY, 'This task has no executable content'),
1069        (OUTCOME_NA, ''),
1070    )
1071
1072    search_allowed_fields = [ "recipe__name", "recipe__version", "task_name", "logfile" ]
1073
1074    def __init__(self, *args, **kwargs):
1075        super(Task, self).__init__(*args, **kwargs)
1076        try:
1077            self._helptext = HelpText.objects.get(key=self.task_name, area=HelpText.VARIABLE, build=self.build).text
1078        except HelpText.DoesNotExist:
1079            self._helptext = None
1080
1081    def get_related_setscene(self):
1082        return Task.objects.filter(task_executed=True, build = self.build, recipe = self.recipe, task_name=self.task_name+"_setscene")
1083
1084    def get_outcome_text(self):
1085        return Task.TASK_OUTCOME[int(self.outcome) + 1][1]
1086
1087    def get_outcome_help(self):
1088        return Task.TASK_OUTCOME_HELP[int(self.outcome)][1]
1089
1090    def get_sstate_text(self):
1091        if self.sstate_result==Task.SSTATE_NA:
1092            return ''
1093        else:
1094            return Task.SSTATE_RESULT[int(self.sstate_result)][1]
1095
1096    def get_executed_display(self):
1097        if self.task_executed:
1098            return "Executed"
1099        return "Not Executed"
1100
1101    def get_description(self):
1102        return self._helptext
1103
1104    build = models.ForeignKey(Build, on_delete=models.CASCADE, related_name='task_build')
1105    order = models.IntegerField(null=True)
1106    task_executed = models.BooleanField(default=False) # True means Executed, False means Not/Executed
1107    outcome = models.IntegerField(choices=TASK_OUTCOME, default=OUTCOME_NA)
1108    sstate_checksum = models.CharField(max_length=100, blank=True)
1109    path_to_sstate_obj = models.FilePathField(max_length=500, blank=True)
1110    recipe = models.ForeignKey('Recipe', on_delete=models.CASCADE, related_name='tasks')
1111    task_name = models.CharField(max_length=100)
1112    source_url = models.FilePathField(max_length=255, blank=True)
1113    work_directory = models.FilePathField(max_length=255, blank=True)
1114    script_type = models.IntegerField(choices=TASK_CODING, default=CODING_NA)
1115    line_number = models.IntegerField(default=0)
1116
1117    # start/end times
1118    started = models.DateTimeField(null=True)
1119    ended = models.DateTimeField(null=True)
1120
1121    # in seconds; this is stored to enable sorting
1122    elapsed_time = models.DecimalField(max_digits=8, decimal_places=2, null=True)
1123
1124    # in bytes; note that disk_io is stored to enable sorting
1125    disk_io = models.IntegerField(null=True)
1126    disk_io_read = models.IntegerField(null=True)
1127    disk_io_write = models.IntegerField(null=True)
1128
1129    # in seconds
1130    cpu_time_user = models.DecimalField(max_digits=8, decimal_places=2, null=True)
1131    cpu_time_system = models.DecimalField(max_digits=8, decimal_places=2, null=True)
1132
1133    sstate_result = models.IntegerField(choices=SSTATE_RESULT, default=SSTATE_NA)
1134    message = models.CharField(max_length=240)
1135    logfile = models.FilePathField(max_length=255, blank=True)
1136
1137    outcome_text = property(get_outcome_text)
1138    sstate_text  = property(get_sstate_text)
1139
1140    def __unicode__(self):
1141        return "%d(%d) %s:%s" % (self.pk, self.build.pk, self.recipe.name, self.task_name)
1142
1143    class Meta:
1144        ordering = ('order', 'recipe' ,)
1145        unique_together = ('build', 'recipe', 'task_name', )
1146
1147
1148class Task_Dependency(models.Model):
1149    task = models.ForeignKey(Task, on_delete=models.CASCADE, related_name='task_dependencies_task')
1150    depends_on = models.ForeignKey(Task, on_delete=models.CASCADE, related_name='task_dependencies_depends')
1151
1152class Package(models.Model):
1153    search_allowed_fields = ['name', 'version', 'revision', 'recipe__name', 'recipe__version', 'recipe__license', 'recipe__layer_version__layer__name', 'recipe__layer_version__branch', 'recipe__layer_version__commit', 'recipe__layer_version__local_path', 'installed_name']
1154    build = models.ForeignKey('Build', on_delete=models.CASCADE, null=True)
1155    recipe = models.ForeignKey('Recipe', on_delete=models.CASCADE, null=True)
1156    name = models.CharField(max_length=100)
1157    installed_name = models.CharField(max_length=100, default='')
1158    version = models.CharField(max_length=100, blank=True)
1159    revision = models.CharField(max_length=32, blank=True)
1160    summary = models.TextField(blank=True)
1161    description = models.TextField(blank=True)
1162    size = models.IntegerField(default=0)
1163    installed_size = models.IntegerField(default=0)
1164    section = models.CharField(max_length=80, blank=True)
1165    license = models.CharField(max_length=80, blank=True)
1166
1167    @property
1168    def is_locale_package(self):
1169        """ Returns True if this package is identifiable as a locale package """
1170        if self.name.find('locale') != -1:
1171            return True
1172        return False
1173
1174    @property
1175    def is_packagegroup(self):
1176        """ Returns True is this package is identifiable as a packagegroup """
1177        if self.name.find('packagegroup') != -1:
1178            return True
1179        return False
1180
1181class CustomImagePackage(Package):
1182    # CustomImageRecipe fields to track pacakges appended,
1183    # included and excluded from a CustomImageRecipe
1184    recipe_includes = models.ManyToManyField('CustomImageRecipe',
1185                                             related_name='includes_set')
1186    recipe_excludes = models.ManyToManyField('CustomImageRecipe',
1187                                             related_name='excludes_set')
1188    recipe_appends = models.ManyToManyField('CustomImageRecipe',
1189                                            related_name='appends_set')
1190
1191
1192class Package_DependencyManager(models.Manager):
1193    use_for_related_fields = True
1194    TARGET_LATEST = "use-latest-target-for-target"
1195
1196    def get_queryset(self):
1197        return super(Package_DependencyManager, self).get_queryset().exclude(package_id = F('depends_on__id'))
1198
1199    def for_target_or_none(self, target):
1200        """ filter the dependencies to be displayed by the supplied target
1201        if no dependences are found for the target then try None as the target
1202        which will return the dependences calculated without the context of a
1203        target e.g. non image recipes.
1204
1205        returns: { size, packages }
1206        """
1207        package_dependencies = self.all_depends().order_by('depends_on__name')
1208
1209        if target is self.TARGET_LATEST:
1210            installed_deps =\
1211                    package_dependencies.filter(~Q(target__target=None))
1212        else:
1213            installed_deps =\
1214                    package_dependencies.filter(Q(target__target=target))
1215
1216        packages_list = None
1217        total_size = 0
1218
1219        # If we have installed depdencies for this package and target then use
1220        # these to display
1221        if installed_deps.count() > 0:
1222            packages_list = installed_deps
1223            total_size = installed_deps.aggregate(
1224                Sum('depends_on__size'))['depends_on__size__sum']
1225        else:
1226            new_list = []
1227            package_names = []
1228
1229            # Find dependencies for the package that we know about even if
1230            # it's not installed on a target e.g. from a non-image recipe
1231            for p in package_dependencies.filter(Q(target=None)):
1232                if p.depends_on.name in package_names:
1233                    continue
1234                else:
1235                    package_names.append(p.depends_on.name)
1236                    new_list.append(p.pk)
1237                    # while we're here we may as well total up the size to
1238                    # avoid iterating again
1239                    total_size += p.depends_on.size
1240
1241            # We want to return a queryset here for consistency so pick the
1242            # deps from the new_list
1243            packages_list = package_dependencies.filter(Q(pk__in=new_list))
1244
1245        return {'packages': packages_list,
1246                'size': total_size}
1247
1248    def all_depends(self):
1249        """ Returns just the depends packages and not any other dep_type
1250        Note that this is for any target
1251        """
1252        return self.filter(Q(dep_type=Package_Dependency.TYPE_RDEPENDS) |
1253                           Q(dep_type=Package_Dependency.TYPE_TRDEPENDS))
1254
1255
1256class Package_Dependency(models.Model):
1257    TYPE_RDEPENDS = 0
1258    TYPE_TRDEPENDS = 1
1259    TYPE_RRECOMMENDS = 2
1260    TYPE_TRECOMMENDS = 3
1261    TYPE_RSUGGESTS = 4
1262    TYPE_RPROVIDES = 5
1263    TYPE_RREPLACES = 6
1264    TYPE_RCONFLICTS = 7
1265    ' TODO: bpackage should be changed to remove the DEPENDS_TYPE access '
1266    DEPENDS_TYPE = (
1267        (TYPE_RDEPENDS, "depends"),
1268        (TYPE_TRDEPENDS, "depends"),
1269        (TYPE_TRECOMMENDS, "recommends"),
1270        (TYPE_RRECOMMENDS, "recommends"),
1271        (TYPE_RSUGGESTS, "suggests"),
1272        (TYPE_RPROVIDES, "provides"),
1273        (TYPE_RREPLACES, "replaces"),
1274        (TYPE_RCONFLICTS, "conflicts"),
1275    )
1276    """ Indexed by dep_type, in view order, key for short name and help
1277        description which when viewed will be printf'd with the
1278        package name.
1279    """
1280    DEPENDS_DICT = {
1281        TYPE_RDEPENDS :     ("depends", "%s is required to run %s"),
1282        TYPE_TRDEPENDS :    ("depends", "%s is required to run %s"),
1283        TYPE_TRECOMMENDS :  ("recommends", "%s extends the usability of %s"),
1284        TYPE_RRECOMMENDS :  ("recommends", "%s extends the usability of %s"),
1285        TYPE_RSUGGESTS :    ("suggests", "%s is suggested for installation with %s"),
1286        TYPE_RPROVIDES :    ("provides", "%s is provided by %s"),
1287        TYPE_RREPLACES :    ("replaces", "%s is replaced by %s"),
1288        TYPE_RCONFLICTS :   ("conflicts", "%s conflicts with %s, which will not be installed if this package is not first removed"),
1289    }
1290
1291    package = models.ForeignKey(Package, on_delete=models.CASCADE, related_name='package_dependencies_source')
1292    depends_on = models.ForeignKey(Package, on_delete=models.CASCADE, related_name='package_dependencies_target')   # soft dependency
1293    dep_type = models.IntegerField(choices=DEPENDS_TYPE)
1294    target = models.ForeignKey(Target, on_delete=models.CASCADE, null=True)
1295    objects = Package_DependencyManager()
1296
1297class Target_Installed_Package(models.Model):
1298    target = models.ForeignKey(Target, on_delete=models.CASCADE)
1299    package = models.ForeignKey(Package, on_delete=models.CASCADE, related_name='buildtargetlist_package')
1300
1301
1302class Package_File(models.Model):
1303    package = models.ForeignKey(Package, on_delete=models.CASCADE, related_name='buildfilelist_package')
1304    path = models.FilePathField(max_length=255, blank=True)
1305    size = models.IntegerField()
1306
1307
1308class Recipe(models.Model):
1309    search_allowed_fields = ['name', 'version', 'file_path', 'section',
1310                             'summary', 'description', 'license',
1311                             'layer_version__layer__name',
1312                             'layer_version__branch', 'layer_version__commit',
1313                             'layer_version__local_path',
1314                             'layer_version__layer_source']
1315
1316    up_date = models.DateTimeField(null=True, default=None)
1317
1318    name = models.CharField(max_length=100, blank=True)
1319    version = models.CharField(max_length=100, blank=True)
1320    layer_version = models.ForeignKey('Layer_Version', on_delete=models.CASCADE,
1321                                      related_name='recipe_layer_version')
1322    summary = models.TextField(blank=True)
1323    description = models.TextField(blank=True)
1324    section = models.CharField(max_length=100, blank=True)
1325    license = models.CharField(max_length=200, blank=True)
1326    homepage = models.URLField(blank=True)
1327    bugtracker = models.URLField(blank=True)
1328    file_path = models.FilePathField(max_length=255)
1329    pathflags = models.CharField(max_length=200, blank=True)
1330    is_image = models.BooleanField(default=False)
1331
1332    def __unicode__(self):
1333        return "Recipe " + self.name + ":" + self.version
1334
1335    def get_vcs_recipe_file_link_url(self):
1336        return self.layer_version.get_vcs_file_link_url(self.file_path)
1337
1338    def get_description_or_summary(self):
1339        if self.description:
1340            return self.description
1341        elif self.summary:
1342            return self.summary
1343        else:
1344            return ""
1345
1346    class Meta:
1347        unique_together = (("layer_version", "file_path", "pathflags"), )
1348
1349
1350class Recipe_DependencyManager(models.Manager):
1351    use_for_related_fields = True
1352
1353    def get_queryset(self):
1354        return super(Recipe_DependencyManager, self).get_queryset().exclude(recipe_id = F('depends_on__id'))
1355
1356class Provides(models.Model):
1357    name = models.CharField(max_length=100)
1358    recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
1359
1360class Recipe_Dependency(models.Model):
1361    TYPE_DEPENDS = 0
1362    TYPE_RDEPENDS = 1
1363
1364    DEPENDS_TYPE = (
1365        (TYPE_DEPENDS, "depends"),
1366        (TYPE_RDEPENDS, "rdepends"),
1367    )
1368    recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, related_name='r_dependencies_recipe')
1369    depends_on = models.ForeignKey(Recipe, on_delete=models.CASCADE, related_name='r_dependencies_depends')
1370    via = models.ForeignKey(Provides, on_delete=models.CASCADE, null=True, default=None)
1371    dep_type = models.IntegerField(choices=DEPENDS_TYPE)
1372    objects = Recipe_DependencyManager()
1373
1374
1375class Machine(models.Model):
1376    search_allowed_fields = ["name", "description", "layer_version__layer__name"]
1377    up_date = models.DateTimeField(null = True, default = None)
1378
1379    layer_version = models.ForeignKey('Layer_Version', on_delete=models.CASCADE)
1380    name = models.CharField(max_length=255)
1381    description = models.CharField(max_length=255)
1382
1383    def get_vcs_machine_file_link_url(self):
1384        path = 'conf/machine/'+self.name+'.conf'
1385
1386        return self.layer_version.get_vcs_file_link_url(path)
1387
1388    def __unicode__(self):
1389        return "Machine " + self.name + "(" + self.description + ")"
1390
1391
1392class BitbakeVersion(models.Model):
1393
1394    name = models.CharField(max_length=32, unique = True)
1395    giturl = GitURLField()
1396    branch = models.CharField(max_length=32)
1397    dirpath = models.CharField(max_length=255)
1398
1399    def __unicode__(self):
1400        return "%s (Branch: %s)" % (self.name, self.branch)
1401
1402
1403class Release(models.Model):
1404    """ A release is a project template, used to pre-populate Project settings with a configuration set """
1405    name = models.CharField(max_length=32, unique = True)
1406    description = models.CharField(max_length=255)
1407    bitbake_version = models.ForeignKey(BitbakeVersion, on_delete=models.CASCADE)
1408    branch_name = models.CharField(max_length=50, default = "")
1409    helptext = models.TextField(null=True)
1410
1411    def __unicode__(self):
1412        return "%s (%s)" % (self.name, self.branch_name)
1413
1414    def __str__(self):
1415        return self.name
1416
1417class ReleaseDefaultLayer(models.Model):
1418    release = models.ForeignKey(Release, on_delete=models.CASCADE)
1419    layer_name = models.CharField(max_length=100, default="")
1420
1421
1422class LayerSource(object):
1423    """ Where the layer metadata came from """
1424    TYPE_LOCAL = 0
1425    TYPE_LAYERINDEX = 1
1426    TYPE_IMPORTED = 2
1427    TYPE_BUILD = 3
1428
1429    SOURCE_TYPE = (
1430        (TYPE_LOCAL, "local"),
1431        (TYPE_LAYERINDEX, "layerindex"),
1432        (TYPE_IMPORTED, "imported"),
1433        (TYPE_BUILD, "build"),
1434    )
1435
1436    def types_dict():
1437        """ Turn the TYPES enums into a simple dictionary """
1438        dictionary = {}
1439        for key in LayerSource.__dict__:
1440            if "TYPE" in key:
1441                dictionary[key] = getattr(LayerSource, key)
1442        return dictionary
1443
1444
1445class Layer(models.Model):
1446
1447    up_date = models.DateTimeField(null=True, default=timezone.now)
1448
1449    name = models.CharField(max_length=100)
1450    layer_index_url = models.URLField()
1451    vcs_url = GitURLField(default=None, null=True)
1452    local_source_dir = models.TextField(null=True, default=None)
1453    vcs_web_url = models.URLField(null=True, default=None)
1454    vcs_web_tree_base_url = models.URLField(null=True, default=None)
1455    vcs_web_file_base_url = models.URLField(null=True, default=None)
1456
1457    summary = models.TextField(help_text='One-line description of the layer',
1458                               null=True, default=None)
1459    description = models.TextField(null=True, default=None)
1460
1461    def __unicode__(self):
1462        return "%s / %s " % (self.name, self.summary)
1463
1464
1465class Layer_Version(models.Model):
1466    """
1467    A Layer_Version either belongs to a single project or no project
1468    """
1469    search_allowed_fields = ["layer__name", "layer__summary",
1470                             "layer__description", "layer__vcs_url",
1471                             "dirpath", "release__name", "commit", "branch"]
1472
1473    build = models.ForeignKey(Build, on_delete=models.CASCADE, related_name='layer_version_build',
1474                              default=None, null=True)
1475
1476    layer = models.ForeignKey(Layer, on_delete=models.CASCADE, related_name='layer_version_layer')
1477
1478    layer_source = models.IntegerField(choices=LayerSource.SOURCE_TYPE,
1479                                       default=0)
1480
1481    up_date = models.DateTimeField(null=True, default=timezone.now)
1482
1483    # To which metadata release does this layer version belong to
1484    release = models.ForeignKey(Release, on_delete=models.CASCADE, null=True, default=None)
1485
1486    branch = models.CharField(max_length=80)
1487    commit = models.CharField(max_length=100)
1488    # If the layer is in a subdir
1489    dirpath = models.CharField(max_length=255, null=True, default=None)
1490
1491    # if -1, this is a default layer
1492    priority = models.IntegerField(default=0)
1493
1494    # where this layer exists on the filesystem
1495    local_path = models.FilePathField(max_length=1024, default="/")
1496
1497    # Set if this layer is restricted to a particular project
1498    project = models.ForeignKey('Project', on_delete=models.CASCADE, null=True, default=None)
1499
1500    # code lifted, with adaptations, from the layerindex-web application
1501    # https://git.yoctoproject.org/cgit/cgit.cgi/layerindex-web/
1502    def _handle_url_path(self, base_url, path):
1503        import re, posixpath
1504        if base_url:
1505            if self.dirpath:
1506                if path:
1507                    extra_path = self.dirpath + '/' + path
1508                    # Normalise out ../ in path for usage URL
1509                    extra_path = posixpath.normpath(extra_path)
1510                    # Minor workaround to handle case where subdirectory has been added between branches
1511                    # (should probably support usage URL per branch to handle this... sigh...)
1512                    if extra_path.startswith('../'):
1513                        extra_path = extra_path[3:]
1514                else:
1515                    extra_path = self.dirpath
1516            else:
1517                extra_path = path
1518            branchname = self.release.name
1519            url = base_url.replace('%branch%', branchname)
1520
1521            # If there's a % in the path (e.g. a wildcard bbappend) we need to encode it
1522            if extra_path:
1523                extra_path = extra_path.replace('%', '%25')
1524
1525            if '%path%' in base_url:
1526                if extra_path:
1527                    url = re.sub(r'\[([^\]]*%path%[^\]]*)\]', '\\1', url)
1528                else:
1529                    url = re.sub(r'\[([^\]]*%path%[^\]]*)\]', '', url)
1530                return url.replace('%path%', extra_path)
1531            else:
1532                return url + extra_path
1533        return None
1534
1535    def get_vcs_link_url(self):
1536        if self.layer.vcs_web_url is None:
1537            return None
1538        return self.layer.vcs_web_url
1539
1540    def get_vcs_file_link_url(self, file_path=""):
1541        if self.layer.vcs_web_file_base_url is None:
1542            return None
1543        return self._handle_url_path(self.layer.vcs_web_file_base_url,
1544                                     file_path)
1545
1546    def get_vcs_dirpath_link_url(self):
1547        if self.layer.vcs_web_tree_base_url is None:
1548            return None
1549        return self._handle_url_path(self.layer.vcs_web_tree_base_url, '')
1550
1551    def get_vcs_reference(self):
1552        if self.commit is not None and len(self.commit) > 0:
1553            return self.commit
1554        if self.branch is not None and len(self.branch) > 0:
1555            return self.branch
1556        if self.release is not None:
1557            return self.release.name
1558        return 'N/A'
1559
1560    def get_detailspage_url(self, project_id=None):
1561        """ returns the url to the layer details page uses own project
1562        field if project_id is not specified """
1563
1564        if project_id is None:
1565            project_id = self.project.pk
1566
1567        return reverse('layerdetails', args=(project_id, self.pk))
1568
1569    def get_alldeps(self, project_id):
1570        """Get full list of unique layer dependencies."""
1571        def gen_layerdeps(lver, project, depth):
1572            if depth == 0:
1573                return
1574            for ldep in lver.dependencies.all():
1575                yield ldep.depends_on
1576                # get next level of deps recursively calling gen_layerdeps
1577                for subdep in gen_layerdeps(ldep.depends_on, project, depth-1):
1578                    yield subdep
1579
1580        project = Project.objects.get(pk=project_id)
1581        result = []
1582        projectlvers = [player.layercommit for player in
1583                        project.projectlayer_set.all()]
1584        # protect against infinite layer dependency loops
1585        maxdepth = 20
1586        for dep in gen_layerdeps(self, project, maxdepth):
1587            # filter out duplicates and layers already belonging to the project
1588            if dep not in result + projectlvers:
1589                result.append(dep)
1590
1591        return sorted(result, key=lambda x: x.layer.name)
1592
1593    def __unicode__(self):
1594        return ("id %d belongs to layer: %s" % (self.pk, self.layer.name))
1595
1596    def __str__(self):
1597        if self.release:
1598            release = self.release.name
1599        else:
1600            release = "No release set"
1601
1602        return "%d %s (%s)" % (self.pk, self.layer.name, release)
1603
1604
1605class LayerVersionDependency(models.Model):
1606
1607    layer_version = models.ForeignKey(Layer_Version, on_delete=models.CASCADE,
1608                                      related_name="dependencies")
1609    depends_on = models.ForeignKey(Layer_Version, on_delete=models.CASCADE,
1610                                   related_name="dependees")
1611
1612class ProjectLayer(models.Model):
1613    project = models.ForeignKey(Project, on_delete=models.CASCADE)
1614    layercommit = models.ForeignKey(Layer_Version, on_delete=models.CASCADE, null=True)
1615    optional = models.BooleanField(default = True)
1616
1617    def __unicode__(self):
1618        return "%s, %s" % (self.project.name, self.layercommit)
1619
1620    class Meta:
1621        unique_together = (("project", "layercommit"),)
1622
1623class CustomImageRecipe(Recipe):
1624
1625    # CustomImageRecipe's belong to layers called:
1626    LAYER_NAME = "toaster-custom-images"
1627
1628    search_allowed_fields = ['name']
1629    base_recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE, related_name='based_on_recipe')
1630    project = models.ForeignKey(Project, on_delete=models.CASCADE)
1631    last_updated = models.DateTimeField(null=True, default=None)
1632
1633    def get_last_successful_built_target(self):
1634        """ Return the last successful built target object if one exists
1635        otherwise return None """
1636        return Target.objects.filter(Q(build__outcome=Build.SUCCEEDED) &
1637                                     Q(build__project=self.project) &
1638                                     Q(target=self.name)).last()
1639
1640    def update_package_list(self):
1641        """ Update the package list from the last good build of this
1642        CustomImageRecipe
1643        """
1644        # Check if we're aldready up-to-date or not
1645        target = self.get_last_successful_built_target()
1646        if target is None:
1647            # So we've never actually built this Custom recipe but what about
1648            # the recipe it's based on?
1649            target = \
1650                Target.objects.filter(Q(build__outcome=Build.SUCCEEDED) &
1651                                      Q(build__project=self.project) &
1652                                      Q(target=self.base_recipe.name)).last()
1653            if target is None:
1654                return
1655
1656        if target.build.completed_on == self.last_updated:
1657            return
1658
1659        self.includes_set.clear()
1660
1661        excludes_list = self.excludes_set.values_list('name', flat=True)
1662        appends_list = self.appends_set.values_list('name', flat=True)
1663
1664        built_packages_list = \
1665            target.target_installed_package_set.values_list('package__name',
1666                                                            flat=True)
1667        for built_package in built_packages_list:
1668            # Is the built package in the custom packages list?
1669            if built_package in excludes_list:
1670                continue
1671
1672            if built_package in appends_list:
1673                continue
1674
1675            cust_img_p = \
1676                    CustomImagePackage.objects.get(name=built_package)
1677            self.includes_set.add(cust_img_p)
1678
1679
1680        self.last_updated = target.build.completed_on
1681        self.save()
1682
1683    def get_all_packages(self):
1684        """Get the included packages and any appended packages"""
1685        self.update_package_list()
1686
1687        return CustomImagePackage.objects.filter((Q(recipe_appends=self) |
1688                                                  Q(recipe_includes=self)) &
1689                                                 ~Q(recipe_excludes=self))
1690
1691    def get_base_recipe_file(self):
1692        """Get the base recipe file path if it exists on the file system"""
1693        path_schema_one = "%s/%s" % (self.base_recipe.layer_version.local_path,
1694                                     self.base_recipe.file_path)
1695
1696        path_schema_two = self.base_recipe.file_path
1697
1698        path_schema_three = "%s/%s" % (self.base_recipe.layer_version.layer.local_source_dir,
1699                                     self.base_recipe.file_path)
1700
1701        if os.path.exists(path_schema_one):
1702            return path_schema_one
1703
1704        # The path may now be the full path if the recipe has been built
1705        if os.path.exists(path_schema_two):
1706            return path_schema_two
1707
1708        # Or a local path if all layers are local
1709        if os.path.exists(path_schema_three):
1710            return path_schema_three
1711
1712        return None
1713
1714    def generate_recipe_file_contents(self):
1715        """Generate the contents for the recipe file."""
1716        # If we have no excluded packages we only need to :append
1717        if self.excludes_set.count() == 0:
1718            packages_conf = "IMAGE_INSTALL:append = \" "
1719
1720            for pkg in self.appends_set.all():
1721                packages_conf += pkg.name+' '
1722        else:
1723            packages_conf = "IMAGE_FEATURES =\"\"\nIMAGE_INSTALL = \""
1724            # We add all the known packages to be built by this recipe apart
1725            # from locale packages which are are controlled with IMAGE_LINGUAS.
1726            for pkg in self.get_all_packages().exclude(
1727                    name__icontains="locale"):
1728                packages_conf += pkg.name+' '
1729
1730        packages_conf += "\""
1731
1732        base_recipe_path = self.get_base_recipe_file()
1733        if base_recipe_path and os.path.isfile(base_recipe_path):
1734            base_recipe = open(base_recipe_path, 'r').read()
1735        else:
1736            # Pass back None to trigger error message to user
1737            return None
1738
1739        # Add a special case for when the recipe we have based a custom image
1740        # recipe on requires another recipe.
1741        # For example:
1742        # "require core-image-minimal.bb" is changed to:
1743        # "require recipes-core/images/core-image-minimal.bb"
1744
1745        req_search = re.search(r'(require\s+)(.+\.bb\s*$)',
1746                               base_recipe,
1747                               re.MULTILINE)
1748        if req_search:
1749            require_filename = req_search.group(2).strip()
1750
1751            corrected_location = Recipe.objects.filter(
1752                Q(layer_version=self.base_recipe.layer_version) &
1753                Q(file_path__icontains=require_filename)).last().file_path
1754
1755            new_require_line = "require %s" % corrected_location
1756
1757            base_recipe = base_recipe.replace(req_search.group(0),
1758                                              new_require_line)
1759
1760        info = {
1761            "date": timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
1762            "base_recipe": base_recipe,
1763            "recipe_name": self.name,
1764            "base_recipe_name": self.base_recipe.name,
1765            "license": self.license,
1766            "summary": self.summary,
1767            "description": self.description,
1768            "packages_conf": packages_conf.strip()
1769        }
1770
1771        recipe_contents = ("# Original recipe %(base_recipe_name)s \n"
1772                           "%(base_recipe)s\n\n"
1773                           "# Recipe %(recipe_name)s \n"
1774                           "# Customisation Generated by Toaster on %(date)s\n"
1775                           "SUMMARY = \"%(summary)s\"\n"
1776                           "DESCRIPTION = \"%(description)s\"\n"
1777                           "LICENSE = \"%(license)s\"\n"
1778                           "%(packages_conf)s") % info
1779
1780        return recipe_contents
1781
1782class ProjectVariable(models.Model):
1783    project = models.ForeignKey(Project, on_delete=models.CASCADE)
1784    name = models.CharField(max_length=100)
1785    value = models.TextField(blank = True)
1786
1787class Variable(models.Model):
1788    search_allowed_fields = ['variable_name', 'variable_value',
1789                             'vhistory__file_name', "description"]
1790    build = models.ForeignKey(Build, on_delete=models.CASCADE, related_name='variable_build')
1791    variable_name = models.CharField(max_length=100)
1792    variable_value = models.TextField(blank=True)
1793    changed = models.BooleanField(default=False)
1794    human_readable_name = models.CharField(max_length=200)
1795    description = models.TextField(blank=True)
1796
1797class VariableHistory(models.Model):
1798    variable = models.ForeignKey(Variable, on_delete=models.CASCADE, related_name='vhistory')
1799    value   = models.TextField(blank=True)
1800    file_name = models.FilePathField(max_length=255)
1801    line_number = models.IntegerField(null=True)
1802    operation = models.CharField(max_length=64)
1803
1804class HelpText(models.Model):
1805    VARIABLE = 0
1806    HELPTEXT_AREA = ((VARIABLE, 'variable'), )
1807
1808    build = models.ForeignKey(Build, on_delete=models.CASCADE, related_name='helptext_build')
1809    area = models.IntegerField(choices=HELPTEXT_AREA)
1810    key = models.CharField(max_length=100)
1811    text = models.TextField()
1812
1813class LogMessage(models.Model):
1814    EXCEPTION = -1      # used to signal self-toaster-exceptions
1815    INFO = 0
1816    WARNING = 1
1817    ERROR = 2
1818    CRITICAL = 3
1819
1820    LOG_LEVEL = (
1821        (INFO, "info"),
1822        (WARNING, "warn"),
1823        (ERROR, "error"),
1824        (CRITICAL, "critical"),
1825        (EXCEPTION, "toaster exception")
1826    )
1827
1828    build = models.ForeignKey(Build, on_delete=models.CASCADE)
1829    task  = models.ForeignKey(Task, on_delete=models.CASCADE, blank = True, null=True)
1830    level = models.IntegerField(choices=LOG_LEVEL, default=INFO)
1831    message = models.TextField(blank=True, null=True)
1832    pathname = models.FilePathField(max_length=255, blank=True)
1833    lineno = models.IntegerField(null=True)
1834
1835    def __str__(self):
1836        return force_bytes('%s %s %s' % (self.get_level_display(), self.message, self.build))
1837
1838def invalidate_cache(**kwargs):
1839    from django.core.cache import cache
1840    try:
1841      cache.clear()
1842    except Exception as e:
1843      logger.warning("Problem with cache backend: Failed to clear cache: %s" % e)
1844
1845def signal_runbuilds():
1846    """Send SIGUSR1 to runbuilds process"""
1847    try:
1848        with open(os.path.join(os.getenv('BUILDDIR', '.'),
1849                               '.runbuilds.pid')) as pidf:
1850            os.kill(int(pidf.read()), SIGUSR1)
1851    except FileNotFoundError:
1852        logger.info("Stopping existing runbuilds: no current process found")
1853    except ProcessLookupError:
1854        logger.warning("Stopping existing runbuilds: process lookup not found")
1855
1856class Distro(models.Model):
1857    search_allowed_fields = ["name", "description", "layer_version__layer__name"]
1858    up_date = models.DateTimeField(null = True, default = None)
1859
1860    layer_version = models.ForeignKey('Layer_Version', on_delete=models.CASCADE)
1861    name = models.CharField(max_length=255)
1862    description = models.CharField(max_length=255)
1863
1864    def get_vcs_distro_file_link_url(self):
1865        path = 'conf/distro/%s.conf' % self.name
1866        return self.layer_version.get_vcs_file_link_url(path)
1867
1868    def __unicode__(self):
1869        return "Distro " + self.name + "(" + self.description + ")"
1870
1871class EventLogsImports(models.Model):
1872    name = models.CharField(max_length=255)
1873    imported = models.BooleanField(default=False)
1874    build_id = models.IntegerField(blank=True, null=True)
1875
1876    def __str__(self):
1877        return self.name
1878
1879
1880django.db.models.signals.post_save.connect(invalidate_cache)
1881django.db.models.signals.post_delete.connect(invalidate_cache)
1882django.db.models.signals.m2m_changed.connect(invalidate_cache)
1883