blob: 0b83b991b955b6a7a3869d27879264124c2142a7 [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001#
2# ex:ts=4:sw=4:sts=4:et
3# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
4#
5# BitBake Toaster Implementation
6#
7# Copyright (C) 2013 Intel Corporation
8#
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License version 2 as
11# published by the Free Software Foundation.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License along
19# with this program; if not, write to the Free Software Foundation, Inc.,
20# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050022from __future__ import unicode_literals
23
Patrick Williamsc124f4f2015-09-15 14:41:29 -050024from django.db import models, IntegrityError
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050025from django.db.models import F, Q, Avg, Max, Sum
Patrick Williamsc124f4f2015-09-15 14:41:29 -050026from django.utils import timezone
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050027from django.utils.encoding import force_bytes
Patrick Williamsc124f4f2015-09-15 14:41:29 -050028
29from django.core.urlresolvers import reverse
30
31from django.core import validators
32from django.conf import settings
33import django.db.models.signals
34
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050035import os.path
36import re
37import itertools
Patrick Williamsc124f4f2015-09-15 14:41:29 -050038
39import logging
40logger = logging.getLogger("toaster")
41
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050042if 'sqlite' in settings.DATABASES['default']['ENGINE']:
43 from django.db import transaction, OperationalError
44 from time import sleep
45
46 _base_save = models.Model.save
47 def save(self, *args, **kwargs):
48 while True:
49 try:
50 with transaction.atomic():
51 return _base_save(self, *args, **kwargs)
52 except OperationalError as err:
53 if 'database is locked' in str(err):
54 logger.warning("%s, model: %s, args: %s, kwargs: %s",
55 err, self.__class__, args, kwargs)
56 sleep(0.5)
57 continue
58 raise
59
60 models.Model.save = save
61
62 # HACK: Monkey patch Django to fix 'database is locked' issue
63
64 from django.db.models.query import QuerySet
65 _base_insert = QuerySet._insert
66 def _insert(self, *args, **kwargs):
67 with transaction.atomic(using=self.db, savepoint=False):
68 return _base_insert(self, *args, **kwargs)
69 QuerySet._insert = _insert
70
71 from django.utils import six
72 def _create_object_from_params(self, lookup, params):
73 """
74 Tries to create an object using passed params.
75 Used by get_or_create and update_or_create
76 """
77 try:
78 obj = self.create(**params)
79 return obj, True
80 except IntegrityError:
81 exc_info = sys.exc_info()
82 try:
83 return self.get(**lookup), False
84 except self.model.DoesNotExist:
85 pass
86 six.reraise(*exc_info)
87
88 QuerySet._create_object_from_params = _create_object_from_params
89
90 # end of HACK
Patrick Williamsc124f4f2015-09-15 14:41:29 -050091
92class GitURLValidator(validators.URLValidator):
93 import re
94 regex = re.compile(
95 r'^(?:ssh|git|http|ftp)s?://' # http:// or https://
96 r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain...
97 r'localhost|' # localhost...
98 r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4
99 r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' # ...or ipv6
100 r'(?::\d+)?' # optional port
101 r'(?:/?|[/?]\S+)$', re.IGNORECASE)
102
103def GitURLField(**kwargs):
104 r = models.URLField(**kwargs)
105 for i in xrange(len(r.validators)):
106 if isinstance(r.validators[i], validators.URLValidator):
107 r.validators[i] = GitURLValidator()
108 return r
109
110
111class ToasterSetting(models.Model):
112 name = models.CharField(max_length=63)
113 helptext = models.TextField()
114 value = models.CharField(max_length=255)
115
116 def __unicode__(self):
117 return "Setting %s = %s" % (self.name, self.value)
118
119class ProjectManager(models.Manager):
120 def create_project(self, name, release):
121 if release is not None:
122 prj = self.model(name = name, bitbake_version = release.bitbake_version, release = release)
123 else:
124 prj = self.model(name = name, bitbake_version = None, release = None)
125
126 prj.save()
127
128 for defaultconf in ToasterSetting.objects.filter(name__startswith="DEFCONF_"):
129 name = defaultconf.name[8:]
130 ProjectVariable.objects.create( project = prj,
131 name = name,
132 value = defaultconf.value)
133
134 if release is None:
135 return prj
136
137 for rdl in release.releasedefaultlayer_set.all():
138 try:
139 lv = Layer_Version.objects.filter(layer__name = rdl.layer_name, up_branch__name = release.branch_name)[0].get_equivalents_wpriority(prj)[0]
140 ProjectLayer.objects.create( project = prj,
141 layercommit = lv,
142 optional = False )
143 except IndexError:
144 # we may have no valid layer version objects, and that's ok
145 pass
146
147 return prj
148
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500149 # return single object with is_default = True
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500150 def get_or_create_default_project(self):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500151 projects = super(ProjectManager, self).filter(is_default = True)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500152
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500153 if len(projects) > 1:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500154 raise Exception('Inconsistent project data: multiple ' +
155 'default projects (i.e. with is_default=True)')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500156 elif len(projects) < 1:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500157 options = {
158 'name': 'Command line builds',
159 'short_description': 'Project for builds started outside Toaster',
160 'is_default': True
161 }
162 project = Project.objects.create(**options)
163 project.save()
164
165 return project
166 else:
167 return projects[0]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500168
169class Project(models.Model):
170 search_allowed_fields = ['name', 'short_description', 'release__name', 'release__branch_name']
171 name = models.CharField(max_length=100)
172 short_description = models.CharField(max_length=50, blank=True)
173 bitbake_version = models.ForeignKey('BitbakeVersion', null=True)
174 release = models.ForeignKey("Release", null=True)
175 created = models.DateTimeField(auto_now_add = True)
176 updated = models.DateTimeField(auto_now = True)
177 # This is a horrible hack; since Toaster has no "User" model available when
178 # running in interactive mode, we can't reference the field here directly
179 # Instead, we keep a possible null reference to the User id, as not to force
180 # hard links to possibly missing models
181 user_id = models.IntegerField(null = True)
182 objects = ProjectManager()
183
184 # set to True for the project which is the default container
185 # for builds initiated by the command line etc.
186 is_default = models.BooleanField(default = False)
187
188 def __unicode__(self):
189 return "%s (Release %s, BBV %s)" % (self.name, self.release, self.bitbake_version)
190
191 def get_current_machine_name(self):
192 try:
193 return self.projectvariable_set.get(name="MACHINE").value
194 except (ProjectVariable.DoesNotExist,IndexError):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500195 return None;
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500196
197 def get_number_of_builds(self):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500198 """Return the number of builds which have ended"""
199
200 return self.build_set.exclude(
201 Q(outcome=Build.IN_PROGRESS) |
202 Q(outcome=Build.CANCELLED)
203 ).count()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500204
205 def get_last_build_id(self):
206 try:
207 return Build.objects.filter( project = self.id ).order_by('-completed_on')[0].id
208 except (Build.DoesNotExist,IndexError):
209 return( -1 )
210
211 def get_last_outcome(self):
212 build_id = self.get_last_build_id
213 if (-1 == build_id):
214 return( "" )
215 try:
216 return Build.objects.filter( id = self.get_last_build_id )[ 0 ].outcome
217 except (Build.DoesNotExist,IndexError):
218 return( "not_found" )
219
220 def get_last_target(self):
221 build_id = self.get_last_build_id
222 if (-1 == build_id):
223 return( "" )
224 try:
225 return Target.objects.filter(build = build_id)[0].target
226 except (Target.DoesNotExist,IndexError):
227 return( "not_found" )
228
229 def get_last_errors(self):
230 build_id = self.get_last_build_id
231 if (-1 == build_id):
232 return( 0 )
233 try:
234 return Build.objects.filter(id = build_id)[ 0 ].errors.count()
235 except (Build.DoesNotExist,IndexError):
236 return( "not_found" )
237
238 def get_last_warnings(self):
239 build_id = self.get_last_build_id
240 if (-1 == build_id):
241 return( 0 )
242 try:
243 return Build.objects.filter(id = build_id)[ 0 ].warnings.count()
244 except (Build.DoesNotExist,IndexError):
245 return( "not_found" )
246
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500247 def get_last_build_extensions(self):
248 """
249 Get list of file name extensions for images produced by the most
250 recent build
251 """
252 last_build = Build.objects.get(pk = self.get_last_build_id())
253 return last_build.get_image_file_extensions()
254
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500255 def get_last_imgfiles(self):
256 build_id = self.get_last_build_id
257 if (-1 == build_id):
258 return( "" )
259 try:
260 return Variable.objects.filter(build = build_id, variable_name = "IMAGE_FSTYPES")[ 0 ].variable_value
261 except (Variable.DoesNotExist,IndexError):
262 return( "not_found" )
263
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500264 def get_all_compatible_layer_versions(self):
265 """ Returns Queryset of all Layer_Versions which are compatible with
266 this project"""
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500267 queryset = None
268
269 # guard on release, as it can be null
270 if self.release:
271 queryset = Layer_Version.objects.filter(
272 (Q(up_branch__name=self.release.branch_name) &
273 Q(build=None) &
274 Q(project=None)) |
275 Q(project=self))
276 else:
277 queryset = Layer_Version.objects.none()
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500278
279 return queryset
280
281 def get_project_layer_versions(self, pk=False):
282 """ Returns the Layer_Versions currently added to this project """
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500283 layer_versions = self.projectlayer_set.all().values_list('layercommit',
284 flat=True)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500285
286 if pk is False:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500287 return Layer_Version.objects.filter(pk__in=layer_versions)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500288 else:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500289 return layer_versions
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500290
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500291
292 def get_available_machines(self):
293 """ Returns QuerySet of all Machines which are provided by the
294 Layers currently added to the Project """
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500295 queryset = Machine.objects.filter(
296 layer_version__in=self.get_project_layer_versions())
297
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500298 return queryset
299
300 def get_all_compatible_machines(self):
301 """ Returns QuerySet of all the compatible machines available to the
302 project including ones from Layers not currently added """
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500303 queryset = Machine.objects.filter(
304 layer_version__in=self.get_all_compatible_layer_versions())
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500305
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500306 return queryset
307
308 def get_available_recipes(self):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500309 """ Returns QuerySet of all the recipes that are provided by layers
310 added to this project """
311 queryset = Recipe.objects.filter(
312 layer_version__in=self.get_project_layer_versions())
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500313
314 return queryset
315
316 def get_all_compatible_recipes(self):
317 """ Returns QuerySet of all the compatible Recipes available to the
318 project including ones from Layers not currently added """
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500319 queryset = Recipe.objects.filter(
320 layer_version__in=self.get_all_compatible_layer_versions()).exclude(name__exact='')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500321
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500322 return queryset
323
324
325 def schedule_build(self):
326 from bldcontrol.models import BuildRequest, BRTarget, BRLayer, BRVariable, BRBitbake
327 br = BuildRequest.objects.create(project = self)
328 try:
329
330 BRBitbake.objects.create(req = br,
331 giturl = self.bitbake_version.giturl,
332 commit = self.bitbake_version.branch,
333 dirpath = self.bitbake_version.dirpath)
334
335 for l in self.projectlayer_set.all().order_by("pk"):
336 commit = l.layercommit.get_vcs_reference()
337 print("ii Building layer ", l.layercommit.layer.name, " at vcs point ", commit)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500338 BRLayer.objects.create(req = br, name = l.layercommit.layer.name, giturl = l.layercommit.layer.vcs_url, commit = commit, dirpath = l.layercommit.dirpath, layer_version=l.layercommit)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500339
340 br.state = BuildRequest.REQ_QUEUED
341 now = timezone.now()
342 br.build = Build.objects.create(project = self,
343 completed_on=now,
344 started_on=now,
345 )
346 for t in self.projecttarget_set.all():
347 BRTarget.objects.create(req = br, target = t.target, task = t.task)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500348 Target.objects.create(build = br.build, target = t.target, task = t.task)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500349
350 for v in self.projectvariable_set.all():
351 BRVariable.objects.create(req = br, name = v.name, value = v.value)
352
353
354 try:
355 br.build.machine = self.projectvariable_set.get(name = 'MACHINE').value
356 br.build.save()
357 except ProjectVariable.DoesNotExist:
358 pass
359 br.save()
360 except Exception:
361 # revert the build request creation since we're not done cleanly
362 br.delete()
363 raise
364 return br
365
366class Build(models.Model):
367 SUCCEEDED = 0
368 FAILED = 1
369 IN_PROGRESS = 2
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500370 CANCELLED = 3
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500371
372 BUILD_OUTCOME = (
373 (SUCCEEDED, 'Succeeded'),
374 (FAILED, 'Failed'),
375 (IN_PROGRESS, 'In Progress'),
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500376 (CANCELLED, 'Cancelled'),
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500377 )
378
379 search_allowed_fields = ['machine', 'cooker_log_path', "target__target", "target__target_image_file__file_name"]
380
381 project = models.ForeignKey(Project) # must have a project
382 machine = models.CharField(max_length=100)
383 distro = models.CharField(max_length=100)
384 distro_version = models.CharField(max_length=100)
385 started_on = models.DateTimeField()
386 completed_on = models.DateTimeField()
387 outcome = models.IntegerField(choices=BUILD_OUTCOME, default=IN_PROGRESS)
388 cooker_log_path = models.CharField(max_length=500)
389 build_name = models.CharField(max_length=100)
390 bitbake_version = models.CharField(max_length=50)
391
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500392 @staticmethod
393 def get_recent(project=None):
394 """
395 Return recent builds as a list; if project is set, only return
396 builds for that project
397 """
398
399 builds = Build.objects.all()
400
401 if project:
402 builds = builds.filter(project=project)
403
404 finished_criteria = \
405 Q(outcome=Build.SUCCEEDED) | \
406 Q(outcome=Build.FAILED) | \
407 Q(outcome=Build.CANCELLED)
408
409 recent_builds = list(itertools.chain(
410 builds.filter(outcome=Build.IN_PROGRESS).order_by("-started_on"),
411 builds.filter(finished_criteria).order_by("-completed_on")[:3]
412 ))
413
414 # add percentage done property to each build; this is used
415 # to show build progress in mrb_section.html
416 for build in recent_builds:
417 build.percentDone = build.completeper()
418
419 return recent_builds
420
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500421 def completeper(self):
422 tf = Task.objects.filter(build = self)
423 tfc = tf.count()
424 if tfc > 0:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500425 completeper = tf.exclude(order__isnull=True).count()*100/tfc
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500426 else:
427 completeper = 0
428 return completeper
429
430 def eta(self):
431 eta = timezone.now()
432 completeper = self.completeper()
433 if self.completeper() > 0:
434 eta += ((eta - self.started_on)*(100-completeper))/completeper
435 return eta
436
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500437 def get_image_file_extensions(self):
438 """
439 Get list of file name extensions for images produced by this build
440 """
441 targets = Target.objects.filter(build_id = self.id)
442 extensions = []
443
444 # pattern to match against file path for building extension string
445 pattern = re.compile('\.([^\.]+?)$')
446
447 for target in targets:
448 if (not target.is_image):
449 continue
450
451 target_image_files = Target_Image_File.objects.filter(target_id = target.id)
452
453 for target_image_file in target_image_files:
454 file_name = os.path.basename(target_image_file.file_name)
455 suffix = ''
456
457 continue_matching = True
458
459 # incrementally extract the suffix from the file path,
460 # checking it against the list of valid suffixes at each
461 # step; if the path is stripped of all potential suffix
462 # parts without matching a valid suffix, this returns all
463 # characters after the first '.' in the file name
464 while continue_matching:
465 matches = pattern.search(file_name)
466
467 if None == matches:
468 continue_matching = False
469 suffix = re.sub('^\.', '', suffix)
470 continue
471 else:
472 suffix = matches.group(1) + suffix
473
474 if suffix in Target_Image_File.SUFFIXES:
475 continue_matching = False
476 continue
477 else:
478 # reduce the file name and try to find the next
479 # segment from the path which might be part
480 # of the suffix
481 file_name = re.sub('.' + matches.group(1), '', file_name)
482 suffix = '.' + suffix
483
484 if not suffix in extensions:
485 extensions.append(suffix)
486
487 return ', '.join(extensions)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500488
489 def get_sorted_target_list(self):
490 tgts = Target.objects.filter(build_id = self.id).order_by( 'target' );
491 return( tgts );
492
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500493 def get_recipes(self):
494 """
495 Get the recipes related to this build;
496 note that the related layer versions and layers are also prefetched
497 by this query, as this queryset can be sorted by these objects in the
498 build recipes view; prefetching them here removes the need
499 for another query in that view
500 """
501 layer_versions = Layer_Version.objects.filter(build=self)
502 criteria = Q(layer_version__id__in=layer_versions)
503 return Recipe.objects.filter(criteria) \
504 .select_related('layer_version', 'layer_version__layer')
505
506 def get_image_recipes(self):
507 """
508 Returns a list of image Recipes (custom and built-in) related to this
509 build, sorted by name; note that this has to be done in two steps, as
510 there's no way to get all the custom image recipes and image recipes
511 in one query
512 """
513 custom_image_recipes = self.get_custom_image_recipes()
514 custom_image_recipe_names = custom_image_recipes.values_list('name', flat=True)
515
516 not_custom_image_recipes = ~Q(name__in=custom_image_recipe_names) & \
517 Q(is_image=True)
518
519 built_image_recipes = self.get_recipes().filter(not_custom_image_recipes)
520
521 # append to the custom image recipes and sort
522 customisable_image_recipes = list(
523 itertools.chain(custom_image_recipes, built_image_recipes)
524 )
525
526 return sorted(customisable_image_recipes, key=lambda recipe: recipe.name)
527
528 def get_custom_image_recipes(self):
529 """
530 Returns a queryset of CustomImageRecipes related to this build,
531 sorted by name
532 """
533 built_recipe_names = self.get_recipes().values_list('name', flat=True)
534 criteria = Q(name__in=built_recipe_names) & Q(project=self.project)
535 queryset = CustomImageRecipe.objects.filter(criteria).order_by('name')
536 return queryset
537
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500538 def get_outcome_text(self):
539 return Build.BUILD_OUTCOME[int(self.outcome)][1]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500540
541 @property
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500542 def failed_tasks(self):
543 """ Get failed tasks for the build """
544 tasks = self.task_build.all()
545 return tasks.filter(order__gt=0, outcome=Task.OUTCOME_FAILED)
546
547 @property
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500548 def errors(self):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500549 return (self.logmessage_set.filter(level=LogMessage.ERROR) |
550 self.logmessage_set.filter(level=LogMessage.EXCEPTION) |
551 self.logmessage_set.filter(level=LogMessage.CRITICAL))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500552
553 @property
554 def warnings(self):
555 return self.logmessage_set.filter(level=LogMessage.WARNING)
556
557 @property
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500558 def timespent(self):
559 return self.completed_on - self.started_on
560
561 @property
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500562 def timespent_seconds(self):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500563 return self.timespent.total_seconds()
564
565 @property
566 def target_labels(self):
567 """
568 Sorted (a-z) "target1:task, target2, target3" etc. string for all
569 targets in this build
570 """
571 targets = self.target_set.all()
572 target_labels = [target.target +
573 (':' + target.task if target.task else '')
574 for target in targets]
575 target_labels.sort()
576
577 return target_labels
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500578
579 def get_current_status(self):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500580 """
581 get the status string from the build request if the build
582 has one, or the text for the build outcome if it doesn't
583 """
584
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500585 from bldcontrol.models import BuildRequest
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500586
587 build_request = None
588 if hasattr(self, 'buildrequest'):
589 build_request = self.buildrequest
590
591 if (build_request
592 and build_request.state != BuildRequest.REQ_INPROGRESS
593 and self.outcome == Build.IN_PROGRESS):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500594 return self.buildrequest.get_state_display()
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500595 else:
596 return self.get_outcome_text()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500597
598 def __str__(self):
599 return "%d %s %s" % (self.id, self.project, ",".join([t.target for t in self.target_set.all()]))
600
601
602# an Artifact is anything that results from a Build, and may be of interest to the user, and is not stored elsewhere
603class BuildArtifact(models.Model):
604 build = models.ForeignKey(Build)
605 file_name = models.FilePathField()
606 file_size = models.IntegerField()
607
608 def get_local_file_name(self):
609 try:
610 deploydir = Variable.objects.get(build = self.build, variable_name="DEPLOY_DIR").variable_value
611 return self.file_name[len(deploydir)+1:]
612 except:
613 raise
614
615 return self.file_name
616
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500617 def get_basename(self):
618 return os.path.basename(self.file_name)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500619
620 def is_available(self):
621 return self.build.buildrequest.environment.has_artifact(self.file_name)
622
623class ProjectTarget(models.Model):
624 project = models.ForeignKey(Project)
625 target = models.CharField(max_length=100)
626 task = models.CharField(max_length=100, null=True)
627
628class Target(models.Model):
629 search_allowed_fields = ['target', 'file_name']
630 build = models.ForeignKey(Build)
631 target = models.CharField(max_length=100)
632 task = models.CharField(max_length=100, null=True)
633 is_image = models.BooleanField(default = False)
634 image_size = models.IntegerField(default=0)
635 license_manifest_path = models.CharField(max_length=500, null=True)
636
637 def package_count(self):
638 return Target_Installed_Package.objects.filter(target_id__exact=self.id).count()
639
640 def __unicode__(self):
641 return self.target
642
643class Target_Image_File(models.Model):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500644 # valid suffixes for image files produced by a build
645 SUFFIXES = {
646 'btrfs', 'cpio', 'cpio.gz', 'cpio.lz4', 'cpio.lzma', 'cpio.xz',
647 'cramfs', 'elf', 'ext2', 'ext2.bz2', 'ext2.gz', 'ext2.lzma', 'ext4',
648 'ext4.gz', 'ext3', 'ext3.gz', 'hddimg', 'iso', 'jffs2', 'jffs2.sum',
649 'squashfs', 'squashfs-lzo', 'squashfs-xz', 'tar.bz2', 'tar.lz4',
650 'tar.xz', 'tartar.gz', 'ubi', 'ubifs', 'vmdk'
651 }
652
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500653 target = models.ForeignKey(Target)
654 file_name = models.FilePathField(max_length=254)
655 file_size = models.IntegerField()
656
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500657 @property
658 def suffix(self):
659 filename, suffix = os.path.splitext(self.file_name)
660 suffix = suffix.lstrip('.')
661 return suffix
662
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500663class Target_File(models.Model):
664 ITYPE_REGULAR = 1
665 ITYPE_DIRECTORY = 2
666 ITYPE_SYMLINK = 3
667 ITYPE_SOCKET = 4
668 ITYPE_FIFO = 5
669 ITYPE_CHARACTER = 6
670 ITYPE_BLOCK = 7
671 ITYPES = ( (ITYPE_REGULAR ,'regular'),
672 ( ITYPE_DIRECTORY ,'directory'),
673 ( ITYPE_SYMLINK ,'symlink'),
674 ( ITYPE_SOCKET ,'socket'),
675 ( ITYPE_FIFO ,'fifo'),
676 ( ITYPE_CHARACTER ,'character'),
677 ( ITYPE_BLOCK ,'block'),
678 )
679
680 target = models.ForeignKey(Target)
681 path = models.FilePathField()
682 size = models.IntegerField()
683 inodetype = models.IntegerField(choices = ITYPES)
684 permission = models.CharField(max_length=16)
685 owner = models.CharField(max_length=128)
686 group = models.CharField(max_length=128)
687 directory = models.ForeignKey('Target_File', related_name="directory_set", null=True)
688 sym_target = models.ForeignKey('Target_File', related_name="symlink_set", null=True)
689
690
691class Task(models.Model):
692
693 SSTATE_NA = 0
694 SSTATE_MISS = 1
695 SSTATE_FAILED = 2
696 SSTATE_RESTORED = 3
697
698 SSTATE_RESULT = (
699 (SSTATE_NA, 'Not Applicable'), # For rest of tasks, but they still need checking.
700 (SSTATE_MISS, 'File not in cache'), # the sstate object was not found
701 (SSTATE_FAILED, 'Failed'), # there was a pkg, but the script failed
702 (SSTATE_RESTORED, 'Succeeded'), # successfully restored
703 )
704
705 CODING_NA = 0
706 CODING_PYTHON = 2
707 CODING_SHELL = 3
708
709 TASK_CODING = (
710 (CODING_NA, 'N/A'),
711 (CODING_PYTHON, 'Python'),
712 (CODING_SHELL, 'Shell'),
713 )
714
715 OUTCOME_NA = -1
716 OUTCOME_SUCCESS = 0
717 OUTCOME_COVERED = 1
718 OUTCOME_CACHED = 2
719 OUTCOME_PREBUILT = 3
720 OUTCOME_FAILED = 4
721 OUTCOME_EMPTY = 5
722
723 TASK_OUTCOME = (
724 (OUTCOME_NA, 'Not Available'),
725 (OUTCOME_SUCCESS, 'Succeeded'),
726 (OUTCOME_COVERED, 'Covered'),
727 (OUTCOME_CACHED, 'Cached'),
728 (OUTCOME_PREBUILT, 'Prebuilt'),
729 (OUTCOME_FAILED, 'Failed'),
730 (OUTCOME_EMPTY, 'Empty'),
731 )
732
733 TASK_OUTCOME_HELP = (
734 (OUTCOME_SUCCESS, 'This task successfully completed'),
735 (OUTCOME_COVERED, 'This task did not run because its output is provided by another task'),
736 (OUTCOME_CACHED, 'This task restored output from the sstate-cache directory or mirrors'),
737 (OUTCOME_PREBUILT, 'This task did not run because its outcome was reused from a previous build'),
738 (OUTCOME_FAILED, 'This task did not complete'),
739 (OUTCOME_EMPTY, 'This task has no executable content'),
740 (OUTCOME_NA, ''),
741 )
742
743 search_allowed_fields = [ "recipe__name", "recipe__version", "task_name", "logfile" ]
744
745 def __init__(self, *args, **kwargs):
746 super(Task, self).__init__(*args, **kwargs)
747 try:
748 self._helptext = HelpText.objects.get(key=self.task_name, area=HelpText.VARIABLE, build=self.build).text
749 except HelpText.DoesNotExist:
750 self._helptext = None
751
752 def get_related_setscene(self):
753 return Task.objects.filter(task_executed=True, build = self.build, recipe = self.recipe, task_name=self.task_name+"_setscene")
754
755 def get_outcome_text(self):
756 return Task.TASK_OUTCOME[int(self.outcome) + 1][1]
757
758 def get_outcome_help(self):
759 return Task.TASK_OUTCOME_HELP[int(self.outcome)][1]
760
761 def get_sstate_text(self):
762 if self.sstate_result==Task.SSTATE_NA:
763 return ''
764 else:
765 return Task.SSTATE_RESULT[int(self.sstate_result)][1]
766
767 def get_executed_display(self):
768 if self.task_executed:
769 return "Executed"
770 return "Not Executed"
771
772 def get_description(self):
773 return self._helptext
774
775 build = models.ForeignKey(Build, related_name='task_build')
776 order = models.IntegerField(null=True)
777 task_executed = models.BooleanField(default=False) # True means Executed, False means Not/Executed
778 outcome = models.IntegerField(choices=TASK_OUTCOME, default=OUTCOME_NA)
779 sstate_checksum = models.CharField(max_length=100, blank=True)
780 path_to_sstate_obj = models.FilePathField(max_length=500, blank=True)
781 recipe = models.ForeignKey('Recipe', related_name='tasks')
782 task_name = models.CharField(max_length=100)
783 source_url = models.FilePathField(max_length=255, blank=True)
784 work_directory = models.FilePathField(max_length=255, blank=True)
785 script_type = models.IntegerField(choices=TASK_CODING, default=CODING_NA)
786 line_number = models.IntegerField(default=0)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500787
788 # start/end times
789 started = models.DateTimeField(null=True)
790 ended = models.DateTimeField(null=True)
791
792 # in seconds; this is stored to enable sorting
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500793 elapsed_time = models.DecimalField(max_digits=8, decimal_places=2, null=True)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500794
795 # in bytes; note that disk_io is stored to enable sorting
796 disk_io = models.IntegerField(null=True)
797 disk_io_read = models.IntegerField(null=True)
798 disk_io_write = models.IntegerField(null=True)
799
800 # in seconds
801 cpu_time_user = models.DecimalField(max_digits=8, decimal_places=2, null=True)
802 cpu_time_system = models.DecimalField(max_digits=8, decimal_places=2, null=True)
803
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500804 sstate_result = models.IntegerField(choices=SSTATE_RESULT, default=SSTATE_NA)
805 message = models.CharField(max_length=240)
806 logfile = models.FilePathField(max_length=255, blank=True)
807
808 outcome_text = property(get_outcome_text)
809 sstate_text = property(get_sstate_text)
810
811 def __unicode__(self):
812 return "%d(%d) %s:%s" % (self.pk, self.build.pk, self.recipe.name, self.task_name)
813
814 class Meta:
815 ordering = ('order', 'recipe' ,)
816 unique_together = ('build', 'recipe', 'task_name', )
817
818
819class Task_Dependency(models.Model):
820 task = models.ForeignKey(Task, related_name='task_dependencies_task')
821 depends_on = models.ForeignKey(Task, related_name='task_dependencies_depends')
822
823class Package(models.Model):
824 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']
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500825 build = models.ForeignKey('Build', null=True)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500826 recipe = models.ForeignKey('Recipe', null=True)
827 name = models.CharField(max_length=100)
828 installed_name = models.CharField(max_length=100, default='')
829 version = models.CharField(max_length=100, blank=True)
830 revision = models.CharField(max_length=32, blank=True)
831 summary = models.TextField(blank=True)
832 description = models.TextField(blank=True)
833 size = models.IntegerField(default=0)
834 installed_size = models.IntegerField(default=0)
835 section = models.CharField(max_length=80, blank=True)
836 license = models.CharField(max_length=80, blank=True)
837
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500838 @property
839 def is_locale_package(self):
840 """ Returns True if this package is identifiable as a locale package """
841 if self.name.find('locale') != -1:
842 return True
843 return False
844
845 @property
846 def is_packagegroup(self):
847 """ Returns True is this package is identifiable as a packagegroup """
848 if self.name.find('packagegroup') != -1:
849 return True
850 return False
851
852class CustomImagePackage(Package):
853 # CustomImageRecipe fields to track pacakges appended,
854 # included and excluded from a CustomImageRecipe
855 recipe_includes = models.ManyToManyField('CustomImageRecipe',
856 related_name='includes_set')
857 recipe_excludes = models.ManyToManyField('CustomImageRecipe',
858 related_name='excludes_set')
859 recipe_appends = models.ManyToManyField('CustomImageRecipe',
860 related_name='appends_set')
861
862
863
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500864class Package_DependencyManager(models.Manager):
865 use_for_related_fields = True
866
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500867 def get_queryset(self):
868 return super(Package_DependencyManager, self).get_queryset().exclude(package_id = F('depends_on__id'))
869
870 def get_total_source_deps_size(self):
871 """ Returns the total file size of all the packages that depend on
872 thispackage.
873 """
874 return self.all().aggregate(Sum('depends_on__size'))
875
876 def get_total_revdeps_size(self):
877 """ Returns the total file size of all the packages that depend on
878 this package.
879 """
880 return self.all().aggregate(Sum('package_id__size'))
881
882
883 def all_depends(self):
884 """ Returns just the depends packages and not any other dep_type """
885 return self.filter(Q(dep_type=Package_Dependency.TYPE_RDEPENDS) |
886 Q(dep_type=Package_Dependency.TYPE_TRDEPENDS))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500887
888class Package_Dependency(models.Model):
889 TYPE_RDEPENDS = 0
890 TYPE_TRDEPENDS = 1
891 TYPE_RRECOMMENDS = 2
892 TYPE_TRECOMMENDS = 3
893 TYPE_RSUGGESTS = 4
894 TYPE_RPROVIDES = 5
895 TYPE_RREPLACES = 6
896 TYPE_RCONFLICTS = 7
897 ' TODO: bpackage should be changed to remove the DEPENDS_TYPE access '
898 DEPENDS_TYPE = (
899 (TYPE_RDEPENDS, "depends"),
900 (TYPE_TRDEPENDS, "depends"),
901 (TYPE_TRECOMMENDS, "recommends"),
902 (TYPE_RRECOMMENDS, "recommends"),
903 (TYPE_RSUGGESTS, "suggests"),
904 (TYPE_RPROVIDES, "provides"),
905 (TYPE_RREPLACES, "replaces"),
906 (TYPE_RCONFLICTS, "conflicts"),
907 )
908 """ Indexed by dep_type, in view order, key for short name and help
909 description which when viewed will be printf'd with the
910 package name.
911 """
912 DEPENDS_DICT = {
913 TYPE_RDEPENDS : ("depends", "%s is required to run %s"),
914 TYPE_TRDEPENDS : ("depends", "%s is required to run %s"),
915 TYPE_TRECOMMENDS : ("recommends", "%s extends the usability of %s"),
916 TYPE_RRECOMMENDS : ("recommends", "%s extends the usability of %s"),
917 TYPE_RSUGGESTS : ("suggests", "%s is suggested for installation with %s"),
918 TYPE_RPROVIDES : ("provides", "%s is provided by %s"),
919 TYPE_RREPLACES : ("replaces", "%s is replaced by %s"),
920 TYPE_RCONFLICTS : ("conflicts", "%s conflicts with %s, which will not be installed if this package is not first removed"),
921 }
922
923 package = models.ForeignKey(Package, related_name='package_dependencies_source')
924 depends_on = models.ForeignKey(Package, related_name='package_dependencies_target') # soft dependency
925 dep_type = models.IntegerField(choices=DEPENDS_TYPE)
926 target = models.ForeignKey(Target, null=True)
927 objects = Package_DependencyManager()
928
929class Target_Installed_Package(models.Model):
930 target = models.ForeignKey(Target)
931 package = models.ForeignKey(Package, related_name='buildtargetlist_package')
932
933class Package_File(models.Model):
934 package = models.ForeignKey(Package, related_name='buildfilelist_package')
935 path = models.FilePathField(max_length=255, blank=True)
936 size = models.IntegerField()
937
938class Recipe(models.Model):
939 search_allowed_fields = ['name', 'version', 'file_path', 'section', 'summary', 'description', 'license', 'layer_version__layer__name', 'layer_version__branch', 'layer_version__commit', 'layer_version__local_path', 'layer_version__layer_source__name']
940
941 layer_source = models.ForeignKey('LayerSource', default = None, null = True) # from where did we get this recipe
942 up_id = models.IntegerField(null = True, default = None) # id of entry in the source
943 up_date = models.DateTimeField(null = True, default = None)
944
945 name = models.CharField(max_length=100, blank=True) # pn
946 version = models.CharField(max_length=100, blank=True) # pv
947 layer_version = models.ForeignKey('Layer_Version', related_name='recipe_layer_version')
948 summary = models.TextField(blank=True)
949 description = models.TextField(blank=True)
950 section = models.CharField(max_length=100, blank=True)
951 license = models.CharField(max_length=200, blank=True)
952 homepage = models.URLField(blank=True)
953 bugtracker = models.URLField(blank=True)
954 file_path = models.FilePathField(max_length=255)
955 pathflags = models.CharField(max_length=200, blank=True)
956 is_image = models.BooleanField(default=False)
957
958 def get_layersource_view_url(self):
959 if self.layer_source is None:
960 return ""
961
962 url = self.layer_source.get_object_view(self.layer_version.up_branch, "recipes", self.name)
963 return url
964
965 def __unicode__(self):
966 return "Recipe " + self.name + ":" + self.version
967
968 def get_vcs_recipe_file_link_url(self):
969 return self.layer_version.get_vcs_file_link_url(self.file_path)
970
971 def get_description_or_summary(self):
972 if self.description:
973 return self.description
974 elif self.summary:
975 return self.summary
976 else:
977 return ""
978
979 class Meta:
980 unique_together = (("layer_version", "file_path", "pathflags"), )
981
982
983class Recipe_DependencyManager(models.Manager):
984 use_for_related_fields = True
985
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500986 def get_queryset(self):
987 return super(Recipe_DependencyManager, self).get_queryset().exclude(recipe_id = F('depends_on__id'))
988
989class Provides(models.Model):
990 name = models.CharField(max_length=100)
991 recipe = models.ForeignKey(Recipe)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500992
993class Recipe_Dependency(models.Model):
994 TYPE_DEPENDS = 0
995 TYPE_RDEPENDS = 1
996
997 DEPENDS_TYPE = (
998 (TYPE_DEPENDS, "depends"),
999 (TYPE_RDEPENDS, "rdepends"),
1000 )
1001 recipe = models.ForeignKey(Recipe, related_name='r_dependencies_recipe')
1002 depends_on = models.ForeignKey(Recipe, related_name='r_dependencies_depends')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001003 via = models.ForeignKey(Provides, null=True, default=None)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001004 dep_type = models.IntegerField(choices=DEPENDS_TYPE)
1005 objects = Recipe_DependencyManager()
1006
1007
1008class Machine(models.Model):
1009 search_allowed_fields = ["name", "description", "layer_version__layer__name"]
1010 layer_source = models.ForeignKey('LayerSource', default = None, null = True) # from where did we get this machine
1011 up_id = models.IntegerField(null = True, default = None) # id of entry in the source
1012 up_date = models.DateTimeField(null = True, default = None)
1013
1014 layer_version = models.ForeignKey('Layer_Version')
1015 name = models.CharField(max_length=255)
1016 description = models.CharField(max_length=255)
1017
1018 def get_vcs_machine_file_link_url(self):
1019 path = 'conf/machine/'+self.name+'.conf'
1020
1021 return self.layer_version.get_vcs_file_link_url(path)
1022
1023 def __unicode__(self):
1024 return "Machine " + self.name + "(" + self.description + ")"
1025
1026 class Meta:
1027 unique_together = ("layer_source", "up_id")
1028
1029
1030from django.db.models.base import ModelBase
1031
1032class InheritanceMetaclass(ModelBase):
1033 def __call__(cls, *args, **kwargs):
1034 obj = super(InheritanceMetaclass, cls).__call__(*args, **kwargs)
1035 return obj.get_object()
1036
1037
1038class LayerSource(models.Model):
1039 __metaclass__ = InheritanceMetaclass
1040
1041 class Meta:
1042 unique_together = (('sourcetype', 'apiurl'), )
1043
1044 TYPE_LOCAL = 0
1045 TYPE_LAYERINDEX = 1
1046 TYPE_IMPORTED = 2
1047 SOURCE_TYPE = (
1048 (TYPE_LOCAL, "local"),
1049 (TYPE_LAYERINDEX, "layerindex"),
1050 (TYPE_IMPORTED, "imported"),
1051 )
1052
1053 name = models.CharField(max_length=63, unique = True)
1054 sourcetype = models.IntegerField(choices=SOURCE_TYPE)
1055 apiurl = models.CharField(max_length=255, null=True, default=None)
1056
1057 def __init__(self, *args, **kwargs):
1058 super(LayerSource, self).__init__(*args, **kwargs)
1059 if self.sourcetype == LayerSource.TYPE_LOCAL:
1060 self.__class__ = LocalLayerSource
1061 elif self.sourcetype == LayerSource.TYPE_LAYERINDEX:
1062 self.__class__ = LayerIndexLayerSource
1063 elif self.sourcetype == LayerSource.TYPE_IMPORTED:
1064 self.__class__ = ImportedLayerSource
1065 elif self.sourcetype == None:
1066 raise Exception("Unknown LayerSource-derived class. If you added a new layer source type, fill out all code stubs.")
1067
1068
1069 def update(self):
1070 """
1071 Updates the local database information from the upstream layer source
1072 """
1073 raise Exception("Abstract, update() must be implemented by all LayerSource-derived classes (object is %s)" % str(vars(self)))
1074
1075 def save(self, *args, **kwargs):
1076 return super(LayerSource, self).save(*args, **kwargs)
1077
1078 def get_object(self):
1079 # preset an un-initilized object
1080 if None == self.name:
1081 self.name=""
1082 if None == self.apiurl:
1083 self.apiurl=""
1084 if None == self.sourcetype:
1085 self.sourcetype=LayerSource.TYPE_LOCAL
1086
1087 if self.sourcetype == LayerSource.TYPE_LOCAL:
1088 self.__class__ = LocalLayerSource
1089 elif self.sourcetype == LayerSource.TYPE_LAYERINDEX:
1090 self.__class__ = LayerIndexLayerSource
1091 elif self.sourcetype == LayerSource.TYPE_IMPORTED:
1092 self.__class__ = ImportedLayerSource
1093 else:
1094 raise Exception("Unknown LayerSource type. If you added a new layer source type, fill out all code stubs.")
1095 return self
1096
1097 def __unicode__(self):
1098 return "%s (%s)" % (self.name, self.sourcetype)
1099
1100
1101class LocalLayerSource(LayerSource):
1102 class Meta(LayerSource._meta.__class__):
1103 proxy = True
1104
1105 def __init__(self, *args, **kwargs):
1106 super(LocalLayerSource, self).__init__(args, kwargs)
1107 self.sourcetype = LayerSource.TYPE_LOCAL
1108
1109 def update(self):
1110 """
1111 Fetches layer, recipe and machine information from local repository
1112 """
1113 pass
1114
1115class ImportedLayerSource(LayerSource):
1116 class Meta(LayerSource._meta.__class__):
1117 proxy = True
1118
1119 def __init__(self, *args, **kwargs):
1120 super(ImportedLayerSource, self).__init__(args, kwargs)
1121 self.sourcetype = LayerSource.TYPE_IMPORTED
1122
1123 def update(self):
1124 """
1125 Fetches layer, recipe and machine information from local repository
1126 """
1127 pass
1128
1129
1130class LayerIndexLayerSource(LayerSource):
1131 class Meta(LayerSource._meta.__class__):
1132 proxy = True
1133
1134 def __init__(self, *args, **kwargs):
1135 super(LayerIndexLayerSource, self).__init__(args, kwargs)
1136 self.sourcetype = LayerSource.TYPE_LAYERINDEX
1137
1138 def get_object_view(self, branch, objectype, upid):
1139 return self.apiurl + "../branch/" + branch.name + "/" + objectype + "/?q=" + str(upid)
1140
1141 def update(self):
1142 """
1143 Fetches layer, recipe and machine information from remote repository
1144 """
1145 assert self.apiurl is not None
1146 from django.db import transaction, connection
1147
1148 import urllib2, urlparse, json
1149 import os
1150 proxy_settings = os.environ.get("http_proxy", None)
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001151 oe_core_layer = 'openembedded-core'
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001152
1153 def _get_json_response(apiurl = self.apiurl):
1154 _parsedurl = urlparse.urlparse(apiurl)
1155 path = _parsedurl.path
1156
1157 try:
1158 res = urllib2.urlopen(apiurl)
1159 except urllib2.URLError as e:
1160 raise Exception("Failed to read %s: %s" % (path, e.reason))
1161
1162 return json.loads(res.read())
1163
1164 # verify we can get the basic api
1165 try:
1166 apilinks = _get_json_response()
1167 except Exception as e:
1168 import traceback
1169 if proxy_settings is not None:
1170 logger.info("EE: Using proxy %s" % proxy_settings)
1171 logger.warning("EE: could not connect to %s, skipping update: %s\n%s" % (self.apiurl, e, traceback.format_exc(e)))
1172 return
1173
1174 # update branches; only those that we already have names listed in the
1175 # Releases table
1176 whitelist_branch_names = map(lambda x: x.branch_name, Release.objects.all())
1177 if len(whitelist_branch_names) == 0:
1178 raise Exception("Failed to make list of branches to fetch")
1179
1180 logger.debug("Fetching branches")
1181 branches_info = _get_json_response(apilinks['branches']
1182 + "?filter=name:%s" % "OR".join(whitelist_branch_names))
1183 for bi in branches_info:
1184 b, created = Branch.objects.get_or_create(layer_source = self, name = bi['name'])
1185 b.up_id = bi['id']
1186 b.up_date = bi['updated']
1187 b.name = bi['name']
1188 b.short_description = bi['short_description']
1189 b.save()
1190
1191 # update layers
1192 layers_info = _get_json_response(apilinks['layerItems'])
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001193
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001194 for li in layers_info:
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001195 # Special case for the openembedded-core layer
1196 if li['name'] == oe_core_layer:
1197 try:
1198 # If we have an existing openembedded-core for example
1199 # from the toasterconf.json augment the info using the
1200 # layerindex rather than duplicate it
1201 oe_core_l = Layer.objects.get(name=oe_core_layer)
1202 # Take ownership of the layer as now coming from the
1203 # layerindex
1204 oe_core_l.layer_source = self
1205 oe_core_l.up_id = li['id']
1206 oe_core_l.summary = li['summary']
1207 oe_core_l.description = li['description']
1208 oe_core_l.save()
1209 continue
1210
1211 except Layer.DoesNotExist:
1212 pass
1213
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001214 l, created = Layer.objects.get_or_create(layer_source = self, name = li['name'])
1215 l.up_id = li['id']
1216 l.up_date = li['updated']
1217 l.vcs_url = li['vcs_url']
1218 l.vcs_web_url = li['vcs_web_url']
1219 l.vcs_web_tree_base_url = li['vcs_web_tree_base_url']
1220 l.vcs_web_file_base_url = li['vcs_web_file_base_url']
1221 l.summary = li['summary']
1222 l.description = li['description']
1223 l.save()
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001224
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001225 # update layerbranches/layer_versions
1226 logger.debug("Fetching layer information")
1227 layerbranches_info = _get_json_response(apilinks['layerBranches']
1228 + "?filter=branch:%s" % "OR".join(map(lambda x: str(x.up_id), [i for i in Branch.objects.filter(layer_source = self) if i.up_id is not None] ))
1229 )
1230
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001231 for lbi in layerbranches_info:
1232 lv, created = Layer_Version.objects.get_or_create(layer_source = self,
1233 up_id = lbi['id'],
1234 layer=Layer.objects.get(layer_source = self, up_id = lbi['layer'])
1235 )
1236
1237 lv.up_date = lbi['updated']
1238 lv.up_branch = Branch.objects.get(layer_source = self, up_id = lbi['branch'])
1239 lv.branch = lbi['actual_branch']
1240 lv.commit = lbi['actual_branch']
1241 lv.dirpath = lbi['vcs_subdir']
1242 lv.save()
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001243
1244 # update layer dependencies
1245 layerdependencies_info = _get_json_response(apilinks['layerDependencies'])
1246 dependlist = {}
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001247 for ldi in layerdependencies_info:
1248 try:
1249 lv = Layer_Version.objects.get(layer_source = self, up_id = ldi['layerbranch'])
1250 except Layer_Version.DoesNotExist as e:
1251 continue
1252
1253 if lv not in dependlist:
1254 dependlist[lv] = []
1255 try:
1256 dependlist[lv].append(Layer_Version.objects.get(layer_source = self, layer__up_id = ldi['dependency'], up_branch = lv.up_branch))
1257 except Layer_Version.DoesNotExist:
1258 logger.warning("Cannot find layer version (ls:%s), up_id:%s lv:%s" % (self, ldi['dependency'], lv))
1259
1260 for lv in dependlist:
1261 LayerVersionDependency.objects.filter(layer_version = lv).delete()
1262 for lvd in dependlist[lv]:
1263 LayerVersionDependency.objects.get_or_create(layer_version = lv, depends_on = lvd)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001264
1265
1266 # update machines
1267 logger.debug("Fetching machine information")
1268 machines_info = _get_json_response(apilinks['machines']
1269 + "?filter=layerbranch:%s" % "OR".join(map(lambda x: str(x.up_id), Layer_Version.objects.filter(layer_source = self)))
1270 )
1271
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001272 for mi in machines_info:
1273 mo, created = Machine.objects.get_or_create(layer_source = self, up_id = mi['id'], layer_version = Layer_Version.objects.get(layer_source = self, up_id = mi['layerbranch']))
1274 mo.up_date = mi['updated']
1275 mo.name = mi['name']
1276 mo.description = mi['description']
1277 mo.save()
1278
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001279 # update recipes; paginate by layer version / layer branch
1280 logger.debug("Fetching target information")
1281 recipes_info = _get_json_response(apilinks['recipes']
1282 + "?filter=layerbranch:%s" % "OR".join(map(lambda x: str(x.up_id), Layer_Version.objects.filter(layer_source = self)))
1283 )
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001284 for ri in recipes_info:
1285 try:
1286 ro, created = Recipe.objects.get_or_create(layer_source = self, up_id = ri['id'], layer_version = Layer_Version.objects.get(layer_source = self, up_id = ri['layerbranch']))
1287 ro.up_date = ri['updated']
1288 ro.name = ri['pn']
1289 ro.version = ri['pv']
1290 ro.summary = ri['summary']
1291 ro.description = ri['description']
1292 ro.section = ri['section']
1293 ro.license = ri['license']
1294 ro.homepage = ri['homepage']
1295 ro.bugtracker = ri['bugtracker']
1296 ro.file_path = ri['filepath'] + "/" + ri['filename']
1297 if 'inherits' in ri:
1298 ro.is_image = 'image' in ri['inherits'].split()
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001299 else: # workaround for old style layer index
1300 ro.is_image = "-image-" in ri['pn']
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001301 ro.save()
1302 except IntegrityError as e:
1303 logger.debug("Failed saving recipe, ignoring: %s (%s:%s)" % (e, ro.layer_version, ri['filepath']+"/"+ri['filename']))
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001304 ro.delete()
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001305
1306class BitbakeVersion(models.Model):
1307
1308 name = models.CharField(max_length=32, unique = True)
1309 giturl = GitURLField()
1310 branch = models.CharField(max_length=32)
1311 dirpath = models.CharField(max_length=255)
1312
1313 def __unicode__(self):
1314 return "%s (Branch: %s)" % (self.name, self.branch)
1315
1316
1317class Release(models.Model):
1318 """ A release is a project template, used to pre-populate Project settings with a configuration set """
1319 name = models.CharField(max_length=32, unique = True)
1320 description = models.CharField(max_length=255)
1321 bitbake_version = models.ForeignKey(BitbakeVersion)
1322 branch_name = models.CharField(max_length=50, default = "")
1323 helptext = models.TextField(null=True)
1324
1325 def __unicode__(self):
1326 return "%s (%s)" % (self.name, self.branch_name)
1327
1328class ReleaseLayerSourcePriority(models.Model):
1329 """ Each release selects layers from the set up layer sources, ordered by priority """
1330 release = models.ForeignKey("Release")
1331 layer_source = models.ForeignKey("LayerSource")
1332 priority = models.IntegerField(default = 0)
1333
1334 def __unicode__(self):
1335 return "%s-%s:%d" % (self.release.name, self.layer_source.name, self.priority)
1336 class Meta:
1337 unique_together = (('release', 'layer_source'),)
1338
1339
1340class ReleaseDefaultLayer(models.Model):
1341 release = models.ForeignKey(Release)
1342 layer_name = models.CharField(max_length=100, default="")
1343
1344
1345# Branch class is synced with layerindex.Branch, branches can only come from remote layer indexes
1346class Branch(models.Model):
1347 layer_source = models.ForeignKey('LayerSource', null = True, default = True)
1348 up_id = models.IntegerField(null = True, default = None) # id of branch in the source
1349 up_date = models.DateTimeField(null = True, default = None)
1350
1351 name = models.CharField(max_length=50)
1352 short_description = models.CharField(max_length=50, blank=True)
1353
1354 class Meta:
1355 verbose_name_plural = "Branches"
1356 unique_together = (('layer_source', 'name'),('layer_source', 'up_id'))
1357
1358 def __unicode__(self):
1359 return self.name
1360
1361
1362# Layer class synced with layerindex.LayerItem
1363class Layer(models.Model):
1364 layer_source = models.ForeignKey(LayerSource, null = True, default = None) # from where did we got this layer
1365 up_id = models.IntegerField(null = True, default = None) # id of layer in the remote source
1366 up_date = models.DateTimeField(null = True, default = None)
1367
1368 name = models.CharField(max_length=100)
1369 layer_index_url = models.URLField()
1370 vcs_url = GitURLField(default = None, null = True)
1371 vcs_web_url = models.URLField(null = True, default = None)
1372 vcs_web_tree_base_url = models.URLField(null = True, default = None)
1373 vcs_web_file_base_url = models.URLField(null = True, default = None)
1374
1375 summary = models.TextField(help_text='One-line description of the layer', null = True, default = None)
1376 description = models.TextField(null = True, default = None)
1377
1378 def __unicode__(self):
1379 return "%s / %s " % (self.name, self.layer_source)
1380
1381 class Meta:
1382 unique_together = (("layer_source", "up_id"), ("layer_source", "name"))
1383
1384
1385# LayerCommit class is synced with layerindex.LayerBranch
1386class Layer_Version(models.Model):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001387 """
1388 A Layer_Version either belongs to a single project or no project
1389 """
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001390 search_allowed_fields = ["layer__name", "layer__summary", "layer__description", "layer__vcs_url", "dirpath", "up_branch__name", "commit", "branch"]
1391 build = models.ForeignKey(Build, related_name='layer_version_build', default = None, null = True)
1392 layer = models.ForeignKey(Layer, related_name='layer_version_layer')
1393
1394 layer_source = models.ForeignKey(LayerSource, null = True, default = None) # from where did we get this Layer Version
1395 up_id = models.IntegerField(null = True, default = None) # id of layerbranch in the remote source
1396 up_date = models.DateTimeField(null = True, default = None)
1397 up_branch = models.ForeignKey(Branch, null = True, default = None)
1398
1399 branch = models.CharField(max_length=80) # LayerBranch.actual_branch
1400 commit = models.CharField(max_length=100) # LayerBranch.vcs_last_rev
1401 dirpath = models.CharField(max_length=255, null = True, default = None) # LayerBranch.vcs_subdir
1402 priority = models.IntegerField(default = 0) # if -1, this is a default layer
1403
1404 local_path = models.FilePathField(max_length=1024, default = "/") # where this layer was checked-out
1405
1406 project = models.ForeignKey('Project', null = True, default = None) # Set if this layer is project-specific; always set for imported layers, and project-set branches
1407
1408 # code lifted, with adaptations, from the layerindex-web application https://git.yoctoproject.org/cgit/cgit.cgi/layerindex-web/
1409 def _handle_url_path(self, base_url, path):
1410 import re, posixpath
1411 if base_url:
1412 if self.dirpath:
1413 if path:
1414 extra_path = self.dirpath + '/' + path
1415 # Normalise out ../ in path for usage URL
1416 extra_path = posixpath.normpath(extra_path)
1417 # Minor workaround to handle case where subdirectory has been added between branches
1418 # (should probably support usage URL per branch to handle this... sigh...)
1419 if extra_path.startswith('../'):
1420 extra_path = extra_path[3:]
1421 else:
1422 extra_path = self.dirpath
1423 else:
1424 extra_path = path
1425 branchname = self.up_branch.name
1426 url = base_url.replace('%branch%', branchname)
1427
1428 # If there's a % in the path (e.g. a wildcard bbappend) we need to encode it
1429 if extra_path:
1430 extra_path = extra_path.replace('%', '%25')
1431
1432 if '%path%' in base_url:
1433 if extra_path:
1434 url = re.sub(r'\[([^\]]*%path%[^\]]*)\]', '\\1', url)
1435 else:
1436 url = re.sub(r'\[([^\]]*%path%[^\]]*)\]', '', url)
1437 return url.replace('%path%', extra_path)
1438 else:
1439 return url + extra_path
1440 return None
1441
1442 def get_vcs_link_url(self):
1443 if self.layer.vcs_web_url is None:
1444 return None
1445 return self.layer.vcs_web_url
1446
1447 def get_vcs_file_link_url(self, file_path=""):
1448 if self.layer.vcs_web_file_base_url is None:
1449 return None
1450 return self._handle_url_path(self.layer.vcs_web_file_base_url, file_path)
1451
1452 def get_vcs_dirpath_link_url(self):
1453 if self.layer.vcs_web_tree_base_url is None:
1454 return None
1455 return self._handle_url_path(self.layer.vcs_web_tree_base_url, '')
1456
1457 def get_equivalents_wpriority(self, project):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001458 layer_versions = project.get_all_compatible_layer_versions()
1459 filtered = layer_versions.filter(layer__name = self.layer.name)
1460 return filtered.order_by("-layer_source__releaselayersourcepriority__priority")
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001461
1462 def get_vcs_reference(self):
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001463 if self.branch is not None and len(self.branch) > 0:
1464 return self.branch
1465 if self.up_branch is not None:
1466 return self.up_branch.name
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001467 if self.commit is not None and len(self.commit) > 0:
1468 return self.commit
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001469 return 'N/A'
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001470
1471 def get_detailspage_url(self, project_id):
1472 return reverse('layerdetails', args=(project_id, self.pk))
1473
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001474 def get_alldeps(self, project_id):
1475 """Get full list of unique layer dependencies."""
1476 def gen_layerdeps(lver, project):
1477 for ldep in lver.dependencies.all():
1478 yield ldep.depends_on
1479 # get next level of deps recursively calling gen_layerdeps
1480 for subdep in gen_layerdeps(ldep.depends_on, project):
1481 yield subdep
1482
1483 project = Project.objects.get(pk=project_id)
1484 result = []
1485 projectlvers = [player.layercommit for player in project.projectlayer_set.all()]
1486 for dep in gen_layerdeps(self, project):
1487 # filter out duplicates and layers already belonging to the project
1488 if dep not in result + projectlvers:
1489 result.append(dep)
1490
1491 return sorted(result, key=lambda x: x.layer.name)
1492
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001493 def __unicode__(self):
1494 return "%d %s (VCS %s, Project %s)" % (self.pk, str(self.layer), self.get_vcs_reference(), self.build.project if self.build is not None else "No project")
1495
1496 class Meta:
1497 unique_together = ("layer_source", "up_id")
1498
1499class LayerVersionDependency(models.Model):
1500 layer_source = models.ForeignKey(LayerSource, null = True, default = None) # from where did we got this layer
1501 up_id = models.IntegerField(null = True, default = None) # id of layerbranch in the remote source
1502
1503 layer_version = models.ForeignKey(Layer_Version, related_name="dependencies")
1504 depends_on = models.ForeignKey(Layer_Version, related_name="dependees")
1505
1506 class Meta:
1507 unique_together = ("layer_source", "up_id")
1508
1509class ProjectLayer(models.Model):
1510 project = models.ForeignKey(Project)
1511 layercommit = models.ForeignKey(Layer_Version, null=True)
1512 optional = models.BooleanField(default = True)
1513
1514 def __unicode__(self):
1515 return "%s, %s" % (self.project.name, self.layercommit)
1516
1517 class Meta:
1518 unique_together = (("project", "layercommit"),)
1519
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001520class CustomImageRecipe(Recipe):
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001521
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001522 # CustomImageRecipe's belong to layers called:
1523 LAYER_NAME = "toaster-custom-images"
1524
1525 search_allowed_fields = ['name']
1526 base_recipe = models.ForeignKey(Recipe, related_name='based_on_recipe')
1527 project = models.ForeignKey(Project)
1528 last_updated = models.DateTimeField(null=True, default=None)
1529
1530 def get_last_successful_built_target(self):
1531 """ Return the last successful built target object if one exists
1532 otherwise return None """
1533 return Target.objects.filter(Q(build__outcome=Build.SUCCEEDED) &
1534 Q(build__project=self.project) &
1535 Q(target=self.name)).last()
1536
1537 def update_package_list(self):
1538 """ Update the package list from the last good build of this
1539 CustomImageRecipe
1540 """
1541 # Check if we're aldready up-to-date or not
1542 target = self.get_last_successful_built_target()
1543 if target == None:
1544 # So we've never actually built this Custom recipe but what about
1545 # the recipe it's based on?
1546 target = \
1547 Target.objects.filter(Q(build__outcome=Build.SUCCEEDED) &
1548 Q(build__project=self.project) &
1549 Q(target=self.base_recipe.name)).last()
1550 if target == None:
1551 return
1552
1553 if target.build.completed_on == self.last_updated:
1554 return
1555
1556 self.includes_set.clear()
1557
1558 excludes_list = self.excludes_set.values_list('name', flat=True)
1559 appends_list = self.appends_set.values_list('name', flat=True)
1560
1561 built_packages_list = \
1562 target.target_installed_package_set.values_list('package__name',
1563 flat=True)
1564 for built_package in built_packages_list:
1565 # Is the built package in the custom packages list?
1566 if built_package in excludes_list:
1567 continue
1568
1569 if built_package in appends_list:
1570 continue
1571
1572 cust_img_p = \
1573 CustomImagePackage.objects.get(name=built_package)
1574 self.includes_set.add(cust_img_p)
1575
1576
1577 self.last_updated = target.build.completed_on
1578 self.save()
1579
1580 def get_all_packages(self):
1581 """Get the included packages and any appended packages"""
1582 self.update_package_list()
1583
1584 return CustomImagePackage.objects.filter((Q(recipe_appends=self) |
1585 Q(recipe_includes=self)) &
1586 ~Q(recipe_excludes=self))
1587
1588
1589 def generate_recipe_file_contents(self):
1590 """Generate the contents for the recipe file."""
1591 # If we have no excluded packages we only need to _append
1592 if self.excludes_set.count() == 0:
1593 packages_conf = "IMAGE_INSTALL_append = \" "
1594
1595 for pkg in self.appends_set.all():
1596 packages_conf += pkg.name+' '
1597 else:
1598 packages_conf = "IMAGE_FEATURES =\"\"\nIMAGE_INSTALL = \""
1599 # We add all the known packages to be built by this recipe apart
1600 # from locale packages which are are controlled with IMAGE_LINGUAS.
1601 for pkg in self.get_all_packages().exclude(
1602 name__icontains="locale"):
1603 packages_conf += pkg.name+' '
1604
1605 packages_conf += "\""
1606 try:
1607 base_recipe = open("%s/%s" %
1608 (self.base_recipe.layer_version.dirpath,
1609 self.base_recipe.file_path), 'r').read()
1610 except IOError:
1611 # The path may now be the full path if the recipe has been built
1612 base_recipe = open(self.base_recipe.file_path, 'r').read()
1613
1614 # Add a special case for when the recipe we have based a custom image
1615 # recipe on requires another recipe.
1616 # For example:
1617 # "require core-image-minimal.bb" is changed to:
1618 # "require recipes-core/images/core-image-minimal.bb"
1619
1620 req_search = re.search(r'(require\s+)(.+\.bb\s*$)',
1621 base_recipe,
1622 re.MULTILINE)
1623 if req_search:
1624 require_filename = req_search.group(2).strip()
1625
1626 corrected_location = Recipe.objects.filter(
1627 Q(layer_version=self.base_recipe.layer_version) &
1628 Q(file_path__icontains=require_filename)).last().file_path
1629
1630 new_require_line = "require %s" % corrected_location
1631
1632 base_recipe = \
1633 base_recipe.replace(req_search.group(0), new_require_line)
1634
1635
1636 info = {"date" : timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
1637 "base_recipe" : base_recipe,
1638 "recipe_name" : self.name,
1639 "base_recipe_name" : self.base_recipe.name,
1640 "license" : self.license,
1641 "summary" : self.summary,
1642 "description" : self.description,
1643 "packages_conf" : packages_conf.strip(),
1644 }
1645
1646 recipe_contents = ("# Original recipe %(base_recipe_name)s \n"
1647 "%(base_recipe)s\n\n"
1648 "# Recipe %(recipe_name)s \n"
1649 "# Customisation Generated by Toaster on %(date)s\n"
1650 "SUMMARY = \"%(summary)s\"\n"
1651 "DESCRIPTION = \"%(description)s\"\n"
1652 "LICENSE = \"%(license)s\"\n"
1653 "%(packages_conf)s") % info
1654
1655 return recipe_contents
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001656
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001657class ProjectVariable(models.Model):
1658 project = models.ForeignKey(Project)
1659 name = models.CharField(max_length=100)
1660 value = models.TextField(blank = True)
1661
1662class Variable(models.Model):
1663 search_allowed_fields = ['variable_name', 'variable_value',
1664 'vhistory__file_name', "description"]
1665 build = models.ForeignKey(Build, related_name='variable_build')
1666 variable_name = models.CharField(max_length=100)
1667 variable_value = models.TextField(blank=True)
1668 changed = models.BooleanField(default=False)
1669 human_readable_name = models.CharField(max_length=200)
1670 description = models.TextField(blank=True)
1671
1672class VariableHistory(models.Model):
1673 variable = models.ForeignKey(Variable, related_name='vhistory')
1674 value = models.TextField(blank=True)
1675 file_name = models.FilePathField(max_length=255)
1676 line_number = models.IntegerField(null=True)
1677 operation = models.CharField(max_length=64)
1678
1679class HelpText(models.Model):
1680 VARIABLE = 0
1681 HELPTEXT_AREA = ((VARIABLE, 'variable'), )
1682
1683 build = models.ForeignKey(Build, related_name='helptext_build')
1684 area = models.IntegerField(choices=HELPTEXT_AREA)
1685 key = models.CharField(max_length=100)
1686 text = models.TextField()
1687
1688class LogMessage(models.Model):
1689 EXCEPTION = -1 # used to signal self-toaster-exceptions
1690 INFO = 0
1691 WARNING = 1
1692 ERROR = 2
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001693 CRITICAL = 3
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001694
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001695 LOG_LEVEL = (
1696 (INFO, "info"),
1697 (WARNING, "warn"),
1698 (ERROR, "error"),
1699 (CRITICAL, "critical"),
1700 (EXCEPTION, "toaster exception")
1701 )
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001702
1703 build = models.ForeignKey(Build)
1704 task = models.ForeignKey(Task, blank = True, null=True)
1705 level = models.IntegerField(choices=LOG_LEVEL, default=INFO)
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001706 message = models.TextField(blank=True, null=True)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001707 pathname = models.FilePathField(max_length=255, blank=True)
1708 lineno = models.IntegerField(null=True)
1709
1710 def __str__(self):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001711 return force_bytes('%s %s %s' % (self.get_level_display(), self.message, self.build))
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001712
1713def invalidate_cache(**kwargs):
1714 from django.core.cache import cache
1715 try:
1716 cache.clear()
1717 except Exception as e:
1718 logger.warning("Problem with cache backend: Failed to clear cache: %s" % e)
1719
1720django.db.models.signals.post_save.connect(invalidate_cache)
1721django.db.models.signals.post_delete.connect(invalidate_cache)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001722django.db.models.signals.m2m_changed.connect(invalidate_cache)