blob: 3832905838f8d035940630c40231454c418e4c8f [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
22from django.db import models, IntegrityError
23from django.db.models import F, Q, Avg, Max
24from django.utils import timezone
25
26from django.core.urlresolvers import reverse
27
28from django.core import validators
29from django.conf import settings
30import django.db.models.signals
31
32
33import logging
34logger = logging.getLogger("toaster")
35
36
37class GitURLValidator(validators.URLValidator):
38 import re
39 regex = re.compile(
40 r'^(?:ssh|git|http|ftp)s?://' # http:// or https://
41 r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain...
42 r'localhost|' # localhost...
43 r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4
44 r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' # ...or ipv6
45 r'(?::\d+)?' # optional port
46 r'(?:/?|[/?]\S+)$', re.IGNORECASE)
47
48def GitURLField(**kwargs):
49 r = models.URLField(**kwargs)
50 for i in xrange(len(r.validators)):
51 if isinstance(r.validators[i], validators.URLValidator):
52 r.validators[i] = GitURLValidator()
53 return r
54
55
56class ToasterSetting(models.Model):
57 name = models.CharField(max_length=63)
58 helptext = models.TextField()
59 value = models.CharField(max_length=255)
60
61 def __unicode__(self):
62 return "Setting %s = %s" % (self.name, self.value)
63
64class ProjectManager(models.Manager):
65 def create_project(self, name, release):
66 if release is not None:
67 prj = self.model(name = name, bitbake_version = release.bitbake_version, release = release)
68 else:
69 prj = self.model(name = name, bitbake_version = None, release = None)
70
71 prj.save()
72
73 for defaultconf in ToasterSetting.objects.filter(name__startswith="DEFCONF_"):
74 name = defaultconf.name[8:]
75 ProjectVariable.objects.create( project = prj,
76 name = name,
77 value = defaultconf.value)
78
79 if release is None:
80 return prj
81
82 for rdl in release.releasedefaultlayer_set.all():
83 try:
84 lv = Layer_Version.objects.filter(layer__name = rdl.layer_name, up_branch__name = release.branch_name)[0].get_equivalents_wpriority(prj)[0]
85 ProjectLayer.objects.create( project = prj,
86 layercommit = lv,
87 optional = False )
88 except IndexError:
89 # we may have no valid layer version objects, and that's ok
90 pass
91
92 return prj
93
94 def create(self, *args, **kwargs):
95 raise Exception("Invalid call to Project.objects.create. Use Project.objects.create_project() to create a project")
96
97 # return single object with is_default = True
98 def get_default_project(self):
99 projects = super(ProjectManager, self).filter(is_default = True)
100 if len(projects) > 1:
101 raise Exception("Inconsistent project data: multiple " +
102 "default projects (i.e. with is_default=True)")
103 elif len(projects) < 1:
104 raise Exception("Inconsistent project data: no default project found")
105 return projects[0]
106
107class Project(models.Model):
108 search_allowed_fields = ['name', 'short_description', 'release__name', 'release__branch_name']
109 name = models.CharField(max_length=100)
110 short_description = models.CharField(max_length=50, blank=True)
111 bitbake_version = models.ForeignKey('BitbakeVersion', null=True)
112 release = models.ForeignKey("Release", null=True)
113 created = models.DateTimeField(auto_now_add = True)
114 updated = models.DateTimeField(auto_now = True)
115 # This is a horrible hack; since Toaster has no "User" model available when
116 # running in interactive mode, we can't reference the field here directly
117 # Instead, we keep a possible null reference to the User id, as not to force
118 # hard links to possibly missing models
119 user_id = models.IntegerField(null = True)
120 objects = ProjectManager()
121
122 # set to True for the project which is the default container
123 # for builds initiated by the command line etc.
124 is_default = models.BooleanField(default = False)
125
126 def __unicode__(self):
127 return "%s (Release %s, BBV %s)" % (self.name, self.release, self.bitbake_version)
128
129 def get_current_machine_name(self):
130 try:
131 return self.projectvariable_set.get(name="MACHINE").value
132 except (ProjectVariable.DoesNotExist,IndexError):
133 return( "None" );
134
135 def get_number_of_builds(self):
136 try:
137 return len(Build.objects.filter( project = self.id ))
138 except (Build.DoesNotExist,IndexError):
139 return( 0 )
140
141 def get_last_build_id(self):
142 try:
143 return Build.objects.filter( project = self.id ).order_by('-completed_on')[0].id
144 except (Build.DoesNotExist,IndexError):
145 return( -1 )
146
147 def get_last_outcome(self):
148 build_id = self.get_last_build_id
149 if (-1 == build_id):
150 return( "" )
151 try:
152 return Build.objects.filter( id = self.get_last_build_id )[ 0 ].outcome
153 except (Build.DoesNotExist,IndexError):
154 return( "not_found" )
155
156 def get_last_target(self):
157 build_id = self.get_last_build_id
158 if (-1 == build_id):
159 return( "" )
160 try:
161 return Target.objects.filter(build = build_id)[0].target
162 except (Target.DoesNotExist,IndexError):
163 return( "not_found" )
164
165 def get_last_errors(self):
166 build_id = self.get_last_build_id
167 if (-1 == build_id):
168 return( 0 )
169 try:
170 return Build.objects.filter(id = build_id)[ 0 ].errors.count()
171 except (Build.DoesNotExist,IndexError):
172 return( "not_found" )
173
174 def get_last_warnings(self):
175 build_id = self.get_last_build_id
176 if (-1 == build_id):
177 return( 0 )
178 try:
179 return Build.objects.filter(id = build_id)[ 0 ].warnings.count()
180 except (Build.DoesNotExist,IndexError):
181 return( "not_found" )
182
183 def get_last_imgfiles(self):
184 build_id = self.get_last_build_id
185 if (-1 == build_id):
186 return( "" )
187 try:
188 return Variable.objects.filter(build = build_id, variable_name = "IMAGE_FSTYPES")[ 0 ].variable_value
189 except (Variable.DoesNotExist,IndexError):
190 return( "not_found" )
191
192 # returns a queryset of compatible layers for a project
193 def compatible_layerversions(self, release = None, layer_name = None):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500194 logger.warning("This function is deprecated")
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500195 if release == None:
196 release = self.release
197 # layers on the same branch or layers specifically set for this project
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500198 queryset = Layer_Version.objects.filter(((Q(up_branch__name = release.branch_name) & Q(project = None)) | Q(project = self)) & Q(build__isnull=True))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500199
200 if layer_name is not None:
201 # we select only a layer name
202 queryset = queryset.filter(layer__name = layer_name)
203
204 # order by layer version priority
205 queryset = queryset.filter(Q(layer_source=None) | Q(layer_source__releaselayersourcepriority__release = release)).select_related('layer_source', 'layer', 'up_branch', "layer_source__releaselayersourcepriority__priority").order_by("-layer_source__releaselayersourcepriority__priority")
206
207 return queryset
208
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500209 def get_all_compatible_layer_versions(self):
210 """ Returns Queryset of all Layer_Versions which are compatible with
211 this project"""
212 queryset = Layer_Version.objects.filter(
213 (Q(up_branch__name=self.release.branch_name) & Q(build=None))
214 | Q(project=self))
215
216 return queryset
217
218 def get_project_layer_versions(self, pk=False):
219 """ Returns the Layer_Versions currently added to this project """
220 layer_versions = self.projectlayer_set.all().values('layercommit')
221
222 if pk is False:
223 return layer_versions
224 else:
225 return layer_versions.values_list('layercommit__pk', flat=True)
226
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500227
228 def get_available_machines(self):
229 """ Returns QuerySet of all Machines which are provided by the
230 Layers currently added to the Project """
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500231 queryset = Machine.objects.filter(
232 layer_version__in=self.get_project_layer_versions())
233
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500234 return queryset
235
236 def get_all_compatible_machines(self):
237 """ Returns QuerySet of all the compatible machines available to the
238 project including ones from Layers not currently added """
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500239 queryset = Machine.objects.filter(
240 layer_version__in=self.get_all_compatible_layer_versions())
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500241
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500242 return queryset
243
244 def get_available_recipes(self):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500245 """ Returns QuerySet of all the recipes that are provided by layers
246 added to this project """
247 queryset = Recipe.objects.filter(
248 layer_version__in=self.get_project_layer_versions())
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500249
250 return queryset
251
252 def get_all_compatible_recipes(self):
253 """ Returns QuerySet of all the compatible Recipes available to the
254 project including ones from Layers not currently added """
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500255 queryset = Recipe.objects.filter(
256 layer_version__in=self.get_all_compatible_layer_versions()).exclude(name__exact='')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500257
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500258 return queryset
259
260
261 def schedule_build(self):
262 from bldcontrol.models import BuildRequest, BRTarget, BRLayer, BRVariable, BRBitbake
263 br = BuildRequest.objects.create(project = self)
264 try:
265
266 BRBitbake.objects.create(req = br,
267 giturl = self.bitbake_version.giturl,
268 commit = self.bitbake_version.branch,
269 dirpath = self.bitbake_version.dirpath)
270
271 for l in self.projectlayer_set.all().order_by("pk"):
272 commit = l.layercommit.get_vcs_reference()
273 print("ii Building layer ", l.layercommit.layer.name, " at vcs point ", commit)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500274 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 -0500275
276 br.state = BuildRequest.REQ_QUEUED
277 now = timezone.now()
278 br.build = Build.objects.create(project = self,
279 completed_on=now,
280 started_on=now,
281 )
282 for t in self.projecttarget_set.all():
283 BRTarget.objects.create(req = br, target = t.target, task = t.task)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500284 Target.objects.create(build = br.build, target = t.target, task = t.task)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500285
286 for v in self.projectvariable_set.all():
287 BRVariable.objects.create(req = br, name = v.name, value = v.value)
288
289
290 try:
291 br.build.machine = self.projectvariable_set.get(name = 'MACHINE').value
292 br.build.save()
293 except ProjectVariable.DoesNotExist:
294 pass
295 br.save()
296 except Exception:
297 # revert the build request creation since we're not done cleanly
298 br.delete()
299 raise
300 return br
301
302class Build(models.Model):
303 SUCCEEDED = 0
304 FAILED = 1
305 IN_PROGRESS = 2
306
307 BUILD_OUTCOME = (
308 (SUCCEEDED, 'Succeeded'),
309 (FAILED, 'Failed'),
310 (IN_PROGRESS, 'In Progress'),
311 )
312
313 search_allowed_fields = ['machine', 'cooker_log_path', "target__target", "target__target_image_file__file_name"]
314
315 project = models.ForeignKey(Project) # must have a project
316 machine = models.CharField(max_length=100)
317 distro = models.CharField(max_length=100)
318 distro_version = models.CharField(max_length=100)
319 started_on = models.DateTimeField()
320 completed_on = models.DateTimeField()
321 outcome = models.IntegerField(choices=BUILD_OUTCOME, default=IN_PROGRESS)
322 cooker_log_path = models.CharField(max_length=500)
323 build_name = models.CharField(max_length=100)
324 bitbake_version = models.CharField(max_length=50)
325
326 def completeper(self):
327 tf = Task.objects.filter(build = self)
328 tfc = tf.count()
329 if tfc > 0:
330 completeper = tf.exclude(order__isnull=True).count()*100/tf.count()
331 else:
332 completeper = 0
333 return completeper
334
335 def eta(self):
336 eta = timezone.now()
337 completeper = self.completeper()
338 if self.completeper() > 0:
339 eta += ((eta - self.started_on)*(100-completeper))/completeper
340 return eta
341
342
343 def get_sorted_target_list(self):
344 tgts = Target.objects.filter(build_id = self.id).order_by( 'target' );
345 return( tgts );
346
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500347 def get_outcome_text(self):
348 return Build.BUILD_OUTCOME[int(self.outcome)][1]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500349
350 @property
351 def errors(self):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500352 return (self.logmessage_set.filter(level=LogMessage.ERROR) |
353 self.logmessage_set.filter(level=LogMessage.EXCEPTION) |
354 self.logmessage_set.filter(level=LogMessage.CRITICAL))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500355
356 @property
357 def warnings(self):
358 return self.logmessage_set.filter(level=LogMessage.WARNING)
359
360 @property
361 def timespent_seconds(self):
362 return (self.completed_on - self.started_on).total_seconds()
363
364 def get_current_status(self):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500365 """
366 get the status string from the build request if the build
367 has one, or the text for the build outcome if it doesn't
368 """
369
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500370 from bldcontrol.models import BuildRequest
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500371
372 build_request = None
373 if hasattr(self, 'buildrequest'):
374 build_request = self.buildrequest
375
376 if (build_request
377 and build_request.state != BuildRequest.REQ_INPROGRESS
378 and self.outcome == Build.IN_PROGRESS):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500379 return self.buildrequest.get_state_display()
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500380 else:
381 return self.get_outcome_text()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500382
383 def __str__(self):
384 return "%d %s %s" % (self.id, self.project, ",".join([t.target for t in self.target_set.all()]))
385
386
387# an Artifact is anything that results from a Build, and may be of interest to the user, and is not stored elsewhere
388class BuildArtifact(models.Model):
389 build = models.ForeignKey(Build)
390 file_name = models.FilePathField()
391 file_size = models.IntegerField()
392
393 def get_local_file_name(self):
394 try:
395 deploydir = Variable.objects.get(build = self.build, variable_name="DEPLOY_DIR").variable_value
396 return self.file_name[len(deploydir)+1:]
397 except:
398 raise
399
400 return self.file_name
401
402
403 def is_available(self):
404 return self.build.buildrequest.environment.has_artifact(self.file_name)
405
406class ProjectTarget(models.Model):
407 project = models.ForeignKey(Project)
408 target = models.CharField(max_length=100)
409 task = models.CharField(max_length=100, null=True)
410
411class Target(models.Model):
412 search_allowed_fields = ['target', 'file_name']
413 build = models.ForeignKey(Build)
414 target = models.CharField(max_length=100)
415 task = models.CharField(max_length=100, null=True)
416 is_image = models.BooleanField(default = False)
417 image_size = models.IntegerField(default=0)
418 license_manifest_path = models.CharField(max_length=500, null=True)
419
420 def package_count(self):
421 return Target_Installed_Package.objects.filter(target_id__exact=self.id).count()
422
423 def __unicode__(self):
424 return self.target
425
426class Target_Image_File(models.Model):
427 target = models.ForeignKey(Target)
428 file_name = models.FilePathField(max_length=254)
429 file_size = models.IntegerField()
430
431class Target_File(models.Model):
432 ITYPE_REGULAR = 1
433 ITYPE_DIRECTORY = 2
434 ITYPE_SYMLINK = 3
435 ITYPE_SOCKET = 4
436 ITYPE_FIFO = 5
437 ITYPE_CHARACTER = 6
438 ITYPE_BLOCK = 7
439 ITYPES = ( (ITYPE_REGULAR ,'regular'),
440 ( ITYPE_DIRECTORY ,'directory'),
441 ( ITYPE_SYMLINK ,'symlink'),
442 ( ITYPE_SOCKET ,'socket'),
443 ( ITYPE_FIFO ,'fifo'),
444 ( ITYPE_CHARACTER ,'character'),
445 ( ITYPE_BLOCK ,'block'),
446 )
447
448 target = models.ForeignKey(Target)
449 path = models.FilePathField()
450 size = models.IntegerField()
451 inodetype = models.IntegerField(choices = ITYPES)
452 permission = models.CharField(max_length=16)
453 owner = models.CharField(max_length=128)
454 group = models.CharField(max_length=128)
455 directory = models.ForeignKey('Target_File', related_name="directory_set", null=True)
456 sym_target = models.ForeignKey('Target_File', related_name="symlink_set", null=True)
457
458
459class Task(models.Model):
460
461 SSTATE_NA = 0
462 SSTATE_MISS = 1
463 SSTATE_FAILED = 2
464 SSTATE_RESTORED = 3
465
466 SSTATE_RESULT = (
467 (SSTATE_NA, 'Not Applicable'), # For rest of tasks, but they still need checking.
468 (SSTATE_MISS, 'File not in cache'), # the sstate object was not found
469 (SSTATE_FAILED, 'Failed'), # there was a pkg, but the script failed
470 (SSTATE_RESTORED, 'Succeeded'), # successfully restored
471 )
472
473 CODING_NA = 0
474 CODING_PYTHON = 2
475 CODING_SHELL = 3
476
477 TASK_CODING = (
478 (CODING_NA, 'N/A'),
479 (CODING_PYTHON, 'Python'),
480 (CODING_SHELL, 'Shell'),
481 )
482
483 OUTCOME_NA = -1
484 OUTCOME_SUCCESS = 0
485 OUTCOME_COVERED = 1
486 OUTCOME_CACHED = 2
487 OUTCOME_PREBUILT = 3
488 OUTCOME_FAILED = 4
489 OUTCOME_EMPTY = 5
490
491 TASK_OUTCOME = (
492 (OUTCOME_NA, 'Not Available'),
493 (OUTCOME_SUCCESS, 'Succeeded'),
494 (OUTCOME_COVERED, 'Covered'),
495 (OUTCOME_CACHED, 'Cached'),
496 (OUTCOME_PREBUILT, 'Prebuilt'),
497 (OUTCOME_FAILED, 'Failed'),
498 (OUTCOME_EMPTY, 'Empty'),
499 )
500
501 TASK_OUTCOME_HELP = (
502 (OUTCOME_SUCCESS, 'This task successfully completed'),
503 (OUTCOME_COVERED, 'This task did not run because its output is provided by another task'),
504 (OUTCOME_CACHED, 'This task restored output from the sstate-cache directory or mirrors'),
505 (OUTCOME_PREBUILT, 'This task did not run because its outcome was reused from a previous build'),
506 (OUTCOME_FAILED, 'This task did not complete'),
507 (OUTCOME_EMPTY, 'This task has no executable content'),
508 (OUTCOME_NA, ''),
509 )
510
511 search_allowed_fields = [ "recipe__name", "recipe__version", "task_name", "logfile" ]
512
513 def __init__(self, *args, **kwargs):
514 super(Task, self).__init__(*args, **kwargs)
515 try:
516 self._helptext = HelpText.objects.get(key=self.task_name, area=HelpText.VARIABLE, build=self.build).text
517 except HelpText.DoesNotExist:
518 self._helptext = None
519
520 def get_related_setscene(self):
521 return Task.objects.filter(task_executed=True, build = self.build, recipe = self.recipe, task_name=self.task_name+"_setscene")
522
523 def get_outcome_text(self):
524 return Task.TASK_OUTCOME[int(self.outcome) + 1][1]
525
526 def get_outcome_help(self):
527 return Task.TASK_OUTCOME_HELP[int(self.outcome)][1]
528
529 def get_sstate_text(self):
530 if self.sstate_result==Task.SSTATE_NA:
531 return ''
532 else:
533 return Task.SSTATE_RESULT[int(self.sstate_result)][1]
534
535 def get_executed_display(self):
536 if self.task_executed:
537 return "Executed"
538 return "Not Executed"
539
540 def get_description(self):
541 return self._helptext
542
543 build = models.ForeignKey(Build, related_name='task_build')
544 order = models.IntegerField(null=True)
545 task_executed = models.BooleanField(default=False) # True means Executed, False means Not/Executed
546 outcome = models.IntegerField(choices=TASK_OUTCOME, default=OUTCOME_NA)
547 sstate_checksum = models.CharField(max_length=100, blank=True)
548 path_to_sstate_obj = models.FilePathField(max_length=500, blank=True)
549 recipe = models.ForeignKey('Recipe', related_name='tasks')
550 task_name = models.CharField(max_length=100)
551 source_url = models.FilePathField(max_length=255, blank=True)
552 work_directory = models.FilePathField(max_length=255, blank=True)
553 script_type = models.IntegerField(choices=TASK_CODING, default=CODING_NA)
554 line_number = models.IntegerField(default=0)
555 disk_io = models.IntegerField(null=True)
556 cpu_usage = models.DecimalField(max_digits=8, decimal_places=2, null=True)
557 elapsed_time = models.DecimalField(max_digits=8, decimal_places=2, null=True)
558 sstate_result = models.IntegerField(choices=SSTATE_RESULT, default=SSTATE_NA)
559 message = models.CharField(max_length=240)
560 logfile = models.FilePathField(max_length=255, blank=True)
561
562 outcome_text = property(get_outcome_text)
563 sstate_text = property(get_sstate_text)
564
565 def __unicode__(self):
566 return "%d(%d) %s:%s" % (self.pk, self.build.pk, self.recipe.name, self.task_name)
567
568 class Meta:
569 ordering = ('order', 'recipe' ,)
570 unique_together = ('build', 'recipe', 'task_name', )
571
572
573class Task_Dependency(models.Model):
574 task = models.ForeignKey(Task, related_name='task_dependencies_task')
575 depends_on = models.ForeignKey(Task, related_name='task_dependencies_depends')
576
577class Package(models.Model):
578 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 -0500579 build = models.ForeignKey('Build', null=True)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500580 recipe = models.ForeignKey('Recipe', null=True)
581 name = models.CharField(max_length=100)
582 installed_name = models.CharField(max_length=100, default='')
583 version = models.CharField(max_length=100, blank=True)
584 revision = models.CharField(max_length=32, blank=True)
585 summary = models.TextField(blank=True)
586 description = models.TextField(blank=True)
587 size = models.IntegerField(default=0)
588 installed_size = models.IntegerField(default=0)
589 section = models.CharField(max_length=80, blank=True)
590 license = models.CharField(max_length=80, blank=True)
591
592class Package_DependencyManager(models.Manager):
593 use_for_related_fields = True
594
595 def get_query_set(self):
596 return super(Package_DependencyManager, self).get_query_set().exclude(package_id = F('depends_on__id'))
597
598class Package_Dependency(models.Model):
599 TYPE_RDEPENDS = 0
600 TYPE_TRDEPENDS = 1
601 TYPE_RRECOMMENDS = 2
602 TYPE_TRECOMMENDS = 3
603 TYPE_RSUGGESTS = 4
604 TYPE_RPROVIDES = 5
605 TYPE_RREPLACES = 6
606 TYPE_RCONFLICTS = 7
607 ' TODO: bpackage should be changed to remove the DEPENDS_TYPE access '
608 DEPENDS_TYPE = (
609 (TYPE_RDEPENDS, "depends"),
610 (TYPE_TRDEPENDS, "depends"),
611 (TYPE_TRECOMMENDS, "recommends"),
612 (TYPE_RRECOMMENDS, "recommends"),
613 (TYPE_RSUGGESTS, "suggests"),
614 (TYPE_RPROVIDES, "provides"),
615 (TYPE_RREPLACES, "replaces"),
616 (TYPE_RCONFLICTS, "conflicts"),
617 )
618 """ Indexed by dep_type, in view order, key for short name and help
619 description which when viewed will be printf'd with the
620 package name.
621 """
622 DEPENDS_DICT = {
623 TYPE_RDEPENDS : ("depends", "%s is required to run %s"),
624 TYPE_TRDEPENDS : ("depends", "%s is required to run %s"),
625 TYPE_TRECOMMENDS : ("recommends", "%s extends the usability of %s"),
626 TYPE_RRECOMMENDS : ("recommends", "%s extends the usability of %s"),
627 TYPE_RSUGGESTS : ("suggests", "%s is suggested for installation with %s"),
628 TYPE_RPROVIDES : ("provides", "%s is provided by %s"),
629 TYPE_RREPLACES : ("replaces", "%s is replaced by %s"),
630 TYPE_RCONFLICTS : ("conflicts", "%s conflicts with %s, which will not be installed if this package is not first removed"),
631 }
632
633 package = models.ForeignKey(Package, related_name='package_dependencies_source')
634 depends_on = models.ForeignKey(Package, related_name='package_dependencies_target') # soft dependency
635 dep_type = models.IntegerField(choices=DEPENDS_TYPE)
636 target = models.ForeignKey(Target, null=True)
637 objects = Package_DependencyManager()
638
639class Target_Installed_Package(models.Model):
640 target = models.ForeignKey(Target)
641 package = models.ForeignKey(Package, related_name='buildtargetlist_package')
642
643class Package_File(models.Model):
644 package = models.ForeignKey(Package, related_name='buildfilelist_package')
645 path = models.FilePathField(max_length=255, blank=True)
646 size = models.IntegerField()
647
648class Recipe(models.Model):
649 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']
650
651 layer_source = models.ForeignKey('LayerSource', default = None, null = True) # from where did we get this recipe
652 up_id = models.IntegerField(null = True, default = None) # id of entry in the source
653 up_date = models.DateTimeField(null = True, default = None)
654
655 name = models.CharField(max_length=100, blank=True) # pn
656 version = models.CharField(max_length=100, blank=True) # pv
657 layer_version = models.ForeignKey('Layer_Version', related_name='recipe_layer_version')
658 summary = models.TextField(blank=True)
659 description = models.TextField(blank=True)
660 section = models.CharField(max_length=100, blank=True)
661 license = models.CharField(max_length=200, blank=True)
662 homepage = models.URLField(blank=True)
663 bugtracker = models.URLField(blank=True)
664 file_path = models.FilePathField(max_length=255)
665 pathflags = models.CharField(max_length=200, blank=True)
666 is_image = models.BooleanField(default=False)
667
668 def get_layersource_view_url(self):
669 if self.layer_source is None:
670 return ""
671
672 url = self.layer_source.get_object_view(self.layer_version.up_branch, "recipes", self.name)
673 return url
674
675 def __unicode__(self):
676 return "Recipe " + self.name + ":" + self.version
677
678 def get_vcs_recipe_file_link_url(self):
679 return self.layer_version.get_vcs_file_link_url(self.file_path)
680
681 def get_description_or_summary(self):
682 if self.description:
683 return self.description
684 elif self.summary:
685 return self.summary
686 else:
687 return ""
688
689 class Meta:
690 unique_together = (("layer_version", "file_path", "pathflags"), )
691
692
693class Recipe_DependencyManager(models.Manager):
694 use_for_related_fields = True
695
696 def get_query_set(self):
697 return super(Recipe_DependencyManager, self).get_query_set().exclude(recipe_id = F('depends_on__id'))
698
699class Recipe_Dependency(models.Model):
700 TYPE_DEPENDS = 0
701 TYPE_RDEPENDS = 1
702
703 DEPENDS_TYPE = (
704 (TYPE_DEPENDS, "depends"),
705 (TYPE_RDEPENDS, "rdepends"),
706 )
707 recipe = models.ForeignKey(Recipe, related_name='r_dependencies_recipe')
708 depends_on = models.ForeignKey(Recipe, related_name='r_dependencies_depends')
709 dep_type = models.IntegerField(choices=DEPENDS_TYPE)
710 objects = Recipe_DependencyManager()
711
712
713class Machine(models.Model):
714 search_allowed_fields = ["name", "description", "layer_version__layer__name"]
715 layer_source = models.ForeignKey('LayerSource', default = None, null = True) # from where did we get this machine
716 up_id = models.IntegerField(null = True, default = None) # id of entry in the source
717 up_date = models.DateTimeField(null = True, default = None)
718
719 layer_version = models.ForeignKey('Layer_Version')
720 name = models.CharField(max_length=255)
721 description = models.CharField(max_length=255)
722
723 def get_vcs_machine_file_link_url(self):
724 path = 'conf/machine/'+self.name+'.conf'
725
726 return self.layer_version.get_vcs_file_link_url(path)
727
728 def __unicode__(self):
729 return "Machine " + self.name + "(" + self.description + ")"
730
731 class Meta:
732 unique_together = ("layer_source", "up_id")
733
734
735from django.db.models.base import ModelBase
736
737class InheritanceMetaclass(ModelBase):
738 def __call__(cls, *args, **kwargs):
739 obj = super(InheritanceMetaclass, cls).__call__(*args, **kwargs)
740 return obj.get_object()
741
742
743class LayerSource(models.Model):
744 __metaclass__ = InheritanceMetaclass
745
746 class Meta:
747 unique_together = (('sourcetype', 'apiurl'), )
748
749 TYPE_LOCAL = 0
750 TYPE_LAYERINDEX = 1
751 TYPE_IMPORTED = 2
752 SOURCE_TYPE = (
753 (TYPE_LOCAL, "local"),
754 (TYPE_LAYERINDEX, "layerindex"),
755 (TYPE_IMPORTED, "imported"),
756 )
757
758 name = models.CharField(max_length=63, unique = True)
759 sourcetype = models.IntegerField(choices=SOURCE_TYPE)
760 apiurl = models.CharField(max_length=255, null=True, default=None)
761
762 def __init__(self, *args, **kwargs):
763 super(LayerSource, self).__init__(*args, **kwargs)
764 if self.sourcetype == LayerSource.TYPE_LOCAL:
765 self.__class__ = LocalLayerSource
766 elif self.sourcetype == LayerSource.TYPE_LAYERINDEX:
767 self.__class__ = LayerIndexLayerSource
768 elif self.sourcetype == LayerSource.TYPE_IMPORTED:
769 self.__class__ = ImportedLayerSource
770 elif self.sourcetype == None:
771 raise Exception("Unknown LayerSource-derived class. If you added a new layer source type, fill out all code stubs.")
772
773
774 def update(self):
775 """
776 Updates the local database information from the upstream layer source
777 """
778 raise Exception("Abstract, update() must be implemented by all LayerSource-derived classes (object is %s)" % str(vars(self)))
779
780 def save(self, *args, **kwargs):
781 return super(LayerSource, self).save(*args, **kwargs)
782
783 def get_object(self):
784 # preset an un-initilized object
785 if None == self.name:
786 self.name=""
787 if None == self.apiurl:
788 self.apiurl=""
789 if None == self.sourcetype:
790 self.sourcetype=LayerSource.TYPE_LOCAL
791
792 if self.sourcetype == LayerSource.TYPE_LOCAL:
793 self.__class__ = LocalLayerSource
794 elif self.sourcetype == LayerSource.TYPE_LAYERINDEX:
795 self.__class__ = LayerIndexLayerSource
796 elif self.sourcetype == LayerSource.TYPE_IMPORTED:
797 self.__class__ = ImportedLayerSource
798 else:
799 raise Exception("Unknown LayerSource type. If you added a new layer source type, fill out all code stubs.")
800 return self
801
802 def __unicode__(self):
803 return "%s (%s)" % (self.name, self.sourcetype)
804
805
806class LocalLayerSource(LayerSource):
807 class Meta(LayerSource._meta.__class__):
808 proxy = True
809
810 def __init__(self, *args, **kwargs):
811 super(LocalLayerSource, self).__init__(args, kwargs)
812 self.sourcetype = LayerSource.TYPE_LOCAL
813
814 def update(self):
815 """
816 Fetches layer, recipe and machine information from local repository
817 """
818 pass
819
820class ImportedLayerSource(LayerSource):
821 class Meta(LayerSource._meta.__class__):
822 proxy = True
823
824 def __init__(self, *args, **kwargs):
825 super(ImportedLayerSource, self).__init__(args, kwargs)
826 self.sourcetype = LayerSource.TYPE_IMPORTED
827
828 def update(self):
829 """
830 Fetches layer, recipe and machine information from local repository
831 """
832 pass
833
834
835class LayerIndexLayerSource(LayerSource):
836 class Meta(LayerSource._meta.__class__):
837 proxy = True
838
839 def __init__(self, *args, **kwargs):
840 super(LayerIndexLayerSource, self).__init__(args, kwargs)
841 self.sourcetype = LayerSource.TYPE_LAYERINDEX
842
843 def get_object_view(self, branch, objectype, upid):
844 return self.apiurl + "../branch/" + branch.name + "/" + objectype + "/?q=" + str(upid)
845
846 def update(self):
847 """
848 Fetches layer, recipe and machine information from remote repository
849 """
850 assert self.apiurl is not None
851 from django.db import transaction, connection
852
853 import urllib2, urlparse, json
854 import os
855 proxy_settings = os.environ.get("http_proxy", None)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500856 oe_core_layer = 'openembedded-core'
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500857
858 def _get_json_response(apiurl = self.apiurl):
859 _parsedurl = urlparse.urlparse(apiurl)
860 path = _parsedurl.path
861
862 try:
863 res = urllib2.urlopen(apiurl)
864 except urllib2.URLError as e:
865 raise Exception("Failed to read %s: %s" % (path, e.reason))
866
867 return json.loads(res.read())
868
869 # verify we can get the basic api
870 try:
871 apilinks = _get_json_response()
872 except Exception as e:
873 import traceback
874 if proxy_settings is not None:
875 logger.info("EE: Using proxy %s" % proxy_settings)
876 logger.warning("EE: could not connect to %s, skipping update: %s\n%s" % (self.apiurl, e, traceback.format_exc(e)))
877 return
878
879 # update branches; only those that we already have names listed in the
880 # Releases table
881 whitelist_branch_names = map(lambda x: x.branch_name, Release.objects.all())
882 if len(whitelist_branch_names) == 0:
883 raise Exception("Failed to make list of branches to fetch")
884
885 logger.debug("Fetching branches")
886 branches_info = _get_json_response(apilinks['branches']
887 + "?filter=name:%s" % "OR".join(whitelist_branch_names))
888 for bi in branches_info:
889 b, created = Branch.objects.get_or_create(layer_source = self, name = bi['name'])
890 b.up_id = bi['id']
891 b.up_date = bi['updated']
892 b.name = bi['name']
893 b.short_description = bi['short_description']
894 b.save()
895
896 # update layers
897 layers_info = _get_json_response(apilinks['layerItems'])
898 if not connection.features.autocommits_when_autocommit_is_off:
899 transaction.set_autocommit(False)
900 for li in layers_info:
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500901 # Special case for the openembedded-core layer
902 if li['name'] == oe_core_layer:
903 try:
904 # If we have an existing openembedded-core for example
905 # from the toasterconf.json augment the info using the
906 # layerindex rather than duplicate it
907 oe_core_l = Layer.objects.get(name=oe_core_layer)
908 # Take ownership of the layer as now coming from the
909 # layerindex
910 oe_core_l.layer_source = self
911 oe_core_l.up_id = li['id']
912 oe_core_l.summary = li['summary']
913 oe_core_l.description = li['description']
914 oe_core_l.save()
915 continue
916
917 except Layer.DoesNotExist:
918 pass
919
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500920 l, created = Layer.objects.get_or_create(layer_source = self, name = li['name'])
921 l.up_id = li['id']
922 l.up_date = li['updated']
923 l.vcs_url = li['vcs_url']
924 l.vcs_web_url = li['vcs_web_url']
925 l.vcs_web_tree_base_url = li['vcs_web_tree_base_url']
926 l.vcs_web_file_base_url = li['vcs_web_file_base_url']
927 l.summary = li['summary']
928 l.description = li['description']
929 l.save()
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500930
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500931 if not connection.features.autocommits_when_autocommit_is_off:
932 transaction.set_autocommit(True)
933
934 # update layerbranches/layer_versions
935 logger.debug("Fetching layer information")
936 layerbranches_info = _get_json_response(apilinks['layerBranches']
937 + "?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] ))
938 )
939
940 if not connection.features.autocommits_when_autocommit_is_off:
941 transaction.set_autocommit(False)
942 for lbi in layerbranches_info:
943 lv, created = Layer_Version.objects.get_or_create(layer_source = self,
944 up_id = lbi['id'],
945 layer=Layer.objects.get(layer_source = self, up_id = lbi['layer'])
946 )
947
948 lv.up_date = lbi['updated']
949 lv.up_branch = Branch.objects.get(layer_source = self, up_id = lbi['branch'])
950 lv.branch = lbi['actual_branch']
951 lv.commit = lbi['actual_branch']
952 lv.dirpath = lbi['vcs_subdir']
953 lv.save()
954 if not connection.features.autocommits_when_autocommit_is_off:
955 transaction.set_autocommit(True)
956
957 # update layer dependencies
958 layerdependencies_info = _get_json_response(apilinks['layerDependencies'])
959 dependlist = {}
960 if not connection.features.autocommits_when_autocommit_is_off:
961 transaction.set_autocommit(False)
962 for ldi in layerdependencies_info:
963 try:
964 lv = Layer_Version.objects.get(layer_source = self, up_id = ldi['layerbranch'])
965 except Layer_Version.DoesNotExist as e:
966 continue
967
968 if lv not in dependlist:
969 dependlist[lv] = []
970 try:
971 dependlist[lv].append(Layer_Version.objects.get(layer_source = self, layer__up_id = ldi['dependency'], up_branch = lv.up_branch))
972 except Layer_Version.DoesNotExist:
973 logger.warning("Cannot find layer version (ls:%s), up_id:%s lv:%s" % (self, ldi['dependency'], lv))
974
975 for lv in dependlist:
976 LayerVersionDependency.objects.filter(layer_version = lv).delete()
977 for lvd in dependlist[lv]:
978 LayerVersionDependency.objects.get_or_create(layer_version = lv, depends_on = lvd)
979 if not connection.features.autocommits_when_autocommit_is_off:
980 transaction.set_autocommit(True)
981
982
983 # update machines
984 logger.debug("Fetching machine information")
985 machines_info = _get_json_response(apilinks['machines']
986 + "?filter=layerbranch:%s" % "OR".join(map(lambda x: str(x.up_id), Layer_Version.objects.filter(layer_source = self)))
987 )
988
989 if not connection.features.autocommits_when_autocommit_is_off:
990 transaction.set_autocommit(False)
991 for mi in machines_info:
992 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']))
993 mo.up_date = mi['updated']
994 mo.name = mi['name']
995 mo.description = mi['description']
996 mo.save()
997
998 if not connection.features.autocommits_when_autocommit_is_off:
999 transaction.set_autocommit(True)
1000
1001 # update recipes; paginate by layer version / layer branch
1002 logger.debug("Fetching target information")
1003 recipes_info = _get_json_response(apilinks['recipes']
1004 + "?filter=layerbranch:%s" % "OR".join(map(lambda x: str(x.up_id), Layer_Version.objects.filter(layer_source = self)))
1005 )
1006 if not connection.features.autocommits_when_autocommit_is_off:
1007 transaction.set_autocommit(False)
1008 for ri in recipes_info:
1009 try:
1010 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']))
1011 ro.up_date = ri['updated']
1012 ro.name = ri['pn']
1013 ro.version = ri['pv']
1014 ro.summary = ri['summary']
1015 ro.description = ri['description']
1016 ro.section = ri['section']
1017 ro.license = ri['license']
1018 ro.homepage = ri['homepage']
1019 ro.bugtracker = ri['bugtracker']
1020 ro.file_path = ri['filepath'] + "/" + ri['filename']
1021 if 'inherits' in ri:
1022 ro.is_image = 'image' in ri['inherits'].split()
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001023 else: # workaround for old style layer index
1024 ro.is_image = "-image-" in ri['pn']
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001025 ro.save()
1026 except IntegrityError as e:
1027 logger.debug("Failed saving recipe, ignoring: %s (%s:%s)" % (e, ro.layer_version, ri['filepath']+"/"+ri['filename']))
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001028 ro.delete()
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001029 if not connection.features.autocommits_when_autocommit_is_off:
1030 transaction.set_autocommit(True)
1031
1032class BitbakeVersion(models.Model):
1033
1034 name = models.CharField(max_length=32, unique = True)
1035 giturl = GitURLField()
1036 branch = models.CharField(max_length=32)
1037 dirpath = models.CharField(max_length=255)
1038
1039 def __unicode__(self):
1040 return "%s (Branch: %s)" % (self.name, self.branch)
1041
1042
1043class Release(models.Model):
1044 """ A release is a project template, used to pre-populate Project settings with a configuration set """
1045 name = models.CharField(max_length=32, unique = True)
1046 description = models.CharField(max_length=255)
1047 bitbake_version = models.ForeignKey(BitbakeVersion)
1048 branch_name = models.CharField(max_length=50, default = "")
1049 helptext = models.TextField(null=True)
1050
1051 def __unicode__(self):
1052 return "%s (%s)" % (self.name, self.branch_name)
1053
1054class ReleaseLayerSourcePriority(models.Model):
1055 """ Each release selects layers from the set up layer sources, ordered by priority """
1056 release = models.ForeignKey("Release")
1057 layer_source = models.ForeignKey("LayerSource")
1058 priority = models.IntegerField(default = 0)
1059
1060 def __unicode__(self):
1061 return "%s-%s:%d" % (self.release.name, self.layer_source.name, self.priority)
1062 class Meta:
1063 unique_together = (('release', 'layer_source'),)
1064
1065
1066class ReleaseDefaultLayer(models.Model):
1067 release = models.ForeignKey(Release)
1068 layer_name = models.CharField(max_length=100, default="")
1069
1070
1071# Branch class is synced with layerindex.Branch, branches can only come from remote layer indexes
1072class Branch(models.Model):
1073 layer_source = models.ForeignKey('LayerSource', null = True, default = True)
1074 up_id = models.IntegerField(null = True, default = None) # id of branch in the source
1075 up_date = models.DateTimeField(null = True, default = None)
1076
1077 name = models.CharField(max_length=50)
1078 short_description = models.CharField(max_length=50, blank=True)
1079
1080 class Meta:
1081 verbose_name_plural = "Branches"
1082 unique_together = (('layer_source', 'name'),('layer_source', 'up_id'))
1083
1084 def __unicode__(self):
1085 return self.name
1086
1087
1088# Layer class synced with layerindex.LayerItem
1089class Layer(models.Model):
1090 layer_source = models.ForeignKey(LayerSource, null = True, default = None) # from where did we got this layer
1091 up_id = models.IntegerField(null = True, default = None) # id of layer in the remote source
1092 up_date = models.DateTimeField(null = True, default = None)
1093
1094 name = models.CharField(max_length=100)
1095 layer_index_url = models.URLField()
1096 vcs_url = GitURLField(default = None, null = True)
1097 vcs_web_url = models.URLField(null = True, default = None)
1098 vcs_web_tree_base_url = models.URLField(null = True, default = None)
1099 vcs_web_file_base_url = models.URLField(null = True, default = None)
1100
1101 summary = models.TextField(help_text='One-line description of the layer', null = True, default = None)
1102 description = models.TextField(null = True, default = None)
1103
1104 def __unicode__(self):
1105 return "%s / %s " % (self.name, self.layer_source)
1106
1107 class Meta:
1108 unique_together = (("layer_source", "up_id"), ("layer_source", "name"))
1109
1110
1111# LayerCommit class is synced with layerindex.LayerBranch
1112class Layer_Version(models.Model):
1113 search_allowed_fields = ["layer__name", "layer__summary", "layer__description", "layer__vcs_url", "dirpath", "up_branch__name", "commit", "branch"]
1114 build = models.ForeignKey(Build, related_name='layer_version_build', default = None, null = True)
1115 layer = models.ForeignKey(Layer, related_name='layer_version_layer')
1116
1117 layer_source = models.ForeignKey(LayerSource, null = True, default = None) # from where did we get this Layer Version
1118 up_id = models.IntegerField(null = True, default = None) # id of layerbranch in the remote source
1119 up_date = models.DateTimeField(null = True, default = None)
1120 up_branch = models.ForeignKey(Branch, null = True, default = None)
1121
1122 branch = models.CharField(max_length=80) # LayerBranch.actual_branch
1123 commit = models.CharField(max_length=100) # LayerBranch.vcs_last_rev
1124 dirpath = models.CharField(max_length=255, null = True, default = None) # LayerBranch.vcs_subdir
1125 priority = models.IntegerField(default = 0) # if -1, this is a default layer
1126
1127 local_path = models.FilePathField(max_length=1024, default = "/") # where this layer was checked-out
1128
1129 project = models.ForeignKey('Project', null = True, default = None) # Set if this layer is project-specific; always set for imported layers, and project-set branches
1130
1131 # code lifted, with adaptations, from the layerindex-web application https://git.yoctoproject.org/cgit/cgit.cgi/layerindex-web/
1132 def _handle_url_path(self, base_url, path):
1133 import re, posixpath
1134 if base_url:
1135 if self.dirpath:
1136 if path:
1137 extra_path = self.dirpath + '/' + path
1138 # Normalise out ../ in path for usage URL
1139 extra_path = posixpath.normpath(extra_path)
1140 # Minor workaround to handle case where subdirectory has been added between branches
1141 # (should probably support usage URL per branch to handle this... sigh...)
1142 if extra_path.startswith('../'):
1143 extra_path = extra_path[3:]
1144 else:
1145 extra_path = self.dirpath
1146 else:
1147 extra_path = path
1148 branchname = self.up_branch.name
1149 url = base_url.replace('%branch%', branchname)
1150
1151 # If there's a % in the path (e.g. a wildcard bbappend) we need to encode it
1152 if extra_path:
1153 extra_path = extra_path.replace('%', '%25')
1154
1155 if '%path%' in base_url:
1156 if extra_path:
1157 url = re.sub(r'\[([^\]]*%path%[^\]]*)\]', '\\1', url)
1158 else:
1159 url = re.sub(r'\[([^\]]*%path%[^\]]*)\]', '', url)
1160 return url.replace('%path%', extra_path)
1161 else:
1162 return url + extra_path
1163 return None
1164
1165 def get_vcs_link_url(self):
1166 if self.layer.vcs_web_url is None:
1167 return None
1168 return self.layer.vcs_web_url
1169
1170 def get_vcs_file_link_url(self, file_path=""):
1171 if self.layer.vcs_web_file_base_url is None:
1172 return None
1173 return self._handle_url_path(self.layer.vcs_web_file_base_url, file_path)
1174
1175 def get_vcs_dirpath_link_url(self):
1176 if self.layer.vcs_web_tree_base_url is None:
1177 return None
1178 return self._handle_url_path(self.layer.vcs_web_tree_base_url, '')
1179
1180 def get_equivalents_wpriority(self, project):
1181 return project.compatible_layerversions(layer_name = self.layer.name)
1182
1183 def get_vcs_reference(self):
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001184 if self.branch is not None and len(self.branch) > 0:
1185 return self.branch
1186 if self.up_branch is not None:
1187 return self.up_branch.name
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001188 if self.commit is not None and len(self.commit) > 0:
1189 return self.commit
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001190 return ("Cannot determine the vcs_reference for layer version %s" % vars(self))
1191
1192 def get_detailspage_url(self, project_id):
1193 return reverse('layerdetails', args=(project_id, self.pk))
1194
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001195 def get_alldeps(self, project_id):
1196 """Get full list of unique layer dependencies."""
1197 def gen_layerdeps(lver, project):
1198 for ldep in lver.dependencies.all():
1199 yield ldep.depends_on
1200 # get next level of deps recursively calling gen_layerdeps
1201 for subdep in gen_layerdeps(ldep.depends_on, project):
1202 yield subdep
1203
1204 project = Project.objects.get(pk=project_id)
1205 result = []
1206 projectlvers = [player.layercommit for player in project.projectlayer_set.all()]
1207 for dep in gen_layerdeps(self, project):
1208 # filter out duplicates and layers already belonging to the project
1209 if dep not in result + projectlvers:
1210 result.append(dep)
1211
1212 return sorted(result, key=lambda x: x.layer.name)
1213
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001214 def __unicode__(self):
1215 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")
1216
1217 class Meta:
1218 unique_together = ("layer_source", "up_id")
1219
1220class LayerVersionDependency(models.Model):
1221 layer_source = models.ForeignKey(LayerSource, null = True, default = None) # from where did we got this layer
1222 up_id = models.IntegerField(null = True, default = None) # id of layerbranch in the remote source
1223
1224 layer_version = models.ForeignKey(Layer_Version, related_name="dependencies")
1225 depends_on = models.ForeignKey(Layer_Version, related_name="dependees")
1226
1227 class Meta:
1228 unique_together = ("layer_source", "up_id")
1229
1230class ProjectLayer(models.Model):
1231 project = models.ForeignKey(Project)
1232 layercommit = models.ForeignKey(Layer_Version, null=True)
1233 optional = models.BooleanField(default = True)
1234
1235 def __unicode__(self):
1236 return "%s, %s" % (self.project.name, self.layercommit)
1237
1238 class Meta:
1239 unique_together = (("project", "layercommit"),)
1240
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001241class CustomImageRecipe(models.Model):
1242 name = models.CharField(max_length=100)
1243 base_recipe = models.ForeignKey(Recipe)
1244 packages = models.ManyToManyField(Package)
1245 project = models.ForeignKey(Project)
1246
1247 class Meta:
1248 unique_together = ("name", "project")
1249
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001250class ProjectVariable(models.Model):
1251 project = models.ForeignKey(Project)
1252 name = models.CharField(max_length=100)
1253 value = models.TextField(blank = True)
1254
1255class Variable(models.Model):
1256 search_allowed_fields = ['variable_name', 'variable_value',
1257 'vhistory__file_name', "description"]
1258 build = models.ForeignKey(Build, related_name='variable_build')
1259 variable_name = models.CharField(max_length=100)
1260 variable_value = models.TextField(blank=True)
1261 changed = models.BooleanField(default=False)
1262 human_readable_name = models.CharField(max_length=200)
1263 description = models.TextField(blank=True)
1264
1265class VariableHistory(models.Model):
1266 variable = models.ForeignKey(Variable, related_name='vhistory')
1267 value = models.TextField(blank=True)
1268 file_name = models.FilePathField(max_length=255)
1269 line_number = models.IntegerField(null=True)
1270 operation = models.CharField(max_length=64)
1271
1272class HelpText(models.Model):
1273 VARIABLE = 0
1274 HELPTEXT_AREA = ((VARIABLE, 'variable'), )
1275
1276 build = models.ForeignKey(Build, related_name='helptext_build')
1277 area = models.IntegerField(choices=HELPTEXT_AREA)
1278 key = models.CharField(max_length=100)
1279 text = models.TextField()
1280
1281class LogMessage(models.Model):
1282 EXCEPTION = -1 # used to signal self-toaster-exceptions
1283 INFO = 0
1284 WARNING = 1
1285 ERROR = 2
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001286 CRITICAL = 3
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001287
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001288 LOG_LEVEL = (
1289 (INFO, "info"),
1290 (WARNING, "warn"),
1291 (ERROR, "error"),
1292 (CRITICAL, "critical"),
1293 (EXCEPTION, "toaster exception")
1294 )
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001295
1296 build = models.ForeignKey(Build)
1297 task = models.ForeignKey(Task, blank = True, null=True)
1298 level = models.IntegerField(choices=LOG_LEVEL, default=INFO)
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001299 message = models.TextField(blank=True, null=True)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001300 pathname = models.FilePathField(max_length=255, blank=True)
1301 lineno = models.IntegerField(null=True)
1302
1303 def __str__(self):
1304 return "%s %s %s" % (self.get_level_display(), self.message, self.build)
1305
1306def invalidate_cache(**kwargs):
1307 from django.core.cache import cache
1308 try:
1309 cache.clear()
1310 except Exception as e:
1311 logger.warning("Problem with cache backend: Failed to clear cache: %s" % e)
1312
1313django.db.models.signals.post_save.connect(invalidate_cache)
1314django.db.models.signals.post_delete.connect(invalidate_cache)