diff options
author | Patrick Williams <patrick@stwcx.xyz> | 2015-09-15 14:41:29 -0500 |
---|---|---|
committer | Patrick Williams <patrick@stwcx.xyz> | 2015-09-15 14:41:29 -0500 |
commit | c124f4f2e04dca16a428a76c89677328bc7bf908 (patch) | |
tree | cc1bfbbced5244130a102cdd63c72584b1d6e408 /bitbake/lib/toaster/orm/models.py | |
download | blackbird-openbmc-c124f4f2e04dca16a428a76c89677328bc7bf908.tar.gz blackbird-openbmc-c124f4f2e04dca16a428a76c89677328bc7bf908.zip |
Squashed 'yocto-poky/' content from commit ea562de
git-subtree-dir: yocto-poky
git-subtree-split: ea562de57590c966cd5a75fda8defecd397e6436
Diffstat (limited to 'bitbake/lib/toaster/orm/models.py')
-rw-r--r-- | bitbake/lib/toaster/orm/models.py | 1233 |
1 files changed, 1233 insertions, 0 deletions
diff --git a/bitbake/lib/toaster/orm/models.py b/bitbake/lib/toaster/orm/models.py new file mode 100644 index 000000000..e4d2e87ae --- /dev/null +++ b/bitbake/lib/toaster/orm/models.py @@ -0,0 +1,1233 @@ +# +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# BitBake Toaster Implementation +# +# Copyright (C) 2013 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from django.db import models, IntegrityError +from django.db.models import F, Q, Avg, Max +from django.utils import timezone + +from django.core.urlresolvers import reverse + +from django.core import validators +from django.conf import settings +import django.db.models.signals + + +import logging +logger = logging.getLogger("toaster") + + +class GitURLValidator(validators.URLValidator): + import re + regex = re.compile( + r'^(?:ssh|git|http|ftp)s?://' # http:// or https:// + r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... + r'localhost|' # localhost... + r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4 + r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' # ...or ipv6 + r'(?::\d+)?' # optional port + r'(?:/?|[/?]\S+)$', re.IGNORECASE) + +def GitURLField(**kwargs): + r = models.URLField(**kwargs) + for i in xrange(len(r.validators)): + if isinstance(r.validators[i], validators.URLValidator): + r.validators[i] = GitURLValidator() + return r + + +class ToasterSetting(models.Model): + name = models.CharField(max_length=63) + helptext = models.TextField() + value = models.CharField(max_length=255) + + def __unicode__(self): + return "Setting %s = %s" % (self.name, self.value) + +class ProjectManager(models.Manager): + def create_project(self, name, release): + if release is not None: + prj = self.model(name = name, bitbake_version = release.bitbake_version, release = release) + else: + prj = self.model(name = name, bitbake_version = None, release = None) + + prj.save() + + for defaultconf in ToasterSetting.objects.filter(name__startswith="DEFCONF_"): + name = defaultconf.name[8:] + ProjectVariable.objects.create( project = prj, + name = name, + value = defaultconf.value) + + if release is None: + return prj + + for rdl in release.releasedefaultlayer_set.all(): + try: + lv = Layer_Version.objects.filter(layer__name = rdl.layer_name, up_branch__name = release.branch_name)[0].get_equivalents_wpriority(prj)[0] + ProjectLayer.objects.create( project = prj, + layercommit = lv, + optional = False ) + except IndexError: + # we may have no valid layer version objects, and that's ok + pass + + return prj + + def create(self, *args, **kwargs): + raise Exception("Invalid call to Project.objects.create. Use Project.objects.create_project() to create a project") + + # return single object with is_default = True + def get_default_project(self): + projects = super(ProjectManager, self).filter(is_default = True) + if len(projects) > 1: + raise Exception("Inconsistent project data: multiple " + + "default projects (i.e. with is_default=True)") + elif len(projects) < 1: + raise Exception("Inconsistent project data: no default project found") + return projects[0] + +class Project(models.Model): + search_allowed_fields = ['name', 'short_description', 'release__name', 'release__branch_name'] + name = models.CharField(max_length=100) + short_description = models.CharField(max_length=50, blank=True) + bitbake_version = models.ForeignKey('BitbakeVersion', null=True) + release = models.ForeignKey("Release", null=True) + created = models.DateTimeField(auto_now_add = True) + updated = models.DateTimeField(auto_now = True) + # This is a horrible hack; since Toaster has no "User" model available when + # running in interactive mode, we can't reference the field here directly + # Instead, we keep a possible null reference to the User id, as not to force + # hard links to possibly missing models + user_id = models.IntegerField(null = True) + objects = ProjectManager() + + # set to True for the project which is the default container + # for builds initiated by the command line etc. + is_default = models.BooleanField(default = False) + + def __unicode__(self): + return "%s (Release %s, BBV %s)" % (self.name, self.release, self.bitbake_version) + + def get_current_machine_name(self): + try: + return self.projectvariable_set.get(name="MACHINE").value + except (ProjectVariable.DoesNotExist,IndexError): + return( "None" ); + + def get_number_of_builds(self): + try: + return len(Build.objects.filter( project = self.id )) + except (Build.DoesNotExist,IndexError): + return( 0 ) + + def get_last_build_id(self): + try: + return Build.objects.filter( project = self.id ).order_by('-completed_on')[0].id + except (Build.DoesNotExist,IndexError): + return( -1 ) + + def get_last_outcome(self): + build_id = self.get_last_build_id + if (-1 == build_id): + return( "" ) + try: + return Build.objects.filter( id = self.get_last_build_id )[ 0 ].outcome + except (Build.DoesNotExist,IndexError): + return( "not_found" ) + + def get_last_target(self): + build_id = self.get_last_build_id + if (-1 == build_id): + return( "" ) + try: + return Target.objects.filter(build = build_id)[0].target + except (Target.DoesNotExist,IndexError): + return( "not_found" ) + + def get_last_errors(self): + build_id = self.get_last_build_id + if (-1 == build_id): + return( 0 ) + try: + return Build.objects.filter(id = build_id)[ 0 ].errors.count() + except (Build.DoesNotExist,IndexError): + return( "not_found" ) + + def get_last_warnings(self): + build_id = self.get_last_build_id + if (-1 == build_id): + return( 0 ) + try: + return Build.objects.filter(id = build_id)[ 0 ].warnings.count() + except (Build.DoesNotExist,IndexError): + return( "not_found" ) + + def get_last_imgfiles(self): + build_id = self.get_last_build_id + if (-1 == build_id): + return( "" ) + try: + return Variable.objects.filter(build = build_id, variable_name = "IMAGE_FSTYPES")[ 0 ].variable_value + except (Variable.DoesNotExist,IndexError): + return( "not_found" ) + + # returns a queryset of compatible layers for a project + def compatible_layerversions(self, release = None, layer_name = None): + if release == None: + release = self.release + # layers on the same branch or layers specifically set for this project + queryset = Layer_Version.objects.filter((Q(up_branch__name = release.branch_name) & Q(project = None)) | Q(project = self) | Q(build__project = self)) + + if layer_name is not None: + # we select only a layer name + queryset = queryset.filter(layer__name = layer_name) + + # order by layer version priority + 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") + + return queryset + + def projectlayer_equivalent_set(self): + return self.compatible_layerversions().filter(layer__name__in = [x.layercommit.layer.name for x in self.projectlayer_set.all()]).select_related("up_branch") + + def get_available_machines(self): + """ Returns QuerySet of all Machines which are provided by the + Layers currently added to the Project """ + queryset = Machine.objects.filter(layer_version__in=self.projectlayer_equivalent_set) + return queryset + + def get_all_compatible_machines(self): + """ Returns QuerySet of all the compatible machines available to the + project including ones from Layers not currently added """ + compatible_layers = self.compatible_layerversions() + + queryset = Machine.objects.filter(layer_version__in=compatible_layers) + return queryset + + def get_available_recipes(self): + """ Returns QuerySet of all Recipes which are provided by the Layers + currently added to the Project """ + project_layers = self.projectlayer_equivalent_set() + queryset = Recipe.objects.filter(layer_version__in = project_layers) + + # Copied from get_all_compatible_recipes + search_maxids = map(lambda i: i[0], list(queryset.values('name').distinct().annotate(max_id=Max('id')).values_list('max_id'))) + queryset = queryset.filter(id__in=search_maxids).select_related('layer_version', 'layer_version__layer', 'layer_version__up_branch', 'layer_source') + # End copy + + return queryset + + def get_all_compatible_recipes(self): + """ Returns QuerySet of all the compatible Recipes available to the + project including ones from Layers not currently added """ + compatible_layerversions = self.compatible_layerversions() + queryset = Recipe.objects.filter(layer_version__in = compatible_layerversions) + + search_maxids = map(lambda i: i[0], list(queryset.values('name').distinct().annotate(max_id=Max('id')).values_list('max_id'))) + + queryset = queryset.filter(id__in=search_maxids).select_related('layer_version', 'layer_version__layer', 'layer_version__up_branch', 'layer_source') + return queryset + + + def schedule_build(self): + from bldcontrol.models import BuildRequest, BRTarget, BRLayer, BRVariable, BRBitbake + br = BuildRequest.objects.create(project = self) + try: + + BRBitbake.objects.create(req = br, + giturl = self.bitbake_version.giturl, + commit = self.bitbake_version.branch, + dirpath = self.bitbake_version.dirpath) + + for l in self.projectlayer_set.all().order_by("pk"): + commit = l.layercommit.get_vcs_reference() + print("ii Building layer ", l.layercommit.layer.name, " at vcs point ", commit) + BRLayer.objects.create(req = br, name = l.layercommit.layer.name, giturl = l.layercommit.layer.vcs_url, commit = commit, dirpath = l.layercommit.dirpath) + + br.state = BuildRequest.REQ_QUEUED + now = timezone.now() + br.build = Build.objects.create(project = self, + completed_on=now, + started_on=now, + ) + for t in self.projecttarget_set.all(): + BRTarget.objects.create(req = br, target = t.target, task = t.task) + Target.objects.create(build = br.build, target = t.target) + + for v in self.projectvariable_set.all(): + BRVariable.objects.create(req = br, name = v.name, value = v.value) + + + try: + br.build.machine = self.projectvariable_set.get(name = 'MACHINE').value + br.build.save() + except ProjectVariable.DoesNotExist: + pass + br.save() + except Exception: + # revert the build request creation since we're not done cleanly + br.delete() + raise + return br + +class Build(models.Model): + SUCCEEDED = 0 + FAILED = 1 + IN_PROGRESS = 2 + + BUILD_OUTCOME = ( + (SUCCEEDED, 'Succeeded'), + (FAILED, 'Failed'), + (IN_PROGRESS, 'In Progress'), + ) + + search_allowed_fields = ['machine', 'cooker_log_path', "target__target", "target__target_image_file__file_name"] + + project = models.ForeignKey(Project) # must have a project + machine = models.CharField(max_length=100) + distro = models.CharField(max_length=100) + distro_version = models.CharField(max_length=100) + started_on = models.DateTimeField() + completed_on = models.DateTimeField() + outcome = models.IntegerField(choices=BUILD_OUTCOME, default=IN_PROGRESS) + cooker_log_path = models.CharField(max_length=500) + build_name = models.CharField(max_length=100) + bitbake_version = models.CharField(max_length=50) + + def completeper(self): + tf = Task.objects.filter(build = self) + tfc = tf.count() + if tfc > 0: + completeper = tf.exclude(order__isnull=True).count()*100/tf.count() + else: + completeper = 0 + return completeper + + def eta(self): + eta = timezone.now() + completeper = self.completeper() + if self.completeper() > 0: + eta += ((eta - self.started_on)*(100-completeper))/completeper + return eta + + + def get_sorted_target_list(self): + tgts = Target.objects.filter(build_id = self.id).order_by( 'target' ); + return( tgts ); + + @property + def toaster_exceptions(self): + return self.logmessage_set.filter(level=LogMessage.EXCEPTION) + + @property + def errors(self): + return (self.logmessage_set.filter(level=LogMessage.ERROR)|self.logmessage_set.filter(level=LogMessage.EXCEPTION)) + + @property + def warnings(self): + return self.logmessage_set.filter(level=LogMessage.WARNING) + + @property + def timespent_seconds(self): + return (self.completed_on - self.started_on).total_seconds() + + def get_current_status(self): + from bldcontrol.models import BuildRequest + if self.outcome == Build.IN_PROGRESS and self.buildrequest.state != BuildRequest.REQ_INPROGRESS: + return self.buildrequest.get_state_display() + return self.get_outcome_display() + + def __str__(self): + return "%d %s %s" % (self.id, self.project, ",".join([t.target for t in self.target_set.all()])) + + +# an Artifact is anything that results from a Build, and may be of interest to the user, and is not stored elsewhere +class BuildArtifact(models.Model): + build = models.ForeignKey(Build) + file_name = models.FilePathField() + file_size = models.IntegerField() + + def get_local_file_name(self): + try: + deploydir = Variable.objects.get(build = self.build, variable_name="DEPLOY_DIR").variable_value + return self.file_name[len(deploydir)+1:] + except: + raise + + return self.file_name + + + def is_available(self): + return self.build.buildrequest.environment.has_artifact(self.file_name) + +class ProjectTarget(models.Model): + project = models.ForeignKey(Project) + target = models.CharField(max_length=100) + task = models.CharField(max_length=100, null=True) + +class Target(models.Model): + search_allowed_fields = ['target', 'file_name'] + build = models.ForeignKey(Build) + target = models.CharField(max_length=100) + task = models.CharField(max_length=100, null=True) + is_image = models.BooleanField(default = False) + image_size = models.IntegerField(default=0) + license_manifest_path = models.CharField(max_length=500, null=True) + + def package_count(self): + return Target_Installed_Package.objects.filter(target_id__exact=self.id).count() + + def __unicode__(self): + return self.target + +class Target_Image_File(models.Model): + target = models.ForeignKey(Target) + file_name = models.FilePathField(max_length=254) + file_size = models.IntegerField() + +class Target_File(models.Model): + ITYPE_REGULAR = 1 + ITYPE_DIRECTORY = 2 + ITYPE_SYMLINK = 3 + ITYPE_SOCKET = 4 + ITYPE_FIFO = 5 + ITYPE_CHARACTER = 6 + ITYPE_BLOCK = 7 + ITYPES = ( (ITYPE_REGULAR ,'regular'), + ( ITYPE_DIRECTORY ,'directory'), + ( ITYPE_SYMLINK ,'symlink'), + ( ITYPE_SOCKET ,'socket'), + ( ITYPE_FIFO ,'fifo'), + ( ITYPE_CHARACTER ,'character'), + ( ITYPE_BLOCK ,'block'), + ) + + target = models.ForeignKey(Target) + path = models.FilePathField() + size = models.IntegerField() + inodetype = models.IntegerField(choices = ITYPES) + permission = models.CharField(max_length=16) + owner = models.CharField(max_length=128) + group = models.CharField(max_length=128) + directory = models.ForeignKey('Target_File', related_name="directory_set", null=True) + sym_target = models.ForeignKey('Target_File', related_name="symlink_set", null=True) + + +class Task(models.Model): + + SSTATE_NA = 0 + SSTATE_MISS = 1 + SSTATE_FAILED = 2 + SSTATE_RESTORED = 3 + + SSTATE_RESULT = ( + (SSTATE_NA, 'Not Applicable'), # For rest of tasks, but they still need checking. + (SSTATE_MISS, 'File not in cache'), # the sstate object was not found + (SSTATE_FAILED, 'Failed'), # there was a pkg, but the script failed + (SSTATE_RESTORED, 'Succeeded'), # successfully restored + ) + + CODING_NA = 0 + CODING_PYTHON = 2 + CODING_SHELL = 3 + + TASK_CODING = ( + (CODING_NA, 'N/A'), + (CODING_PYTHON, 'Python'), + (CODING_SHELL, 'Shell'), + ) + + OUTCOME_NA = -1 + OUTCOME_SUCCESS = 0 + OUTCOME_COVERED = 1 + OUTCOME_CACHED = 2 + OUTCOME_PREBUILT = 3 + OUTCOME_FAILED = 4 + OUTCOME_EMPTY = 5 + + TASK_OUTCOME = ( + (OUTCOME_NA, 'Not Available'), + (OUTCOME_SUCCESS, 'Succeeded'), + (OUTCOME_COVERED, 'Covered'), + (OUTCOME_CACHED, 'Cached'), + (OUTCOME_PREBUILT, 'Prebuilt'), + (OUTCOME_FAILED, 'Failed'), + (OUTCOME_EMPTY, 'Empty'), + ) + + TASK_OUTCOME_HELP = ( + (OUTCOME_SUCCESS, 'This task successfully completed'), + (OUTCOME_COVERED, 'This task did not run because its output is provided by another task'), + (OUTCOME_CACHED, 'This task restored output from the sstate-cache directory or mirrors'), + (OUTCOME_PREBUILT, 'This task did not run because its outcome was reused from a previous build'), + (OUTCOME_FAILED, 'This task did not complete'), + (OUTCOME_EMPTY, 'This task has no executable content'), + (OUTCOME_NA, ''), + ) + + search_allowed_fields = [ "recipe__name", "recipe__version", "task_name", "logfile" ] + + def __init__(self, *args, **kwargs): + super(Task, self).__init__(*args, **kwargs) + try: + self._helptext = HelpText.objects.get(key=self.task_name, area=HelpText.VARIABLE, build=self.build).text + except HelpText.DoesNotExist: + self._helptext = None + + def get_related_setscene(self): + return Task.objects.filter(task_executed=True, build = self.build, recipe = self.recipe, task_name=self.task_name+"_setscene") + + def get_outcome_text(self): + return Task.TASK_OUTCOME[int(self.outcome) + 1][1] + + def get_outcome_help(self): + return Task.TASK_OUTCOME_HELP[int(self.outcome)][1] + + def get_sstate_text(self): + if self.sstate_result==Task.SSTATE_NA: + return '' + else: + return Task.SSTATE_RESULT[int(self.sstate_result)][1] + + def get_executed_display(self): + if self.task_executed: + return "Executed" + return "Not Executed" + + def get_description(self): + return self._helptext + + build = models.ForeignKey(Build, related_name='task_build') + order = models.IntegerField(null=True) + task_executed = models.BooleanField(default=False) # True means Executed, False means Not/Executed + outcome = models.IntegerField(choices=TASK_OUTCOME, default=OUTCOME_NA) + sstate_checksum = models.CharField(max_length=100, blank=True) + path_to_sstate_obj = models.FilePathField(max_length=500, blank=True) + recipe = models.ForeignKey('Recipe', related_name='tasks') + task_name = models.CharField(max_length=100) + source_url = models.FilePathField(max_length=255, blank=True) + work_directory = models.FilePathField(max_length=255, blank=True) + script_type = models.IntegerField(choices=TASK_CODING, default=CODING_NA) + line_number = models.IntegerField(default=0) + disk_io = models.IntegerField(null=True) + cpu_usage = models.DecimalField(max_digits=8, decimal_places=2, null=True) + elapsed_time = models.DecimalField(max_digits=8, decimal_places=2, null=True) + sstate_result = models.IntegerField(choices=SSTATE_RESULT, default=SSTATE_NA) + message = models.CharField(max_length=240) + logfile = models.FilePathField(max_length=255, blank=True) + + outcome_text = property(get_outcome_text) + sstate_text = property(get_sstate_text) + + def __unicode__(self): + return "%d(%d) %s:%s" % (self.pk, self.build.pk, self.recipe.name, self.task_name) + + class Meta: + ordering = ('order', 'recipe' ,) + unique_together = ('build', 'recipe', 'task_name', ) + + +class Task_Dependency(models.Model): + task = models.ForeignKey(Task, related_name='task_dependencies_task') + depends_on = models.ForeignKey(Task, related_name='task_dependencies_depends') + +class Package(models.Model): + 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'] + build = models.ForeignKey('Build') + recipe = models.ForeignKey('Recipe', null=True) + name = models.CharField(max_length=100) + installed_name = models.CharField(max_length=100, default='') + version = models.CharField(max_length=100, blank=True) + revision = models.CharField(max_length=32, blank=True) + summary = models.TextField(blank=True) + description = models.TextField(blank=True) + size = models.IntegerField(default=0) + installed_size = models.IntegerField(default=0) + section = models.CharField(max_length=80, blank=True) + license = models.CharField(max_length=80, blank=True) + +class Package_DependencyManager(models.Manager): + use_for_related_fields = True + + def get_query_set(self): + return super(Package_DependencyManager, self).get_query_set().exclude(package_id = F('depends_on__id')) + +class Package_Dependency(models.Model): + TYPE_RDEPENDS = 0 + TYPE_TRDEPENDS = 1 + TYPE_RRECOMMENDS = 2 + TYPE_TRECOMMENDS = 3 + TYPE_RSUGGESTS = 4 + TYPE_RPROVIDES = 5 + TYPE_RREPLACES = 6 + TYPE_RCONFLICTS = 7 + ' TODO: bpackage should be changed to remove the DEPENDS_TYPE access ' + DEPENDS_TYPE = ( + (TYPE_RDEPENDS, "depends"), + (TYPE_TRDEPENDS, "depends"), + (TYPE_TRECOMMENDS, "recommends"), + (TYPE_RRECOMMENDS, "recommends"), + (TYPE_RSUGGESTS, "suggests"), + (TYPE_RPROVIDES, "provides"), + (TYPE_RREPLACES, "replaces"), + (TYPE_RCONFLICTS, "conflicts"), + ) + """ Indexed by dep_type, in view order, key for short name and help + description which when viewed will be printf'd with the + package name. + """ + DEPENDS_DICT = { + TYPE_RDEPENDS : ("depends", "%s is required to run %s"), + TYPE_TRDEPENDS : ("depends", "%s is required to run %s"), + TYPE_TRECOMMENDS : ("recommends", "%s extends the usability of %s"), + TYPE_RRECOMMENDS : ("recommends", "%s extends the usability of %s"), + TYPE_RSUGGESTS : ("suggests", "%s is suggested for installation with %s"), + TYPE_RPROVIDES : ("provides", "%s is provided by %s"), + TYPE_RREPLACES : ("replaces", "%s is replaced by %s"), + TYPE_RCONFLICTS : ("conflicts", "%s conflicts with %s, which will not be installed if this package is not first removed"), + } + + package = models.ForeignKey(Package, related_name='package_dependencies_source') + depends_on = models.ForeignKey(Package, related_name='package_dependencies_target') # soft dependency + dep_type = models.IntegerField(choices=DEPENDS_TYPE) + target = models.ForeignKey(Target, null=True) + objects = Package_DependencyManager() + +class Target_Installed_Package(models.Model): + target = models.ForeignKey(Target) + package = models.ForeignKey(Package, related_name='buildtargetlist_package') + +class Package_File(models.Model): + package = models.ForeignKey(Package, related_name='buildfilelist_package') + path = models.FilePathField(max_length=255, blank=True) + size = models.IntegerField() + +class Recipe(models.Model): + 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'] + + layer_source = models.ForeignKey('LayerSource', default = None, null = True) # from where did we get this recipe + up_id = models.IntegerField(null = True, default = None) # id of entry in the source + up_date = models.DateTimeField(null = True, default = None) + + name = models.CharField(max_length=100, blank=True) # pn + version = models.CharField(max_length=100, blank=True) # pv + layer_version = models.ForeignKey('Layer_Version', related_name='recipe_layer_version') + summary = models.TextField(blank=True) + description = models.TextField(blank=True) + section = models.CharField(max_length=100, blank=True) + license = models.CharField(max_length=200, blank=True) + homepage = models.URLField(blank=True) + bugtracker = models.URLField(blank=True) + file_path = models.FilePathField(max_length=255) + pathflags = models.CharField(max_length=200, blank=True) + is_image = models.BooleanField(default=False) + + def get_layersource_view_url(self): + if self.layer_source is None: + return "" + + url = self.layer_source.get_object_view(self.layer_version.up_branch, "recipes", self.name) + return url + + def __unicode__(self): + return "Recipe " + self.name + ":" + self.version + + def get_vcs_recipe_file_link_url(self): + return self.layer_version.get_vcs_file_link_url(self.file_path) + + def get_description_or_summary(self): + if self.description: + return self.description + elif self.summary: + return self.summary + else: + return "" + + class Meta: + unique_together = (("layer_version", "file_path", "pathflags"), ) + + +class Recipe_DependencyManager(models.Manager): + use_for_related_fields = True + + def get_query_set(self): + return super(Recipe_DependencyManager, self).get_query_set().exclude(recipe_id = F('depends_on__id')) + +class Recipe_Dependency(models.Model): + TYPE_DEPENDS = 0 + TYPE_RDEPENDS = 1 + + DEPENDS_TYPE = ( + (TYPE_DEPENDS, "depends"), + (TYPE_RDEPENDS, "rdepends"), + ) + recipe = models.ForeignKey(Recipe, related_name='r_dependencies_recipe') + depends_on = models.ForeignKey(Recipe, related_name='r_dependencies_depends') + dep_type = models.IntegerField(choices=DEPENDS_TYPE) + objects = Recipe_DependencyManager() + + +class Machine(models.Model): + search_allowed_fields = ["name", "description", "layer_version__layer__name"] + layer_source = models.ForeignKey('LayerSource', default = None, null = True) # from where did we get this machine + up_id = models.IntegerField(null = True, default = None) # id of entry in the source + up_date = models.DateTimeField(null = True, default = None) + + layer_version = models.ForeignKey('Layer_Version') + name = models.CharField(max_length=255) + description = models.CharField(max_length=255) + + def get_vcs_machine_file_link_url(self): + path = 'conf/machine/'+self.name+'.conf' + + return self.layer_version.get_vcs_file_link_url(path) + + def __unicode__(self): + return "Machine " + self.name + "(" + self.description + ")" + + class Meta: + unique_together = ("layer_source", "up_id") + + +from django.db.models.base import ModelBase + +class InheritanceMetaclass(ModelBase): + def __call__(cls, *args, **kwargs): + obj = super(InheritanceMetaclass, cls).__call__(*args, **kwargs) + return obj.get_object() + + +class LayerSource(models.Model): + __metaclass__ = InheritanceMetaclass + + class Meta: + unique_together = (('sourcetype', 'apiurl'), ) + + TYPE_LOCAL = 0 + TYPE_LAYERINDEX = 1 + TYPE_IMPORTED = 2 + SOURCE_TYPE = ( + (TYPE_LOCAL, "local"), + (TYPE_LAYERINDEX, "layerindex"), + (TYPE_IMPORTED, "imported"), + ) + + name = models.CharField(max_length=63, unique = True) + sourcetype = models.IntegerField(choices=SOURCE_TYPE) + apiurl = models.CharField(max_length=255, null=True, default=None) + + def __init__(self, *args, **kwargs): + super(LayerSource, self).__init__(*args, **kwargs) + if self.sourcetype == LayerSource.TYPE_LOCAL: + self.__class__ = LocalLayerSource + elif self.sourcetype == LayerSource.TYPE_LAYERINDEX: + self.__class__ = LayerIndexLayerSource + elif self.sourcetype == LayerSource.TYPE_IMPORTED: + self.__class__ = ImportedLayerSource + elif self.sourcetype == None: + raise Exception("Unknown LayerSource-derived class. If you added a new layer source type, fill out all code stubs.") + + + def update(self): + """ + Updates the local database information from the upstream layer source + """ + raise Exception("Abstract, update() must be implemented by all LayerSource-derived classes (object is %s)" % str(vars(self))) + + def save(self, *args, **kwargs): + return super(LayerSource, self).save(*args, **kwargs) + + def get_object(self): + # preset an un-initilized object + if None == self.name: + self.name="" + if None == self.apiurl: + self.apiurl="" + if None == self.sourcetype: + self.sourcetype=LayerSource.TYPE_LOCAL + + if self.sourcetype == LayerSource.TYPE_LOCAL: + self.__class__ = LocalLayerSource + elif self.sourcetype == LayerSource.TYPE_LAYERINDEX: + self.__class__ = LayerIndexLayerSource + elif self.sourcetype == LayerSource.TYPE_IMPORTED: + self.__class__ = ImportedLayerSource + else: + raise Exception("Unknown LayerSource type. If you added a new layer source type, fill out all code stubs.") + return self + + def __unicode__(self): + return "%s (%s)" % (self.name, self.sourcetype) + + +class LocalLayerSource(LayerSource): + class Meta(LayerSource._meta.__class__): + proxy = True + + def __init__(self, *args, **kwargs): + super(LocalLayerSource, self).__init__(args, kwargs) + self.sourcetype = LayerSource.TYPE_LOCAL + + def update(self): + """ + Fetches layer, recipe and machine information from local repository + """ + pass + +class ImportedLayerSource(LayerSource): + class Meta(LayerSource._meta.__class__): + proxy = True + + def __init__(self, *args, **kwargs): + super(ImportedLayerSource, self).__init__(args, kwargs) + self.sourcetype = LayerSource.TYPE_IMPORTED + + def update(self): + """ + Fetches layer, recipe and machine information from local repository + """ + pass + + +class LayerIndexLayerSource(LayerSource): + class Meta(LayerSource._meta.__class__): + proxy = True + + def __init__(self, *args, **kwargs): + super(LayerIndexLayerSource, self).__init__(args, kwargs) + self.sourcetype = LayerSource.TYPE_LAYERINDEX + + def get_object_view(self, branch, objectype, upid): + return self.apiurl + "../branch/" + branch.name + "/" + objectype + "/?q=" + str(upid) + + def update(self): + """ + Fetches layer, recipe and machine information from remote repository + """ + assert self.apiurl is not None + from django.db import transaction, connection + + import urllib2, urlparse, json + import os + proxy_settings = os.environ.get("http_proxy", None) + + def _get_json_response(apiurl = self.apiurl): + _parsedurl = urlparse.urlparse(apiurl) + path = _parsedurl.path + + try: + res = urllib2.urlopen(apiurl) + except urllib2.URLError as e: + raise Exception("Failed to read %s: %s" % (path, e.reason)) + + return json.loads(res.read()) + + # verify we can get the basic api + try: + apilinks = _get_json_response() + except Exception as e: + import traceback + if proxy_settings is not None: + logger.info("EE: Using proxy %s" % proxy_settings) + logger.warning("EE: could not connect to %s, skipping update: %s\n%s" % (self.apiurl, e, traceback.format_exc(e))) + return + + # update branches; only those that we already have names listed in the + # Releases table + whitelist_branch_names = map(lambda x: x.branch_name, Release.objects.all()) + if len(whitelist_branch_names) == 0: + raise Exception("Failed to make list of branches to fetch") + + logger.debug("Fetching branches") + branches_info = _get_json_response(apilinks['branches'] + + "?filter=name:%s" % "OR".join(whitelist_branch_names)) + for bi in branches_info: + b, created = Branch.objects.get_or_create(layer_source = self, name = bi['name']) + b.up_id = bi['id'] + b.up_date = bi['updated'] + b.name = bi['name'] + b.short_description = bi['short_description'] + b.save() + + # update layers + layers_info = _get_json_response(apilinks['layerItems']) + if not connection.features.autocommits_when_autocommit_is_off: + transaction.set_autocommit(False) + for li in layers_info: + l, created = Layer.objects.get_or_create(layer_source = self, name = li['name']) + l.up_id = li['id'] + l.up_date = li['updated'] + l.vcs_url = li['vcs_url'] + l.vcs_web_url = li['vcs_web_url'] + l.vcs_web_tree_base_url = li['vcs_web_tree_base_url'] + l.vcs_web_file_base_url = li['vcs_web_file_base_url'] + l.summary = li['summary'] + l.description = li['description'] + l.save() + if not connection.features.autocommits_when_autocommit_is_off: + transaction.set_autocommit(True) + + # update layerbranches/layer_versions + logger.debug("Fetching layer information") + layerbranches_info = _get_json_response(apilinks['layerBranches'] + + "?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] )) + ) + + if not connection.features.autocommits_when_autocommit_is_off: + transaction.set_autocommit(False) + for lbi in layerbranches_info: + lv, created = Layer_Version.objects.get_or_create(layer_source = self, + up_id = lbi['id'], + layer=Layer.objects.get(layer_source = self, up_id = lbi['layer']) + ) + + lv.up_date = lbi['updated'] + lv.up_branch = Branch.objects.get(layer_source = self, up_id = lbi['branch']) + lv.branch = lbi['actual_branch'] + lv.commit = lbi['actual_branch'] + lv.dirpath = lbi['vcs_subdir'] + lv.save() + if not connection.features.autocommits_when_autocommit_is_off: + transaction.set_autocommit(True) + + # update layer dependencies + layerdependencies_info = _get_json_response(apilinks['layerDependencies']) + dependlist = {} + if not connection.features.autocommits_when_autocommit_is_off: + transaction.set_autocommit(False) + for ldi in layerdependencies_info: + try: + lv = Layer_Version.objects.get(layer_source = self, up_id = ldi['layerbranch']) + except Layer_Version.DoesNotExist as e: + continue + + if lv not in dependlist: + dependlist[lv] = [] + try: + dependlist[lv].append(Layer_Version.objects.get(layer_source = self, layer__up_id = ldi['dependency'], up_branch = lv.up_branch)) + except Layer_Version.DoesNotExist: + logger.warning("Cannot find layer version (ls:%s), up_id:%s lv:%s" % (self, ldi['dependency'], lv)) + + for lv in dependlist: + LayerVersionDependency.objects.filter(layer_version = lv).delete() + for lvd in dependlist[lv]: + LayerVersionDependency.objects.get_or_create(layer_version = lv, depends_on = lvd) + if not connection.features.autocommits_when_autocommit_is_off: + transaction.set_autocommit(True) + + + # update machines + logger.debug("Fetching machine information") + machines_info = _get_json_response(apilinks['machines'] + + "?filter=layerbranch:%s" % "OR".join(map(lambda x: str(x.up_id), Layer_Version.objects.filter(layer_source = self))) + ) + + if not connection.features.autocommits_when_autocommit_is_off: + transaction.set_autocommit(False) + for mi in machines_info: + 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'])) + mo.up_date = mi['updated'] + mo.name = mi['name'] + mo.description = mi['description'] + mo.save() + + if not connection.features.autocommits_when_autocommit_is_off: + transaction.set_autocommit(True) + + # update recipes; paginate by layer version / layer branch + logger.debug("Fetching target information") + recipes_info = _get_json_response(apilinks['recipes'] + + "?filter=layerbranch:%s" % "OR".join(map(lambda x: str(x.up_id), Layer_Version.objects.filter(layer_source = self))) + ) + if not connection.features.autocommits_when_autocommit_is_off: + transaction.set_autocommit(False) + for ri in recipes_info: + try: + 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'])) + ro.up_date = ri['updated'] + ro.name = ri['pn'] + ro.version = ri['pv'] + ro.summary = ri['summary'] + ro.description = ri['description'] + ro.section = ri['section'] + ro.license = ri['license'] + ro.homepage = ri['homepage'] + ro.bugtracker = ri['bugtracker'] + ro.file_path = ri['filepath'] + "/" + ri['filename'] + if 'inherits' in ri: + ro.is_image = 'image' in ri['inherits'].split() + ro.save() + except IntegrityError as e: + logger.debug("Failed saving recipe, ignoring: %s (%s:%s)" % (e, ro.layer_version, ri['filepath']+"/"+ri['filename'])) + if not connection.features.autocommits_when_autocommit_is_off: + transaction.set_autocommit(True) + +class BitbakeVersion(models.Model): + + name = models.CharField(max_length=32, unique = True) + giturl = GitURLField() + branch = models.CharField(max_length=32) + dirpath = models.CharField(max_length=255) + + def __unicode__(self): + return "%s (Branch: %s)" % (self.name, self.branch) + + +class Release(models.Model): + """ A release is a project template, used to pre-populate Project settings with a configuration set """ + name = models.CharField(max_length=32, unique = True) + description = models.CharField(max_length=255) + bitbake_version = models.ForeignKey(BitbakeVersion) + branch_name = models.CharField(max_length=50, default = "") + helptext = models.TextField(null=True) + + def __unicode__(self): + return "%s (%s)" % (self.name, self.branch_name) + +class ReleaseLayerSourcePriority(models.Model): + """ Each release selects layers from the set up layer sources, ordered by priority """ + release = models.ForeignKey("Release") + layer_source = models.ForeignKey("LayerSource") + priority = models.IntegerField(default = 0) + + def __unicode__(self): + return "%s-%s:%d" % (self.release.name, self.layer_source.name, self.priority) + class Meta: + unique_together = (('release', 'layer_source'),) + + +class ReleaseDefaultLayer(models.Model): + release = models.ForeignKey(Release) + layer_name = models.CharField(max_length=100, default="") + + +# Branch class is synced with layerindex.Branch, branches can only come from remote layer indexes +class Branch(models.Model): + layer_source = models.ForeignKey('LayerSource', null = True, default = True) + up_id = models.IntegerField(null = True, default = None) # id of branch in the source + up_date = models.DateTimeField(null = True, default = None) + + name = models.CharField(max_length=50) + short_description = models.CharField(max_length=50, blank=True) + + class Meta: + verbose_name_plural = "Branches" + unique_together = (('layer_source', 'name'),('layer_source', 'up_id')) + + def __unicode__(self): + return self.name + + +# Layer class synced with layerindex.LayerItem +class Layer(models.Model): + layer_source = models.ForeignKey(LayerSource, null = True, default = None) # from where did we got this layer + up_id = models.IntegerField(null = True, default = None) # id of layer in the remote source + up_date = models.DateTimeField(null = True, default = None) + + name = models.CharField(max_length=100) + layer_index_url = models.URLField() + vcs_url = GitURLField(default = None, null = True) + vcs_web_url = models.URLField(null = True, default = None) + vcs_web_tree_base_url = models.URLField(null = True, default = None) + vcs_web_file_base_url = models.URLField(null = True, default = None) + + summary = models.TextField(help_text='One-line description of the layer', null = True, default = None) + description = models.TextField(null = True, default = None) + + def __unicode__(self): + return "%s / %s " % (self.name, self.layer_source) + + class Meta: + unique_together = (("layer_source", "up_id"), ("layer_source", "name")) + + +# LayerCommit class is synced with layerindex.LayerBranch +class Layer_Version(models.Model): + search_allowed_fields = ["layer__name", "layer__summary", "layer__description", "layer__vcs_url", "dirpath", "up_branch__name", "commit", "branch"] + build = models.ForeignKey(Build, related_name='layer_version_build', default = None, null = True) + layer = models.ForeignKey(Layer, related_name='layer_version_layer') + + layer_source = models.ForeignKey(LayerSource, null = True, default = None) # from where did we get this Layer Version + up_id = models.IntegerField(null = True, default = None) # id of layerbranch in the remote source + up_date = models.DateTimeField(null = True, default = None) + up_branch = models.ForeignKey(Branch, null = True, default = None) + + branch = models.CharField(max_length=80) # LayerBranch.actual_branch + commit = models.CharField(max_length=100) # LayerBranch.vcs_last_rev + dirpath = models.CharField(max_length=255, null = True, default = None) # LayerBranch.vcs_subdir + priority = models.IntegerField(default = 0) # if -1, this is a default layer + + local_path = models.FilePathField(max_length=1024, default = "/") # where this layer was checked-out + + project = models.ForeignKey('Project', null = True, default = None) # Set if this layer is project-specific; always set for imported layers, and project-set branches + + # code lifted, with adaptations, from the layerindex-web application https://git.yoctoproject.org/cgit/cgit.cgi/layerindex-web/ + def _handle_url_path(self, base_url, path): + import re, posixpath + if base_url: + if self.dirpath: + if path: + extra_path = self.dirpath + '/' + path + # Normalise out ../ in path for usage URL + extra_path = posixpath.normpath(extra_path) + # Minor workaround to handle case where subdirectory has been added between branches + # (should probably support usage URL per branch to handle this... sigh...) + if extra_path.startswith('../'): + extra_path = extra_path[3:] + else: + extra_path = self.dirpath + else: + extra_path = path + branchname = self.up_branch.name + url = base_url.replace('%branch%', branchname) + + # If there's a % in the path (e.g. a wildcard bbappend) we need to encode it + if extra_path: + extra_path = extra_path.replace('%', '%25') + + if '%path%' in base_url: + if extra_path: + url = re.sub(r'\[([^\]]*%path%[^\]]*)\]', '\\1', url) + else: + url = re.sub(r'\[([^\]]*%path%[^\]]*)\]', '', url) + return url.replace('%path%', extra_path) + else: + return url + extra_path + return None + + def get_vcs_link_url(self): + if self.layer.vcs_web_url is None: + return None + return self.layer.vcs_web_url + + def get_vcs_file_link_url(self, file_path=""): + if self.layer.vcs_web_file_base_url is None: + return None + return self._handle_url_path(self.layer.vcs_web_file_base_url, file_path) + + def get_vcs_dirpath_link_url(self): + if self.layer.vcs_web_tree_base_url is None: + return None + return self._handle_url_path(self.layer.vcs_web_tree_base_url, '') + + def get_equivalents_wpriority(self, project): + return project.compatible_layerversions(layer_name = self.layer.name) + + def get_vcs_reference(self): + if self.commit is not None and len(self.commit) > 0: + return self.commit + if self.branch is not None and len(self.branch) > 0: + return self.branch + if self.up_branch is not None: + return self.up_branch.name + return ("Cannot determine the vcs_reference for layer version %s" % vars(self)) + + def get_detailspage_url(self, project_id): + return reverse('layerdetails', args=(project_id, self.pk)) + + def __unicode__(self): + 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") + + class Meta: + unique_together = ("layer_source", "up_id") + +class LayerVersionDependency(models.Model): + layer_source = models.ForeignKey(LayerSource, null = True, default = None) # from where did we got this layer + up_id = models.IntegerField(null = True, default = None) # id of layerbranch in the remote source + + layer_version = models.ForeignKey(Layer_Version, related_name="dependencies") + depends_on = models.ForeignKey(Layer_Version, related_name="dependees") + + class Meta: + unique_together = ("layer_source", "up_id") + +class ProjectLayer(models.Model): + project = models.ForeignKey(Project) + layercommit = models.ForeignKey(Layer_Version, null=True) + optional = models.BooleanField(default = True) + + def __unicode__(self): + return "%s, %s" % (self.project.name, self.layercommit) + + class Meta: + unique_together = (("project", "layercommit"),) + +class ProjectVariable(models.Model): + project = models.ForeignKey(Project) + name = models.CharField(max_length=100) + value = models.TextField(blank = True) + +class Variable(models.Model): + search_allowed_fields = ['variable_name', 'variable_value', + 'vhistory__file_name', "description"] + build = models.ForeignKey(Build, related_name='variable_build') + variable_name = models.CharField(max_length=100) + variable_value = models.TextField(blank=True) + changed = models.BooleanField(default=False) + human_readable_name = models.CharField(max_length=200) + description = models.TextField(blank=True) + +class VariableHistory(models.Model): + variable = models.ForeignKey(Variable, related_name='vhistory') + value = models.TextField(blank=True) + file_name = models.FilePathField(max_length=255) + line_number = models.IntegerField(null=True) + operation = models.CharField(max_length=64) + +class HelpText(models.Model): + VARIABLE = 0 + HELPTEXT_AREA = ((VARIABLE, 'variable'), ) + + build = models.ForeignKey(Build, related_name='helptext_build') + area = models.IntegerField(choices=HELPTEXT_AREA) + key = models.CharField(max_length=100) + text = models.TextField() + +class LogMessage(models.Model): + EXCEPTION = -1 # used to signal self-toaster-exceptions + INFO = 0 + WARNING = 1 + ERROR = 2 + + LOG_LEVEL = ( (INFO, "info"), + (WARNING, "warn"), + (ERROR, "error"), + (EXCEPTION, "toaster exception")) + + build = models.ForeignKey(Build) + task = models.ForeignKey(Task, blank = True, null=True) + level = models.IntegerField(choices=LOG_LEVEL, default=INFO) + message=models.CharField(max_length=240) + pathname = models.FilePathField(max_length=255, blank=True) + lineno = models.IntegerField(null=True) + + def __str__(self): + return "%s %s %s" % (self.get_level_display(), self.message, self.build) + +def invalidate_cache(**kwargs): + from django.core.cache import cache + try: + cache.clear() + except Exception as e: + logger.warning("Problem with cache backend: Failed to clear cache: %s" % e) + +django.db.models.signals.post_save.connect(invalidate_cache) +django.db.models.signals.post_delete.connect(invalidate_cache) |