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