1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216 |
- #
- # BitBake Toaster Implementation
- #
- # Copyright (C) 2016 Intel Corporation
- #
- # SPDX-License-Identifier: GPL-2.0-only
- #
- # Please run flake8 on this file before sending patches
- import os
- import re
- import logging
- import json
- import glob
- from collections import Counter
- from orm.models import Project, ProjectTarget, Build, Layer_Version
- from orm.models import LayerVersionDependency, LayerSource, ProjectLayer
- from orm.models import Recipe, CustomImageRecipe, CustomImagePackage
- from orm.models import Layer, Target, Package, Package_Dependency
- from orm.models import ProjectVariable
- from bldcontrol.models import BuildRequest, BuildEnvironment
- from bldcontrol import bbcontroller
- from django.http import HttpResponse, JsonResponse
- from django.views.generic import View
- from django.urls import reverse
- from django.db.models import Q, F
- from django.db import Error
- from toastergui.templatetags.projecttags import filtered_filesizeformat
- # development/debugging support
- verbose = 2
- def _log(msg):
- if 1 == verbose:
- print(msg)
- elif 2 == verbose:
- f1=open('/tmp/toaster.log', 'a')
- f1.write("|" + msg + "|\n" )
- f1.close()
- logger = logging.getLogger("toaster")
- def error_response(error):
- return JsonResponse({"error": error})
- class XhrBuildRequest(View):
- def get(self, request, *args, **kwargs):
- return HttpResponse()
- @staticmethod
- def cancel_build(br):
- """Cancel a build request"""
- try:
- bbctrl = bbcontroller.BitbakeController(br.environment)
- bbctrl.forceShutDown()
- except:
- # We catch a bunch of exceptions here because
- # this is where the server has not had time to start up
- # and the build request or build is in transit between
- # processes.
- # We can safely just set the build as cancelled
- # already as it never got started
- build = br.build
- build.outcome = Build.CANCELLED
- build.save()
- # We now hand over to the buildinfohelper to update the
- # build state once we've finished cancelling
- br.state = BuildRequest.REQ_CANCELLING
- br.save()
- def post(self, request, *args, **kwargs):
- """
- Build control
- Entry point: /xhr_buildrequest/<project_id>
- Method: POST
- Args:
- id: id of build to change
- buildCancel = build_request_id ...
- buildDelete = id ...
- targets = recipe_name ...
- Returns:
- {"error": "ok"}
- or
- {"error": <error message>}
- """
- project = Project.objects.get(pk=kwargs['pid'])
- if 'buildCancel' in request.POST:
- for i in request.POST['buildCancel'].strip().split(" "):
- try:
- br = BuildRequest.objects.get(project=project, pk=i)
- self.cancel_build(br)
- except BuildRequest.DoesNotExist:
- return error_response('No such build request id %s' % i)
- return error_response('ok')
- if 'buildDelete' in request.POST:
- for i in request.POST['buildDelete'].strip().split(" "):
- try:
- BuildRequest.objects.select_for_update().get(
- project=project,
- pk=i,
- state__lte=BuildRequest.REQ_DELETED).delete()
- except BuildRequest.DoesNotExist:
- pass
- return error_response("ok")
- if 'targets' in request.POST:
- ProjectTarget.objects.filter(project=project).delete()
- s = str(request.POST['targets'])
- for t in re.sub(r'[;%|"]', '', s).split(" "):
- if ":" in t:
- target, task = t.split(":")
- else:
- target = t
- task = ""
- ProjectTarget.objects.create(project=project,
- target=target,
- task=task)
- project.schedule_build()
- return error_response('ok')
- response = HttpResponse()
- response.status_code = 500
- return response
- class XhrProjectUpdate(View):
- def get(self, request, *args, **kwargs):
- return HttpResponse()
- def post(self, request, *args, **kwargs):
- """
- Project Update
- Entry point: /xhr_projectupdate/<project_id>
- Method: POST
- Args:
- pid: pid of project to update
- Returns:
- {"error": "ok"}
- or
- {"error": <error message>}
- """
- project = Project.objects.get(pk=kwargs['pid'])
- logger.debug("ProjectUpdateCallback:project.pk=%d,project.builddir=%s" % (project.pk,project.builddir))
- if 'do_update' in request.POST:
- # Extract any default image recipe
- if 'default_image' in request.POST:
- project.set_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE,str(request.POST['default_image']))
- else:
- project.set_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE,'')
- logger.debug("ProjectUpdateCallback:Chain to the build request")
- # Chain to the build request
- xhrBuildRequest = XhrBuildRequest()
- return xhrBuildRequest.post(request, *args, **kwargs)
- logger.warning("ERROR:XhrProjectUpdate")
- response = HttpResponse()
- response.status_code = 500
- return response
- class XhrSetDefaultImageUrl(View):
- def get(self, request, *args, **kwargs):
- return HttpResponse()
- def post(self, request, *args, **kwargs):
- """
- Project Update
- Entry point: /xhr_setdefaultimage/<project_id>
- Method: POST
- Args:
- pid: pid of project to update default image
- Returns:
- {"error": "ok"}
- or
- {"error": <error message>}
- """
- project = Project.objects.get(pk=kwargs['pid'])
- logger.debug("XhrSetDefaultImageUrl:project.pk=%d" % (project.pk))
- # set any default image recipe
- if 'targets' in request.POST:
- default_target = str(request.POST['targets'])
- project.set_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE,default_target)
- logger.debug("XhrSetDefaultImageUrl,project.pk=%d,project.builddir=%s" % (project.pk,project.builddir))
- return error_response('ok')
- logger.warning("ERROR:XhrSetDefaultImageUrl")
- response = HttpResponse()
- response.status_code = 500
- return response
- #
- # Layer Management
- #
- # Rules for 'local_source_dir' layers
- # * Layers must have a unique name in the Layers table
- # * A 'local_source_dir' layer is supposed to be shared
- # by all projects that use it, so that it can have the
- # same logical name
- # * Each project that uses a layer will have its own
- # LayerVersion and Project Layer for it
- # * During the Project delete process, when the last
- # LayerVersion for a 'local_source_dir' layer is deleted
- # then the Layer record is deleted to remove orphans
- #
- def scan_layer_content(layer,layer_version):
- # if this is a local layer directory, we can immediately scan its content
- if os.path.isdir(layer.local_source_dir):
- try:
- # recipes-*/*/*.bb
- recipes_list = glob.glob(os.path.join(layer.local_source_dir, 'recipes-*/*/*.bb'))
- for recipe in recipes_list:
- for recipe in recipes_list.split('\n'):
- recipe_path = recipe[recipe.rfind('recipes-'):]
- recipe_name = recipe[recipe.rfind('/')+1:].replace('.bb','')
- recipe_ver = recipe_name.rfind('_')
- if recipe_ver > 0:
- recipe_name = recipe_name[0:recipe_ver]
- if recipe_name:
- ro, created = Recipe.objects.get_or_create(
- layer_version=layer_version,
- name=recipe_name
- )
- if created:
- ro.file_path = recipe_path
- ro.summary = 'Recipe %s from layer %s' % (recipe_name,layer.name)
- ro.description = ro.summary
- ro.save()
- except Exception as e:
- logger.warning("ERROR:scan_layer_content: %s" % e)
- else:
- logger.warning("ERROR: wrong path given")
- raise KeyError("local_source_dir")
- class XhrLayer(View):
- """ Delete, Get, Add and Update Layer information
- Methods: GET POST DELETE PUT
- """
- def get(self, request, *args, **kwargs):
- """
- Get layer information
- Method: GET
- Entry point: /xhr_layer/<project id>/<layerversion_id>
- """
- try:
- layer_version = Layer_Version.objects.get(
- pk=kwargs['layerversion_id'])
- project = Project.objects.get(pk=kwargs['pid'])
- project_layers = ProjectLayer.objects.filter(
- project=project).values_list("layercommit_id",
- flat=True)
- ret = {
- 'error': 'ok',
- 'id': layer_version.pk,
- 'name': layer_version.layer.name,
- 'layerdetailurl':
- layer_version.get_detailspage_url(project.pk),
- 'vcs_ref': layer_version.get_vcs_reference(),
- 'vcs_url': layer_version.layer.vcs_url,
- 'local_source_dir': layer_version.layer.local_source_dir,
- 'layerdeps': {
- "list": [
- {
- "id": dep.id,
- "name": dep.layer.name,
- "layerdetailurl":
- dep.get_detailspage_url(project.pk),
- "vcs_url": dep.layer.vcs_url,
- "vcs_reference": dep.get_vcs_reference()
- }
- for dep in layer_version.get_alldeps(project.id)]
- },
- 'projectlayers': list(project_layers)
- }
- return JsonResponse(ret)
- except Layer_Version.DoesNotExist:
- error_response("No such layer")
- def post(self, request, *args, **kwargs):
- """
- Update a layer
- Method: POST
- Entry point: /xhr_layer/<layerversion_id>
- Args:
- vcs_url, dirpath, commit, up_branch, summary, description,
- local_source_dir
- add_dep = append a layerversion_id as a dependency
- rm_dep = remove a layerversion_id as a depedency
- Returns:
- {"error": "ok"}
- or
- {"error": <error message>}
- """
- try:
- # We currently only allow Imported layers to be edited
- layer_version = Layer_Version.objects.get(
- id=kwargs['layerversion_id'],
- project=kwargs['pid'],
- layer_source=LayerSource.TYPE_IMPORTED)
- except Layer_Version.DoesNotExist:
- return error_response("Cannot find imported layer to update")
- if "vcs_url" in request.POST:
- layer_version.layer.vcs_url = request.POST["vcs_url"]
- if "dirpath" in request.POST:
- layer_version.dirpath = request.POST["dirpath"]
- if "commit" in request.POST:
- layer_version.commit = request.POST["commit"]
- layer_version.branch = request.POST["commit"]
- if "summary" in request.POST:
- layer_version.layer.summary = request.POST["summary"]
- if "description" in request.POST:
- layer_version.layer.description = request.POST["description"]
- if "local_source_dir" in request.POST:
- layer_version.layer.local_source_dir = \
- request.POST["local_source_dir"]
- if "add_dep" in request.POST:
- lvd = LayerVersionDependency(
- layer_version=layer_version,
- depends_on_id=request.POST["add_dep"])
- lvd.save()
- if "rm_dep" in request.POST:
- rm_dep = LayerVersionDependency.objects.get(
- layer_version=layer_version,
- depends_on_id=request.POST["rm_dep"])
- rm_dep.delete()
- try:
- layer_version.layer.save()
- layer_version.save()
- except Exception as e:
- return error_response("Could not update layer version entry: %s"
- % e)
- return error_response("ok")
- def put(self, request, *args, **kwargs):
- """ Add a new layer
- Method: PUT
- Entry point: /xhr_layer/<project id>/
- Args:
- project_id, name,
- [vcs_url, dir_path, git_ref], [local_source_dir], [layer_deps
- (csv)]
- """
- try:
- project = Project.objects.get(pk=kwargs['pid'])
- layer_data = json.loads(request.body.decode('utf-8'))
- # We require a unique layer name as otherwise the lists of layers
- # becomes very confusing
- existing_layers = \
- project.get_all_compatible_layer_versions().values_list(
- "layer__name",
- flat=True)
- add_to_project = False
- layer_deps_added = []
- if 'add_to_project' in layer_data:
- add_to_project = True
- if layer_data['name'] in existing_layers:
- return JsonResponse({"error": "layer-name-exists"})
- if ('local_source_dir' in layer_data):
- # Local layer can be shared across projects. They have no 'release'
- # and are not included in get_all_compatible_layer_versions() above
- layer,created = Layer.objects.get_or_create(name=layer_data['name'])
- _log("Local Layer created=%s" % created)
- else:
- layer = Layer.objects.create(name=layer_data['name'])
- layer_version = Layer_Version.objects.create(
- layer=layer,
- project=project,
- layer_source=LayerSource.TYPE_IMPORTED)
- # Local layer
- if ('local_source_dir' in layer_data): ### and layer.local_source_dir:
- layer.local_source_dir = layer_data['local_source_dir']
- # git layer
- elif 'vcs_url' in layer_data:
- layer.vcs_url = layer_data['vcs_url']
- layer_version.dirpath = layer_data['dir_path']
- layer_version.commit = layer_data['git_ref']
- layer_version.branch = layer_data['git_ref']
- layer.save()
- layer_version.save()
- if add_to_project:
- ProjectLayer.objects.get_or_create(
- layercommit=layer_version, project=project)
- # Add the layer dependencies
- if 'layer_deps' in layer_data:
- for layer_dep_id in layer_data['layer_deps'].split(","):
- layer_dep = Layer_Version.objects.get(pk=layer_dep_id)
- LayerVersionDependency.objects.get_or_create(
- layer_version=layer_version, depends_on=layer_dep)
- # Add layer deps to the project if specified
- if add_to_project:
- created, pl = ProjectLayer.objects.get_or_create(
- layercommit=layer_dep, project=project)
- layer_deps_added.append(
- {'name': layer_dep.layer.name,
- 'layerdetailurl':
- layer_dep.get_detailspage_url(project.pk)})
- # Only scan_layer_content if layer is local
- if layer_data.get('local_source_dir', None):
- # Scan the layer's content and update components
- scan_layer_content(layer,layer_version)
- except Layer_Version.DoesNotExist:
- return error_response("layer-dep-not-found")
- except Project.DoesNotExist:
- return error_response("project-not-found")
- except KeyError as e:
- _log("KeyError: %s" % e)
- return error_response(f"incorrect-parameters")
- return JsonResponse({'error': "ok",
- 'imported_layer': {
- 'name': layer.name,
- 'layerdetailurl':
- layer_version.get_detailspage_url()},
- 'deps_added': layer_deps_added})
- def delete(self, request, *args, **kwargs):
- """ Delete an imported layer
- Method: DELETE
- Entry point: /xhr_layer/<projed id>/<layerversion_id>
- """
- try:
- # We currently only allow Imported layers to be deleted
- layer_version = Layer_Version.objects.get(
- id=kwargs['layerversion_id'],
- project=kwargs['pid'],
- layer_source=LayerSource.TYPE_IMPORTED)
- except Layer_Version.DoesNotExist:
- return error_response("Cannot find imported layer to delete")
- try:
- ProjectLayer.objects.get(project=kwargs['pid'],
- layercommit=layer_version).delete()
- except ProjectLayer.DoesNotExist:
- pass
- layer_version.layer.delete()
- layer_version.delete()
- return JsonResponse({
- "error": "ok",
- "gotoUrl": reverse('projectlayers', args=(kwargs['pid'],))
- })
- class XhrCustomRecipe(View):
- """ Create a custom image recipe """
- def post(self, request, *args, **kwargs):
- """
- Custom image recipe REST API
- Entry point: /xhr_customrecipe/
- Method: POST
- Args:
- name: name of custom recipe to create
- project: target project id of orm.models.Project
- base: base recipe id of orm.models.Recipe
- Returns:
- {"error": "ok",
- "url": <url of the created recipe>}
- or
- {"error": <error message>}
- """
- # check if request has all required parameters
- for param in ('name', 'project', 'base'):
- if param not in request.POST:
- return error_response("Missing parameter '%s'" % param)
- # get project and baserecipe objects
- params = {}
- for name, model in [("project", Project),
- ("base", Recipe)]:
- value = request.POST[name]
- try:
- params[name] = model.objects.get(id=value)
- except model.DoesNotExist:
- return error_response("Invalid %s id %s" % (name, value))
- # create custom recipe
- try:
- # Only allowed chars in name are a-z, 0-9 and -
- if re.search(r'[^a-z|0-9|-]', request.POST["name"]):
- return error_response("invalid-name")
- custom_images = CustomImageRecipe.objects.all()
- # Are there any recipes with this name already in our project?
- existing_image_recipes_in_project = custom_images.filter(
- name=request.POST["name"], project=params["project"])
- if existing_image_recipes_in_project.count() > 0:
- return error_response("image-already-exists")
- # Are there any recipes with this name which aren't custom
- # image recipes?
- custom_image_ids = custom_images.values_list('id', flat=True)
- existing_non_image_recipes = Recipe.objects.filter(
- Q(name=request.POST["name"]) & ~Q(pk__in=custom_image_ids)
- )
- if existing_non_image_recipes.count() > 0:
- return error_response("recipe-already-exists")
- # create layer 'Custom layer' and verion if needed
- layer, l_created = Layer.objects.get_or_create(
- name=CustomImageRecipe.LAYER_NAME,
- summary="Layer for custom recipes")
- if l_created:
- layer.local_source_dir = "toaster_created_layer"
- layer.save()
- # Check if we have a layer version already
- # We don't use get_or_create here because the dirpath will change
- # and is a required field
- lver = Layer_Version.objects.filter(Q(project=params['project']) &
- Q(layer=layer) &
- Q(build=None)).last()
- if lver is None:
- lver, lv_created = Layer_Version.objects.get_or_create(
- project=params['project'],
- layer=layer,
- layer_source=LayerSource.TYPE_LOCAL,
- dirpath="toaster_created_layer")
- # Add a dependency on our layer to the base recipe's layer
- LayerVersionDependency.objects.get_or_create(
- layer_version=lver,
- depends_on=params["base"].layer_version)
- # Add it to our current project if needed
- ProjectLayer.objects.get_or_create(project=params['project'],
- layercommit=lver,
- optional=False)
- # Create the actual recipe
- recipe, r_created = CustomImageRecipe.objects.get_or_create(
- name=request.POST["name"],
- base_recipe=params["base"],
- project=params["project"],
- layer_version=lver,
- is_image=True)
- # If we created the object then setup these fields. They may get
- # overwritten later on and cause the get_or_create to create a
- # duplicate if they've changed.
- if r_created:
- recipe.file_path = request.POST["name"]
- recipe.license = "MIT"
- recipe.version = "0.1"
- recipe.save()
- except Error as err:
- return error_response("Can't create custom recipe: %s" % err)
- # Find the package list from the last build of this recipe/target
- target = Target.objects.filter(Q(build__outcome=Build.SUCCEEDED) &
- Q(build__project=params['project']) &
- (Q(target=params['base'].name) |
- Q(target=recipe.name))).last()
- if target:
- # Copy in every package
- # We don't want these packages to be linked to anything because
- # that underlying data may change e.g. delete a build
- for tpackage in target.target_installed_package_set.all():
- try:
- built_package = tpackage.package
- # The package had no recipe information so is a ghost
- # package skip it
- if built_package.recipe is None:
- continue
- config_package = CustomImagePackage.objects.get(
- name=built_package.name)
- recipe.includes_set.add(config_package)
- except Exception as e:
- logger.warning("Error adding package %s %s" %
- (tpackage.package.name, e))
- pass
- # pre-create layer directory structure, so that other builds
- # are not blocked by this new recipe dependecy
- # NOTE: this is parallel code to 'localhostbecontroller.py'
- be = BuildEnvironment.objects.all()[0]
- layerpath = os.path.join(be.builddir,
- CustomImageRecipe.LAYER_NAME)
- for name in ("conf", "recipes"):
- path = os.path.join(layerpath, name)
- if not os.path.isdir(path):
- os.makedirs(path)
- # pre-create layer.conf
- config = os.path.join(layerpath, "conf", "layer.conf")
- if not os.path.isfile(config):
- with open(config, "w") as conf:
- conf.write('BBPATH .= ":${LAYERDIR}"\nBBFILES += "${LAYERDIR}/recipes/*.bb"\n')
- # pre-create new image's recipe file
- recipe_path = os.path.join(layerpath, "recipes", "%s.bb" %
- recipe.name)
- with open(recipe_path, "w") as recipef:
- content = recipe.generate_recipe_file_contents()
- if not content:
- # Delete this incomplete image recipe object
- recipe.delete()
- return error_response("recipe-parent-not-exist")
- else:
- recipef.write(recipe.generate_recipe_file_contents())
- return JsonResponse(
- {"error": "ok",
- "packages": recipe.get_all_packages().count(),
- "url": reverse('customrecipe', args=(params['project'].pk,
- recipe.id))})
- class XhrCustomRecipeId(View):
- """
- Set of ReST API processors working with recipe id.
- Entry point: /xhr_customrecipe/<recipe_id>
- Methods:
- GET - Get details of custom image recipe
- DELETE - Delete custom image recipe
- Returns:
- GET:
- {"error": "ok",
- "info": dictionary of field name -> value pairs
- of the CustomImageRecipe model}
- DELETE:
- {"error": "ok"}
- or
- {"error": <error message>}
- """
- @staticmethod
- def _get_ci_recipe(recipe_id):
- """ Get Custom Image recipe or return an error response"""
- try:
- custom_recipe = \
- CustomImageRecipe.objects.get(pk=recipe_id)
- return custom_recipe, None
- except CustomImageRecipe.DoesNotExist:
- return None, error_response("Custom recipe with id=%s "
- "not found" % recipe_id)
- def get(self, request, *args, **kwargs):
- custom_recipe, error = self._get_ci_recipe(kwargs['recipe_id'])
- if error:
- return error
- if request.method == 'GET':
- info = {"id": custom_recipe.id,
- "name": custom_recipe.name,
- "base_recipe_id": custom_recipe.base_recipe.id,
- "project_id": custom_recipe.project.id}
- return JsonResponse({"error": "ok", "info": info})
- def delete(self, request, *args, **kwargs):
- custom_recipe, error = self._get_ci_recipe(kwargs['recipe_id'])
- if error:
- return error
- project = custom_recipe.project
- custom_recipe.delete()
- return JsonResponse({"error": "ok",
- "gotoUrl": reverse("projectcustomimages",
- args=(project.pk,))})
- class XhrCustomRecipePackages(View):
- """
- ReST API to add/remove packages to/from custom recipe.
- Entry point: /xhr_customrecipe/<recipe_id>/packages/<package_id>
- Methods:
- PUT - Add package to the recipe
- DELETE - Delete package from the recipe
- GET - Get package information
- Returns:
- {"error": "ok"}
- or
- {"error": <error message>}
- """
- @staticmethod
- def _get_package(package_id):
- try:
- package = CustomImagePackage.objects.get(pk=package_id)
- return package, None
- except Package.DoesNotExist:
- return None, error_response("Package with id=%s "
- "not found" % package_id)
- def _traverse_dependents(self, next_package_id,
- rev_deps, all_current_packages, tree_level=0):
- """
- Recurse through reverse dependency tree for next_package_id.
- Limit the reverse dependency search to packages not already scanned,
- that is, not already in rev_deps.
- Limit the scan to a depth (tree_level) not exceeding the count of
- all packages in the custom image, and if that depth is exceeded
- return False, pop out of the recursion, and write a warning
- to the log, but this is unlikely, suggesting a dependency loop
- not caught by bitbake.
- On return, the input/output arg rev_deps is appended with queryset
- dictionary elements, annotated for use in the customimage template.
- The list has unsorted, but unique elements.
- """
- max_dependency_tree_depth = all_current_packages.count()
- if tree_level >= max_dependency_tree_depth:
- logger.warning(
- "The number of reverse dependencies "
- "for this package exceeds " + max_dependency_tree_depth +
- " and the remaining reverse dependencies will not be removed")
- return True
- package = CustomImagePackage.objects.get(id=next_package_id)
- dependents = \
- package.package_dependencies_target.annotate(
- name=F('package__name'),
- pk=F('package__pk'),
- size=F('package__size'),
- ).values("name", "pk", "size").exclude(
- ~Q(pk__in=all_current_packages)
- )
- for pkg in dependents:
- if pkg in rev_deps:
- # already seen, skip dependent search
- continue
- rev_deps.append(pkg)
- if (self._traverse_dependents(pkg["pk"], rev_deps,
- all_current_packages,
- tree_level+1)):
- return True
- return False
- def _get_all_dependents(self, package_id, all_current_packages):
- """
- Returns sorted list of recursive reverse dependencies for package_id,
- as a list of dictionary items, by recursing through dependency
- relationships.
- """
- rev_deps = []
- self._traverse_dependents(package_id, rev_deps, all_current_packages)
- rev_deps = sorted(rev_deps, key=lambda x: x["name"])
- return rev_deps
- def get(self, request, *args, **kwargs):
- recipe, error = XhrCustomRecipeId._get_ci_recipe(
- kwargs['recipe_id'])
- if error:
- return error
- # If no package_id then list all the current packages
- if not kwargs['package_id']:
- total_size = 0
- packages = recipe.get_all_packages().values("id",
- "name",
- "version",
- "size")
- for package in packages:
- package['size_formatted'] = \
- filtered_filesizeformat(package['size'])
- total_size += package['size']
- return JsonResponse({"error": "ok",
- "packages": list(packages),
- "total": len(packages),
- "total_size": total_size,
- "total_size_formatted":
- filtered_filesizeformat(total_size)})
- else:
- package, error = XhrCustomRecipePackages._get_package(
- kwargs['package_id'])
- if error:
- return error
- all_current_packages = recipe.get_all_packages()
- # Dependencies for package which aren't satisfied by the
- # current packages in the custom image recipe
- deps = package.package_dependencies_source.for_target_or_none(
- recipe.name)['packages'].annotate(
- name=F('depends_on__name'),
- pk=F('depends_on__pk'),
- size=F('depends_on__size'),
- ).values("name", "pk", "size").filter(
- # There are two depends types we don't know why
- (Q(dep_type=Package_Dependency.TYPE_TRDEPENDS) |
- Q(dep_type=Package_Dependency.TYPE_RDEPENDS)) &
- ~Q(pk__in=all_current_packages)
- )
- # Reverse dependencies which are needed by packages that are
- # in the image. Recursive search providing all dependents,
- # not just immediate dependents.
- reverse_deps = self._get_all_dependents(kwargs['package_id'],
- all_current_packages)
- total_size_deps = 0
- total_size_reverse_deps = 0
- for dep in deps:
- dep['size_formatted'] = \
- filtered_filesizeformat(dep['size'])
- total_size_deps += dep['size']
- for dep in reverse_deps:
- dep['size_formatted'] = \
- filtered_filesizeformat(dep['size'])
- total_size_reverse_deps += dep['size']
- return JsonResponse(
- {"error": "ok",
- "id": package.pk,
- "name": package.name,
- "version": package.version,
- "unsatisfied_dependencies": list(deps),
- "unsatisfied_dependencies_size": total_size_deps,
- "unsatisfied_dependencies_size_formatted":
- filtered_filesizeformat(total_size_deps),
- "reverse_dependencies": list(reverse_deps),
- "reverse_dependencies_size": total_size_reverse_deps,
- "reverse_dependencies_size_formatted":
- filtered_filesizeformat(total_size_reverse_deps)})
- def put(self, request, *args, **kwargs):
- recipe, error = XhrCustomRecipeId._get_ci_recipe(kwargs['recipe_id'])
- package, error = self._get_package(kwargs['package_id'])
- if error:
- return error
- included_packages = recipe.includes_set.values_list('pk',
- flat=True)
- # If we're adding back a package which used to be included in this
- # image all we need to do is remove it from the excludes
- if package.pk in included_packages:
- try:
- recipe.excludes_set.remove(package)
- return {"error": "ok"}
- except Package.DoesNotExist:
- return error_response("Package %s not found in excludes"
- " but was in included list" %
- package.name)
- else:
- recipe.appends_set.add(package)
- # Make sure that package is not in the excludes set
- try:
- recipe.excludes_set.remove(package)
- except:
- pass
- # Add the dependencies we think will be added to the recipe
- # as a result of appending this package.
- # TODO this should recurse down the entire deps tree
- for dep in package.package_dependencies_source.all_depends():
- try:
- cust_package = CustomImagePackage.objects.get(
- name=dep.depends_on.name)
- recipe.includes_set.add(cust_package)
- try:
- # When adding the pre-requisite package, make
- # sure it's not in the excluded list from a
- # prior removal.
- recipe.excludes_set.remove(cust_package)
- except package.DoesNotExist:
- # Don't care if the package had never been excluded
- pass
- except:
- logger.warning("Could not add package's suggested"
- "dependencies to the list")
- return JsonResponse({"error": "ok"})
- def delete(self, request, *args, **kwargs):
- recipe, error = XhrCustomRecipeId._get_ci_recipe(kwargs['recipe_id'])
- package, error = self._get_package(kwargs['package_id'])
- if error:
- return error
- try:
- included_packages = recipe.includes_set.values_list('pk',
- flat=True)
- # If we're deleting a package which is included we need to
- # Add it to the excludes list.
- if package.pk in included_packages:
- recipe.excludes_set.add(package)
- else:
- recipe.appends_set.remove(package)
- # remove dependencies as well
- all_current_packages = recipe.get_all_packages()
- reverse_deps_dictlist = self._get_all_dependents(
- package.pk,
- all_current_packages)
- ids = [entry['pk'] for entry in reverse_deps_dictlist]
- reverse_deps = CustomImagePackage.objects.filter(id__in=ids)
- for r in reverse_deps:
- try:
- if r.id in included_packages:
- recipe.excludes_set.add(r)
- else:
- recipe.appends_set.remove(r)
- except:
- pass
- return JsonResponse({"error": "ok"})
- except CustomImageRecipe.DoesNotExist:
- return error_response("Tried to remove package that wasn't"
- " present")
- class XhrProject(View):
- """ Create, delete or edit a project
- Entry point: /xhr_project/<project_id>
- """
- def post(self, request, *args, **kwargs):
- """
- Edit project control
- Args:
- layerAdd = layer_version_id layer_version_id ...
- layerDel = layer_version_id layer_version_id ...
- projectName = new_project_name
- machineName = new_machine_name
- Returns:
- {"error": "ok"}
- or
- {"error": <error message>}
- """
- try:
- prj = Project.objects.get(pk=kwargs['project_id'])
- except Project.DoesNotExist:
- return error_response("No such project")
- # Add layers
- if 'layerAdd' in request.POST and len(request.POST['layerAdd']) > 0:
- for layer_version_id in request.POST['layerAdd'].split(','):
- try:
- lv = Layer_Version.objects.get(pk=int(layer_version_id))
- ProjectLayer.objects.get_or_create(project=prj,
- layercommit=lv)
- except Layer_Version.DoesNotExist:
- return error_response("Layer version %s asked to add "
- "doesn't exist" % layer_version_id)
- # Remove layers
- if 'layerDel' in request.POST and len(request.POST['layerDel']) > 0:
- layer_version_ids = request.POST['layerDel'].split(',')
- ProjectLayer.objects.filter(
- project=prj,
- layercommit_id__in=layer_version_ids).delete()
- # Project name change
- if 'projectName' in request.POST:
- prj.name = request.POST['projectName']
- prj.save()
- # Machine name change
- if 'machineName' in request.POST:
- machinevar = prj.projectvariable_set.get(name="MACHINE")
- machinevar.value = request.POST['machineName']
- machinevar.save()
- # Distro name change
- if 'distroName' in request.POST:
- distrovar = prj.projectvariable_set.get(name="DISTRO")
- distrovar.value = request.POST['distroName']
- distrovar.save()
- return JsonResponse({"error": "ok"})
- def get(self, request, *args, **kwargs):
- """
- Returns:
- json object representing the current project
- or:
- {"error": <error message>}
- """
- try:
- project = Project.objects.get(pk=kwargs['project_id'])
- except Project.DoesNotExist:
- return error_response("Project %s does not exist" %
- kwargs['project_id'])
- # Create the frequently built targets list
- freqtargets = Counter(Target.objects.filter(
- Q(build__project=project),
- ~Q(build__outcome=Build.IN_PROGRESS)
- ).order_by("target").values_list("target", flat=True))
- freqtargets = freqtargets.most_common(5)
- # We now have the targets in order of frequency but if there are two
- # with the same frequency then we need to make sure those are in
- # alphabetical order without losing the frequency ordering
- tmp = []
- switch = None
- for i, freqtartget in enumerate(freqtargets):
- target, count = freqtartget
- try:
- target_next, count_next = freqtargets[i+1]
- if count == count_next and target > target_next:
- switch = target
- continue
- except IndexError:
- pass
- tmp.append(target)
- if switch:
- tmp.append(switch)
- switch = None
- freqtargets = tmp
- layers = []
- for layer in project.projectlayer_set.all():
- layers.append({
- "id": layer.layercommit.pk,
- "name": layer.layercommit.layer.name,
- "vcs_url": layer.layercommit.layer.vcs_url,
- "local_source_dir": layer.layercommit.layer.local_source_dir,
- "vcs_reference": layer.layercommit.get_vcs_reference(),
- "url": layer.layercommit.layer.layer_index_url,
- "layerdetailurl": layer.layercommit.get_detailspage_url(
- project.pk),
- "xhrLayerUrl": reverse("xhr_layer",
- args=(project.pk,
- layer.layercommit.pk)),
- "layersource": layer.layercommit.layer_source
- })
- data = {
- "name": project.name,
- "layers": layers,
- "freqtargets": freqtargets,
- }
- if project.release is not None:
- data['release'] = {
- "id": project.release.pk,
- "name": project.release.name,
- "description": project.release.description
- }
- try:
- data["machine"] = {"name":
- project.projectvariable_set.get(
- name="MACHINE").value}
- except ProjectVariable.DoesNotExist:
- data["machine"] = None
- try:
- data["distro"] = {"name":
- project.projectvariable_set.get(
- name="DISTRO").value}
- except ProjectVariable.DoesNotExist:
- data["distro"] = None
- data['error'] = "ok"
- return JsonResponse(data)
- def put(self, request, *args, **kwargs):
- # TODO create new project api
- return HttpResponse()
- def delete(self, request, *args, **kwargs):
- """Delete a project. Cancels any builds in progress"""
- try:
- project = Project.objects.get(pk=kwargs['project_id'])
- # Cancel any builds in progress
- for br in BuildRequest.objects.filter(
- project=project,
- state=BuildRequest.REQ_INPROGRESS):
- XhrBuildRequest.cancel_build(br)
- # gather potential orphaned local layers attached to this project
- project_local_layer_list = []
- for pl in ProjectLayer.objects.filter(project=project):
- if pl.layercommit.layer_source == LayerSource.TYPE_IMPORTED:
- project_local_layer_list.append(pl.layercommit.layer)
- # deep delete the project and its dependencies
- project.delete()
- # delete any local layers now orphaned
- _log("LAYER_ORPHAN_CHECK:Check for orphaned layers")
- for layer in project_local_layer_list:
- layer_refs = Layer_Version.objects.filter(layer=layer)
- _log("LAYER_ORPHAN_CHECK:Ref Count for '%s' = %d" % (layer.name,len(layer_refs)))
- if 0 == len(layer_refs):
- _log("LAYER_ORPHAN_CHECK:DELETE orpahned '%s'" % (layer.name))
- Layer.objects.filter(pk=layer.id).delete()
- except Project.DoesNotExist:
- return error_response("Project %s does not exist" %
- kwargs['project_id'])
- return JsonResponse({
- "error": "ok",
- "gotoUrl": reverse("all-projects", args=[])
- })
- class XhrBuild(View):
- """ Delete a build object
- Entry point: /xhr_build/<build_id>
- """
- def delete(self, request, *args, **kwargs):
- """
- Delete build data
- Args:
- build_id = build_id
- Returns:
- {"error": "ok"}
- or
- {"error": <error message>}
- """
- try:
- build = Build.objects.get(pk=kwargs['build_id'])
- project = build.project
- build.delete()
- except Build.DoesNotExist:
- return error_response("Build %s does not exist" %
- kwargs['build_id'])
- return JsonResponse({
- "error": "ok",
- "gotoUrl": reverse("projectbuilds", args=(project.pk,))
- })
|