blob: 564d595a1cdd4ea996db947fe9dc05fe12ec7cb3 [file] [log] [blame]
rjw1f884582022-01-06 17:20:42 +08001#
2# BitBake Toaster Implementation
3#
4# Copyright (C) 2016 Intel Corporation
5#
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License version 2 as
8# published by the Free Software Foundation.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License along
16# with this program; if not, write to the Free Software Foundation, Inc.,
17# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
19# Please run flake8 on this file before sending patches
20
21import os
22import re
23import logging
24import json
25import subprocess
26from collections import Counter
27from shutil import copyfile
28
29from orm.models import Project, ProjectTarget, Build, Layer_Version
30from orm.models import LayerVersionDependency, LayerSource, ProjectLayer
31from orm.models import Recipe, CustomImageRecipe, CustomImagePackage
32from orm.models import Layer, Target, Package, Package_Dependency
33from orm.models import ProjectVariable
34from bldcontrol.models import BuildRequest, BuildEnvironment
35from bldcontrol import bbcontroller
36
37from django.http import HttpResponse, JsonResponse
38from django.views.generic import View
39from django.core.urlresolvers import reverse
40from django.db.models import Q, F
41from django.db import Error
42from toastergui.templatetags.projecttags import filtered_filesizeformat
43from django.utils import timezone
44import pytz
45
46# development/debugging support
47verbose = 2
48def _log(msg):
49 if 1 == verbose:
50 print(msg)
51 elif 2 == verbose:
52 f1=open('/tmp/toaster.log', 'a')
53 f1.write("|" + msg + "|\n" )
54 f1.close()
55
56logger = logging.getLogger("toaster")
57
58
59def error_response(error):
60 return JsonResponse({"error": error})
61
62
63class XhrBuildRequest(View):
64
65 def get(self, request, *args, **kwargs):
66 return HttpResponse()
67
68 @staticmethod
69 def cancel_build(br):
70 """Cancel a build request"""
71 try:
72 bbctrl = bbcontroller.BitbakeController(br.environment)
73 bbctrl.forceShutDown()
74 except:
75 # We catch a bunch of exceptions here because
76 # this is where the server has not had time to start up
77 # and the build request or build is in transit between
78 # processes.
79 # We can safely just set the build as cancelled
80 # already as it never got started
81 build = br.build
82 build.outcome = Build.CANCELLED
83 build.save()
84
85 # We now hand over to the buildinfohelper to update the
86 # build state once we've finished cancelling
87 br.state = BuildRequest.REQ_CANCELLING
88 br.save()
89
90 def post(self, request, *args, **kwargs):
91 """
92 Build control
93
94 Entry point: /xhr_buildrequest/<project_id>
95 Method: POST
96
97 Args:
98 id: id of build to change
99 buildCancel = build_request_id ...
100 buildDelete = id ...
101 targets = recipe_name ...
102
103 Returns:
104 {"error": "ok"}
105 or
106 {"error": <error message>}
107 """
108
109 project = Project.objects.get(pk=kwargs['pid'])
110
111 if 'buildCancel' in request.POST:
112 for i in request.POST['buildCancel'].strip().split(" "):
113 try:
114 br = BuildRequest.objects.get(project=project, pk=i)
115 self.cancel_build(br)
116 except BuildRequest.DoesNotExist:
117 return error_response('No such build request id %s' % i)
118
119 return error_response('ok')
120
121 if 'buildDelete' in request.POST:
122 for i in request.POST['buildDelete'].strip().split(" "):
123 try:
124 BuildRequest.objects.select_for_update().get(
125 project=project,
126 pk=i,
127 state__lte=BuildRequest.REQ_DELETED).delete()
128
129 except BuildRequest.DoesNotExist:
130 pass
131 return error_response("ok")
132
133 if 'targets' in request.POST:
134 ProjectTarget.objects.filter(project=project).delete()
135 s = str(request.POST['targets'])
136 for t in re.sub(r'[;%|"]', '', s).split(" "):
137 if ":" in t:
138 target, task = t.split(":")
139 else:
140 target = t
141 task = ""
142 ProjectTarget.objects.create(project=project,
143 target=target,
144 task=task)
145 project.schedule_build()
146
147 return error_response('ok')
148
149 response = HttpResponse()
150 response.status_code = 500
151 return response
152
153
154class XhrProjectUpdate(View):
155
156 def get(self, request, *args, **kwargs):
157 return HttpResponse()
158
159 def post(self, request, *args, **kwargs):
160 """
161 Project Update
162
163 Entry point: /xhr_projectupdate/<project_id>
164 Method: POST
165
166 Args:
167 pid: pid of project to update
168
169 Returns:
170 {"error": "ok"}
171 or
172 {"error": <error message>}
173 """
174
175 project = Project.objects.get(pk=kwargs['pid'])
176 logger.debug("ProjectUpdateCallback:project.pk=%d,project.builddir=%s" % (project.pk,project.builddir))
177
178 if 'do_update' in request.POST:
179
180 # Extract any default image recipe
181 if 'default_image' in request.POST:
182 project.set_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE,str(request.POST['default_image']))
183 else:
184 project.set_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE,'')
185
186 logger.debug("ProjectUpdateCallback:Chain to the build request")
187
188 # Chain to the build request
189 xhrBuildRequest = XhrBuildRequest()
190 return xhrBuildRequest.post(request, *args, **kwargs)
191
192 logger.warning("ERROR:XhrProjectUpdate")
193 response = HttpResponse()
194 response.status_code = 500
195 return response
196
197class XhrSetDefaultImageUrl(View):
198
199 def get(self, request, *args, **kwargs):
200 return HttpResponse()
201
202 def post(self, request, *args, **kwargs):
203 """
204 Project Update
205
206 Entry point: /xhr_setdefaultimage/<project_id>
207 Method: POST
208
209 Args:
210 pid: pid of project to update default image
211
212 Returns:
213 {"error": "ok"}
214 or
215 {"error": <error message>}
216 """
217
218 project = Project.objects.get(pk=kwargs['pid'])
219 logger.debug("XhrSetDefaultImageUrl:project.pk=%d" % (project.pk))
220
221 # set any default image recipe
222 if 'targets' in request.POST:
223 default_target = str(request.POST['targets'])
224 project.set_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE,default_target)
225 logger.debug("XhrSetDefaultImageUrl,project.pk=%d,project.builddir=%s" % (project.pk,project.builddir))
226 return error_response('ok')
227
228 logger.warning("ERROR:XhrSetDefaultImageUrl")
229 response = HttpResponse()
230 response.status_code = 500
231 return response
232
233
234#
235# Layer Management
236#
237# Rules for 'local_source_dir' layers
238# * Layers must have a unique name in the Layers table
239# * A 'local_source_dir' layer is supposed to be shared
240# by all projects that use it, so that it can have the
241# same logical name
242# * Each project that uses a layer will have its own
243# LayerVersion and Project Layer for it
244# * During the Paroject delete process, when the last
245# LayerVersion for a 'local_source_dir' layer is deleted
246# then the Layer record is deleted to remove orphans
247#
248
249def scan_layer_content(layer,layer_version):
250 # if this is a local layer directory, we can immediately scan its content
251 if layer.local_source_dir:
252 try:
253 # recipes-*/*/*.bb
254 cmd = '%s %s' % ('ls', os.path.join(layer.local_source_dir,'recipes-*/*/*.bb'))
255 recipes_list = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,stderr=subprocess.STDOUT).stdout.read()
256 recipes_list = recipes_list.decode("utf-8").strip()
257 if recipes_list and 'No such' not in recipes_list:
258 for recipe in recipes_list.split('\n'):
259 recipe_path = recipe[recipe.rfind('recipes-'):]
260 recipe_name = recipe[recipe.rfind('/')+1:].replace('.bb','')
261 recipe_ver = recipe_name.rfind('_')
262 if recipe_ver > 0:
263 recipe_name = recipe_name[0:recipe_ver]
264 if recipe_name:
265 ro, created = Recipe.objects.get_or_create(
266 layer_version=layer_version,
267 name=recipe_name
268 )
269 if created:
270 ro.file_path = recipe_path
271 ro.summary = 'Recipe %s from layer %s' % (recipe_name,layer.name)
272 ro.description = ro.summary
273 ro.save()
274
275 except Exception as e:
276 logger.warning("ERROR:scan_layer_content: %s" % e)
277
278class XhrLayer(View):
279 """ Delete, Get, Add and Update Layer information
280
281 Methods: GET POST DELETE PUT
282 """
283
284 def get(self, request, *args, **kwargs):
285 """
286 Get layer information
287
288 Method: GET
289 Entry point: /xhr_layer/<project id>/<layerversion_id>
290 """
291
292 try:
293 layer_version = Layer_Version.objects.get(
294 pk=kwargs['layerversion_id'])
295
296 project = Project.objects.get(pk=kwargs['pid'])
297
298 project_layers = ProjectLayer.objects.filter(
299 project=project).values_list("layercommit_id",
300 flat=True)
301
302 ret = {
303 'error': 'ok',
304 'id': layer_version.pk,
305 'name': layer_version.layer.name,
306 'layerdetailurl':
307 layer_version.get_detailspage_url(project.pk),
308 'vcs_ref': layer_version.get_vcs_reference(),
309 'vcs_url': layer_version.layer.vcs_url,
310 'local_source_dir': layer_version.layer.local_source_dir,
311 'layerdeps': {
312 "list": [
313 {
314 "id": dep.id,
315 "name": dep.layer.name,
316 "layerdetailurl":
317 dep.get_detailspage_url(project.pk),
318 "vcs_url": dep.layer.vcs_url,
319 "vcs_reference": dep.get_vcs_reference()
320 }
321 for dep in layer_version.get_alldeps(project.id)]
322 },
323 'projectlayers': list(project_layers)
324 }
325
326 return JsonResponse(ret)
327 except Layer_Version.DoesNotExist:
328 error_response("No such layer")
329
330 def post(self, request, *args, **kwargs):
331 """
332 Update a layer
333
334 Method: POST
335 Entry point: /xhr_layer/<layerversion_id>
336
337 Args:
338 vcs_url, dirpath, commit, up_branch, summary, description,
339 local_source_dir
340
341 add_dep = append a layerversion_id as a dependency
342 rm_dep = remove a layerversion_id as a depedency
343 Returns:
344 {"error": "ok"}
345 or
346 {"error": <error message>}
347 """
348
349 try:
350 # We currently only allow Imported layers to be edited
351 layer_version = Layer_Version.objects.get(
352 id=kwargs['layerversion_id'],
353 project=kwargs['pid'],
354 layer_source=LayerSource.TYPE_IMPORTED)
355
356 except Layer_Version.DoesNotExist:
357 return error_response("Cannot find imported layer to update")
358
359 if "vcs_url" in request.POST:
360 layer_version.layer.vcs_url = request.POST["vcs_url"]
361 if "dirpath" in request.POST:
362 layer_version.dirpath = request.POST["dirpath"]
363 if "commit" in request.POST:
364 layer_version.commit = request.POST["commit"]
365 layer_version.branch = request.POST["commit"]
366 if "summary" in request.POST:
367 layer_version.layer.summary = request.POST["summary"]
368 if "description" in request.POST:
369 layer_version.layer.description = request.POST["description"]
370 if "local_source_dir" in request.POST:
371 layer_version.layer.local_source_dir = \
372 request.POST["local_source_dir"]
373
374 if "add_dep" in request.POST:
375 lvd = LayerVersionDependency(
376 layer_version=layer_version,
377 depends_on_id=request.POST["add_dep"])
378 lvd.save()
379
380 if "rm_dep" in request.POST:
381 rm_dep = LayerVersionDependency.objects.get(
382 layer_version=layer_version,
383 depends_on_id=request.POST["rm_dep"])
384 rm_dep.delete()
385
386 try:
387 layer_version.layer.save()
388 layer_version.save()
389 except Exception as e:
390 return error_response("Could not update layer version entry: %s"
391 % e)
392
393 return error_response("ok")
394
395 def put(self, request, *args, **kwargs):
396 """ Add a new layer
397
398 Method: PUT
399 Entry point: /xhr_layer/<project id>/
400 Args:
401 project_id, name,
402 [vcs_url, dir_path, git_ref], [local_source_dir], [layer_deps
403 (csv)]
404
405 """
406
407 try:
408 project = Project.objects.get(pk=kwargs['pid'])
409
410 layer_data = json.loads(request.body.decode('utf-8'))
411
412 # We require a unique layer name as otherwise the lists of layers
413 # becomes very confusing
414 existing_layers = \
415 project.get_all_compatible_layer_versions().values_list(
416 "layer__name",
417 flat=True)
418
419 add_to_project = False
420 layer_deps_added = []
421 if 'add_to_project' in layer_data:
422 add_to_project = True
423
424 if layer_data['name'] in existing_layers:
425 return JsonResponse({"error": "layer-name-exists"})
426
427 if ('local_source_dir' in layer_data):
428 # Local layer can be shared across projects. They have no 'release'
429 # and are not included in get_all_compatible_layer_versions() above
430 layer,created = Layer.objects.get_or_create(name=layer_data['name'])
431 _log("Local Layer created=%s" % created)
432 else:
433 layer = Layer.objects.create(name=layer_data['name'])
434
435 layer_version = Layer_Version.objects.create(
436 layer=layer,
437 project=project,
438 layer_source=LayerSource.TYPE_IMPORTED)
439
440 # Local layer
441 if ('local_source_dir' in layer_data): ### and layer.local_source_dir:
442 layer.local_source_dir = layer_data['local_source_dir']
443 # git layer
444 elif 'vcs_url' in layer_data:
445 layer.vcs_url = layer_data['vcs_url']
446 layer_version.dirpath = layer_data['dir_path']
447 layer_version.commit = layer_data['git_ref']
448 layer_version.branch = layer_data['git_ref']
449
450 layer.save()
451 layer_version.save()
452
453 if add_to_project:
454 ProjectLayer.objects.get_or_create(
455 layercommit=layer_version, project=project)
456
457 # Add the layer dependencies
458 if 'layer_deps' in layer_data:
459 for layer_dep_id in layer_data['layer_deps'].split(","):
460 layer_dep = Layer_Version.objects.get(pk=layer_dep_id)
461 LayerVersionDependency.objects.get_or_create(
462 layer_version=layer_version, depends_on=layer_dep)
463
464 # Add layer deps to the project if specified
465 if add_to_project:
466 created, pl = ProjectLayer.objects.get_or_create(
467 layercommit=layer_dep, project=project)
468 layer_deps_added.append(
469 {'name': layer_dep.layer.name,
470 'layerdetailurl':
471 layer_dep.get_detailspage_url(project.pk)})
472
473 # Scan the layer's content and update components
474 scan_layer_content(layer,layer_version)
475
476 except Layer_Version.DoesNotExist:
477 return error_response("layer-dep-not-found")
478 except Project.DoesNotExist:
479 return error_response("project-not-found")
480 except KeyError:
481 return error_response("incorrect-parameters")
482
483 return JsonResponse({'error': "ok",
484 'imported_layer': {
485 'name': layer.name,
486 'layerdetailurl':
487 layer_version.get_detailspage_url()},
488 'deps_added': layer_deps_added})
489
490 def delete(self, request, *args, **kwargs):
491 """ Delete an imported layer
492
493 Method: DELETE
494 Entry point: /xhr_layer/<projed id>/<layerversion_id>
495
496 """
497 try:
498 # We currently only allow Imported layers to be deleted
499 layer_version = Layer_Version.objects.get(
500 id=kwargs['layerversion_id'],
501 project=kwargs['pid'],
502 layer_source=LayerSource.TYPE_IMPORTED)
503 except Layer_Version.DoesNotExist:
504 return error_response("Cannot find imported layer to delete")
505
506 try:
507 ProjectLayer.objects.get(project=kwargs['pid'],
508 layercommit=layer_version).delete()
509 except ProjectLayer.DoesNotExist:
510 pass
511
512 layer_version.layer.delete()
513 layer_version.delete()
514
515 return JsonResponse({
516 "error": "ok",
517 "gotoUrl": reverse('projectlayers', args=(kwargs['pid'],))
518 })
519
520
521class XhrCustomRecipe(View):
522 """ Create a custom image recipe """
523
524 def post(self, request, *args, **kwargs):
525 """
526 Custom image recipe REST API
527
528 Entry point: /xhr_customrecipe/
529 Method: POST
530
531 Args:
532 name: name of custom recipe to create
533 project: target project id of orm.models.Project
534 base: base recipe id of orm.models.Recipe
535
536 Returns:
537 {"error": "ok",
538 "url": <url of the created recipe>}
539 or
540 {"error": <error message>}
541 """
542 # check if request has all required parameters
543 for param in ('name', 'project', 'base'):
544 if param not in request.POST:
545 return error_response("Missing parameter '%s'" % param)
546
547 # get project and baserecipe objects
548 params = {}
549 for name, model in [("project", Project),
550 ("base", Recipe)]:
551 value = request.POST[name]
552 try:
553 params[name] = model.objects.get(id=value)
554 except model.DoesNotExist:
555 return error_response("Invalid %s id %s" % (name, value))
556
557 # create custom recipe
558 try:
559
560 # Only allowed chars in name are a-z, 0-9 and -
561 if re.search(r'[^a-z|0-9|-]', request.POST["name"]):
562 return error_response("invalid-name")
563
564 custom_images = CustomImageRecipe.objects.all()
565
566 # Are there any recipes with this name already in our project?
567 existing_image_recipes_in_project = custom_images.filter(
568 name=request.POST["name"], project=params["project"])
569
570 if existing_image_recipes_in_project.count() > 0:
571 return error_response("image-already-exists")
572
573 # Are there any recipes with this name which aren't custom
574 # image recipes?
575 custom_image_ids = custom_images.values_list('id', flat=True)
576 existing_non_image_recipes = Recipe.objects.filter(
577 Q(name=request.POST["name"]) & ~Q(pk__in=custom_image_ids)
578 )
579
580 if existing_non_image_recipes.count() > 0:
581 return error_response("recipe-already-exists")
582
583 # create layer 'Custom layer' and verion if needed
584 layer, l_created = Layer.objects.get_or_create(
585 name=CustomImageRecipe.LAYER_NAME,
586 summary="Layer for custom recipes")
587
588 if l_created:
589 layer.local_source_dir = "toaster_created_layer"
590 layer.save()
591
592 # Check if we have a layer version already
593 # We don't use get_or_create here because the dirpath will change
594 # and is a required field
595 lver = Layer_Version.objects.filter(Q(project=params['project']) &
596 Q(layer=layer) &
597 Q(build=None)).last()
598 if lver is None:
599 lver, lv_created = Layer_Version.objects.get_or_create(
600 project=params['project'],
601 layer=layer,
602 layer_source=LayerSource.TYPE_LOCAL,
603 dirpath="toaster_created_layer")
604
605 # Add a dependency on our layer to the base recipe's layer
606 LayerVersionDependency.objects.get_or_create(
607 layer_version=lver,
608 depends_on=params["base"].layer_version)
609
610 # Add it to our current project if needed
611 ProjectLayer.objects.get_or_create(project=params['project'],
612 layercommit=lver,
613 optional=False)
614
615 # Create the actual recipe
616 recipe, r_created = CustomImageRecipe.objects.get_or_create(
617 name=request.POST["name"],
618 base_recipe=params["base"],
619 project=params["project"],
620 layer_version=lver,
621 is_image=True)
622
623 # If we created the object then setup these fields. They may get
624 # overwritten later on and cause the get_or_create to create a
625 # duplicate if they've changed.
626 if r_created:
627 recipe.file_path = request.POST["name"]
628 recipe.license = "MIT"
629 recipe.version = "0.1"
630 recipe.save()
631
632 except Error as err:
633 return error_response("Can't create custom recipe: %s" % err)
634
635 # Find the package list from the last build of this recipe/target
636 target = Target.objects.filter(Q(build__outcome=Build.SUCCEEDED) &
637 Q(build__project=params['project']) &
638 (Q(target=params['base'].name) |
639 Q(target=recipe.name))).last()
640 if target:
641 # Copy in every package
642 # We don't want these packages to be linked to anything because
643 # that underlying data may change e.g. delete a build
644 for tpackage in target.target_installed_package_set.all():
645 try:
646 built_package = tpackage.package
647 # The package had no recipe information so is a ghost
648 # package skip it
649 if built_package.recipe is None:
650 continue
651
652 config_package = CustomImagePackage.objects.get(
653 name=built_package.name)
654
655 recipe.includes_set.add(config_package)
656 except Exception as e:
657 logger.warning("Error adding package %s %s" %
658 (tpackage.package.name, e))
659 pass
660
661 # pre-create layer directory structure, so that other builds
662 # are not blocked by this new recipe dependecy
663 # NOTE: this is parallel code to 'localhostbecontroller.py'
664 be = BuildEnvironment.objects.all()[0]
665 layerpath = os.path.join(be.builddir,
666 CustomImageRecipe.LAYER_NAME)
667 for name in ("conf", "recipes"):
668 path = os.path.join(layerpath, name)
669 if not os.path.isdir(path):
670 os.makedirs(path)
671 # pre-create layer.conf
672 config = os.path.join(layerpath, "conf", "layer.conf")
673 if not os.path.isfile(config):
674 with open(config, "w") as conf:
675 conf.write('BBPATH .= ":${LAYERDIR}"\nBBFILES += "${LAYERDIR}/recipes/*.bb"\n')
676 # pre-create new image's recipe file
677 recipe_path = os.path.join(layerpath, "recipes", "%s.bb" %
678 recipe.name)
679 with open(recipe_path, "w") as recipef:
680 content = recipe.generate_recipe_file_contents()
681 if not content:
682 # Delete this incomplete image recipe object
683 recipe.delete()
684 return error_response("recipe-parent-not-exist")
685 else:
686 recipef.write(recipe.generate_recipe_file_contents())
687
688 return JsonResponse(
689 {"error": "ok",
690 "packages": recipe.get_all_packages().count(),
691 "url": reverse('customrecipe', args=(params['project'].pk,
692 recipe.id))})
693
694
695class XhrCustomRecipeId(View):
696 """
697 Set of ReST API processors working with recipe id.
698
699 Entry point: /xhr_customrecipe/<recipe_id>
700
701 Methods:
702 GET - Get details of custom image recipe
703 DELETE - Delete custom image recipe
704
705 Returns:
706 GET:
707 {"error": "ok",
708 "info": dictionary of field name -> value pairs
709 of the CustomImageRecipe model}
710 DELETE:
711 {"error": "ok"}
712 or
713 {"error": <error message>}
714 """
715 @staticmethod
716 def _get_ci_recipe(recipe_id):
717 """ Get Custom Image recipe or return an error response"""
718 try:
719 custom_recipe = \
720 CustomImageRecipe.objects.get(pk=recipe_id)
721 return custom_recipe, None
722
723 except CustomImageRecipe.DoesNotExist:
724 return None, error_response("Custom recipe with id=%s "
725 "not found" % recipe_id)
726
727 def get(self, request, *args, **kwargs):
728 custom_recipe, error = self._get_ci_recipe(kwargs['recipe_id'])
729 if error:
730 return error
731
732 if request.method == 'GET':
733 info = {"id": custom_recipe.id,
734 "name": custom_recipe.name,
735 "base_recipe_id": custom_recipe.base_recipe.id,
736 "project_id": custom_recipe.project.id}
737
738 return JsonResponse({"error": "ok", "info": info})
739
740 def delete(self, request, *args, **kwargs):
741 custom_recipe, error = self._get_ci_recipe(kwargs['recipe_id'])
742 if error:
743 return error
744
745 project = custom_recipe.project
746
747 custom_recipe.delete()
748 return JsonResponse({"error": "ok",
749 "gotoUrl": reverse("projectcustomimages",
750 args=(project.pk,))})
751
752
753class XhrCustomRecipePackages(View):
754 """
755 ReST API to add/remove packages to/from custom recipe.
756
757 Entry point: /xhr_customrecipe/<recipe_id>/packages/<package_id>
758 Methods:
759 PUT - Add package to the recipe
760 DELETE - Delete package from the recipe
761 GET - Get package information
762
763 Returns:
764 {"error": "ok"}
765 or
766 {"error": <error message>}
767 """
768 @staticmethod
769 def _get_package(package_id):
770 try:
771 package = CustomImagePackage.objects.get(pk=package_id)
772 return package, None
773 except Package.DoesNotExist:
774 return None, error_response("Package with id=%s "
775 "not found" % package_id)
776
777 def _traverse_dependents(self, next_package_id,
778 rev_deps, all_current_packages, tree_level=0):
779 """
780 Recurse through reverse dependency tree for next_package_id.
781 Limit the reverse dependency search to packages not already scanned,
782 that is, not already in rev_deps.
783 Limit the scan to a depth (tree_level) not exceeding the count of
784 all packages in the custom image, and if that depth is exceeded
785 return False, pop out of the recursion, and write a warning
786 to the log, but this is unlikely, suggesting a dependency loop
787 not caught by bitbake.
788 On return, the input/output arg rev_deps is appended with queryset
789 dictionary elements, annotated for use in the customimage template.
790 The list has unsorted, but unique elements.
791 """
792 max_dependency_tree_depth = all_current_packages.count()
793 if tree_level >= max_dependency_tree_depth:
794 logger.warning(
795 "The number of reverse dependencies "
796 "for this package exceeds " + max_dependency_tree_depth +
797 " and the remaining reverse dependencies will not be removed")
798 return True
799
800 package = CustomImagePackage.objects.get(id=next_package_id)
801 dependents = \
802 package.package_dependencies_target.annotate(
803 name=F('package__name'),
804 pk=F('package__pk'),
805 size=F('package__size'),
806 ).values("name", "pk", "size").exclude(
807 ~Q(pk__in=all_current_packages)
808 )
809
810 for pkg in dependents:
811 if pkg in rev_deps:
812 # already seen, skip dependent search
813 continue
814
815 rev_deps.append(pkg)
816 if (self._traverse_dependents(pkg["pk"], rev_deps,
817 all_current_packages,
818 tree_level+1)):
819 return True
820
821 return False
822
823 def _get_all_dependents(self, package_id, all_current_packages):
824 """
825 Returns sorted list of recursive reverse dependencies for package_id,
826 as a list of dictionary items, by recursing through dependency
827 relationships.
828 """
829 rev_deps = []
830 self._traverse_dependents(package_id, rev_deps, all_current_packages)
831 rev_deps = sorted(rev_deps, key=lambda x: x["name"])
832 return rev_deps
833
834 def get(self, request, *args, **kwargs):
835 recipe, error = XhrCustomRecipeId._get_ci_recipe(
836 kwargs['recipe_id'])
837 if error:
838 return error
839
840 # If no package_id then list all the current packages
841 if not kwargs['package_id']:
842 total_size = 0
843 packages = recipe.get_all_packages().values("id",
844 "name",
845 "version",
846 "size")
847 for package in packages:
848 package['size_formatted'] = \
849 filtered_filesizeformat(package['size'])
850 total_size += package['size']
851
852 return JsonResponse({"error": "ok",
853 "packages": list(packages),
854 "total": len(packages),
855 "total_size": total_size,
856 "total_size_formatted":
857 filtered_filesizeformat(total_size)})
858 else:
859 package, error = XhrCustomRecipePackages._get_package(
860 kwargs['package_id'])
861 if error:
862 return error
863
864 all_current_packages = recipe.get_all_packages()
865
866 # Dependencies for package which aren't satisfied by the
867 # current packages in the custom image recipe
868 deps = package.package_dependencies_source.for_target_or_none(
869 recipe.name)['packages'].annotate(
870 name=F('depends_on__name'),
871 pk=F('depends_on__pk'),
872 size=F('depends_on__size'),
873 ).values("name", "pk", "size").filter(
874 # There are two depends types we don't know why
875 (Q(dep_type=Package_Dependency.TYPE_TRDEPENDS) |
876 Q(dep_type=Package_Dependency.TYPE_RDEPENDS)) &
877 ~Q(pk__in=all_current_packages)
878 )
879
880 # Reverse dependencies which are needed by packages that are
881 # in the image. Recursive search providing all dependents,
882 # not just immediate dependents.
883 reverse_deps = self._get_all_dependents(kwargs['package_id'],
884 all_current_packages)
885 total_size_deps = 0
886 total_size_reverse_deps = 0
887
888 for dep in deps:
889 dep['size_formatted'] = \
890 filtered_filesizeformat(dep['size'])
891 total_size_deps += dep['size']
892
893 for dep in reverse_deps:
894 dep['size_formatted'] = \
895 filtered_filesizeformat(dep['size'])
896 total_size_reverse_deps += dep['size']
897
898 return JsonResponse(
899 {"error": "ok",
900 "id": package.pk,
901 "name": package.name,
902 "version": package.version,
903 "unsatisfied_dependencies": list(deps),
904 "unsatisfied_dependencies_size": total_size_deps,
905 "unsatisfied_dependencies_size_formatted":
906 filtered_filesizeformat(total_size_deps),
907 "reverse_dependencies": list(reverse_deps),
908 "reverse_dependencies_size": total_size_reverse_deps,
909 "reverse_dependencies_size_formatted":
910 filtered_filesizeformat(total_size_reverse_deps)})
911
912 def put(self, request, *args, **kwargs):
913 recipe, error = XhrCustomRecipeId._get_ci_recipe(kwargs['recipe_id'])
914 package, error = self._get_package(kwargs['package_id'])
915 if error:
916 return error
917
918 included_packages = recipe.includes_set.values_list('pk',
919 flat=True)
920
921 # If we're adding back a package which used to be included in this
922 # image all we need to do is remove it from the excludes
923 if package.pk in included_packages:
924 try:
925 recipe.excludes_set.remove(package)
926 return {"error": "ok"}
927 except Package.DoesNotExist:
928 return error_response("Package %s not found in excludes"
929 " but was in included list" %
930 package.name)
931 else:
932 recipe.appends_set.add(package)
933 # Make sure that package is not in the excludes set
934 try:
935 recipe.excludes_set.remove(package)
936 except:
937 pass
938
939 # Add the dependencies we think will be added to the recipe
940 # as a result of appending this package.
941 # TODO this should recurse down the entire deps tree
942 for dep in package.package_dependencies_source.all_depends():
943 try:
944 cust_package = CustomImagePackage.objects.get(
945 name=dep.depends_on.name)
946
947 recipe.includes_set.add(cust_package)
948 try:
949 # When adding the pre-requisite package, make
950 # sure it's not in the excluded list from a
951 # prior removal.
952 recipe.excludes_set.remove(cust_package)
953 except package.DoesNotExist:
954 # Don't care if the package had never been excluded
955 pass
956 except:
957 logger.warning("Could not add package's suggested"
958 "dependencies to the list")
959 return JsonResponse({"error": "ok"})
960
961 def delete(self, request, *args, **kwargs):
962 recipe, error = XhrCustomRecipeId._get_ci_recipe(kwargs['recipe_id'])
963 package, error = self._get_package(kwargs['package_id'])
964 if error:
965 return error
966
967 try:
968 included_packages = recipe.includes_set.values_list('pk',
969 flat=True)
970 # If we're deleting a package which is included we need to
971 # Add it to the excludes list.
972 if package.pk in included_packages:
973 recipe.excludes_set.add(package)
974 else:
975 recipe.appends_set.remove(package)
976
977 # remove dependencies as well
978 all_current_packages = recipe.get_all_packages()
979
980 reverse_deps_dictlist = self._get_all_dependents(
981 package.pk,
982 all_current_packages)
983
984 ids = [entry['pk'] for entry in reverse_deps_dictlist]
985 reverse_deps = CustomImagePackage.objects.filter(id__in=ids)
986 for r in reverse_deps:
987 try:
988 if r.id in included_packages:
989 recipe.excludes_set.add(r)
990 else:
991 recipe.appends_set.remove(r)
992 except:
993 pass
994
995 return JsonResponse({"error": "ok"})
996 except CustomImageRecipe.DoesNotExist:
997 return error_response("Tried to remove package that wasn't"
998 " present")
999
1000
1001class XhrProject(View):
1002 """ Create, delete or edit a project
1003
1004 Entry point: /xhr_project/<project_id>
1005 """
1006 def post(self, request, *args, **kwargs):
1007 """
1008 Edit project control
1009
1010 Args:
1011 layerAdd = layer_version_id layer_version_id ...
1012 layerDel = layer_version_id layer_version_id ...
1013 projectName = new_project_name
1014 machineName = new_machine_name
1015
1016 Returns:
1017 {"error": "ok"}
1018 or
1019 {"error": <error message>}
1020 """
1021 try:
1022 prj = Project.objects.get(pk=kwargs['project_id'])
1023 except Project.DoesNotExist:
1024 return error_response("No such project")
1025
1026 # Add layers
1027 if 'layerAdd' in request.POST and len(request.POST['layerAdd']) > 0:
1028 for layer_version_id in request.POST['layerAdd'].split(','):
1029 try:
1030 lv = Layer_Version.objects.get(pk=int(layer_version_id))
1031 ProjectLayer.objects.get_or_create(project=prj,
1032 layercommit=lv)
1033 except Layer_Version.DoesNotExist:
1034 return error_response("Layer version %s asked to add "
1035 "doesn't exist" % layer_version_id)
1036
1037 # Remove layers
1038 if 'layerDel' in request.POST and len(request.POST['layerDel']) > 0:
1039 layer_version_ids = request.POST['layerDel'].split(',')
1040 ProjectLayer.objects.filter(
1041 project=prj,
1042 layercommit_id__in=layer_version_ids).delete()
1043
1044 # Project name change
1045 if 'projectName' in request.POST:
1046 prj.name = request.POST['projectName']
1047 prj.save()
1048
1049 # Machine name change
1050 if 'machineName' in request.POST:
1051 machinevar = prj.projectvariable_set.get(name="MACHINE")
1052 machinevar.value = request.POST['machineName']
1053 machinevar.save()
1054
1055 # Distro name change
1056 if 'distroName' in request.POST:
1057 distrovar = prj.projectvariable_set.get(name="DISTRO")
1058 distrovar.value = request.POST['distroName']
1059 distrovar.save()
1060
1061 return JsonResponse({"error": "ok"})
1062
1063 def get(self, request, *args, **kwargs):
1064 """
1065 Returns:
1066 json object representing the current project
1067 or:
1068 {"error": <error message>}
1069 """
1070
1071 try:
1072 project = Project.objects.get(pk=kwargs['project_id'])
1073 except Project.DoesNotExist:
1074 return error_response("Project %s does not exist" %
1075 kwargs['project_id'])
1076
1077 # Create the frequently built targets list
1078
1079 freqtargets = Counter(Target.objects.filter(
1080 Q(build__project=project),
1081 ~Q(build__outcome=Build.IN_PROGRESS)
1082 ).order_by("target").values_list("target", flat=True))
1083
1084 freqtargets = freqtargets.most_common(5)
1085
1086 # We now have the targets in order of frequency but if there are two
1087 # with the same frequency then we need to make sure those are in
1088 # alphabetical order without losing the frequency ordering
1089
1090 tmp = []
1091 switch = None
1092 for i, freqtartget in enumerate(freqtargets):
1093 target, count = freqtartget
1094 try:
1095 target_next, count_next = freqtargets[i+1]
1096 if count == count_next and target > target_next:
1097 switch = target
1098 continue
1099 except IndexError:
1100 pass
1101
1102 tmp.append(target)
1103
1104 if switch:
1105 tmp.append(switch)
1106 switch = None
1107
1108 freqtargets = tmp
1109
1110 layers = []
1111 for layer in project.projectlayer_set.all():
1112 layers.append({
1113 "id": layer.layercommit.pk,
1114 "name": layer.layercommit.layer.name,
1115 "vcs_url": layer.layercommit.layer.vcs_url,
1116 "local_source_dir": layer.layercommit.layer.local_source_dir,
1117 "vcs_reference": layer.layercommit.get_vcs_reference(),
1118 "url": layer.layercommit.layer.layer_index_url,
1119 "layerdetailurl": layer.layercommit.get_detailspage_url(
1120 project.pk),
1121 "xhrLayerUrl": reverse("xhr_layer",
1122 args=(project.pk,
1123 layer.layercommit.pk)),
1124 "layersource": layer.layercommit.layer_source
1125 })
1126
1127 data = {
1128 "name": project.name,
1129 "layers": layers,
1130 "freqtargets": freqtargets,
1131 }
1132
1133 if project.release is not None:
1134 data['release'] = {
1135 "id": project.release.pk,
1136 "name": project.release.name,
1137 "description": project.release.description
1138 }
1139
1140 try:
1141 data["machine"] = {"name":
1142 project.projectvariable_set.get(
1143 name="MACHINE").value}
1144 except ProjectVariable.DoesNotExist:
1145 data["machine"] = None
1146 try:
1147 data["distro"] = {"name":
1148 project.projectvariable_set.get(
1149 name="DISTRO").value}
1150 except ProjectVariable.DoesNotExist:
1151 data["distro"] = None
1152
1153 data['error'] = "ok"
1154
1155 return JsonResponse(data)
1156
1157 def put(self, request, *args, **kwargs):
1158 # TODO create new project api
1159 return HttpResponse()
1160
1161 def delete(self, request, *args, **kwargs):
1162 """Delete a project. Cancels any builds in progress"""
1163 try:
1164 project = Project.objects.get(pk=kwargs['project_id'])
1165 # Cancel any builds in progress
1166 for br in BuildRequest.objects.filter(
1167 project=project,
1168 state=BuildRequest.REQ_INPROGRESS):
1169 XhrBuildRequest.cancel_build(br)
1170
1171 # gather potential orphaned local layers attached to this project
1172 project_local_layer_list = []
1173 for pl in ProjectLayer.objects.filter(project=project):
1174 if pl.layercommit.layer_source == LayerSource.TYPE_IMPORTED:
1175 project_local_layer_list.append(pl.layercommit.layer)
1176
1177 # deep delete the project and its dependencies
1178 project.delete()
1179
1180 # delete any local layers now orphaned
1181 _log("LAYER_ORPHAN_CHECK:Check for orphaned layers")
1182 for layer in project_local_layer_list:
1183 layer_refs = Layer_Version.objects.filter(layer=layer)
1184 _log("LAYER_ORPHAN_CHECK:Ref Count for '%s' = %d" % (layer.name,len(layer_refs)))
1185 if 0 == len(layer_refs):
1186 _log("LAYER_ORPHAN_CHECK:DELETE orpahned '%s'" % (layer.name))
1187 Layer.objects.filter(pk=layer.id).delete()
1188
1189 except Project.DoesNotExist:
1190 return error_response("Project %s does not exist" %
1191 kwargs['project_id'])
1192
1193 return JsonResponse({
1194 "error": "ok",
1195 "gotoUrl": reverse("all-projects", args=[])
1196 })
1197
1198
1199class XhrBuild(View):
1200 """ Delete a build object
1201
1202 Entry point: /xhr_build/<build_id>
1203 """
1204 def delete(self, request, *args, **kwargs):
1205 """
1206 Delete build data
1207
1208 Args:
1209 build_id = build_id
1210
1211 Returns:
1212 {"error": "ok"}
1213 or
1214 {"error": <error message>}
1215 """
1216 try:
1217 build = Build.objects.get(pk=kwargs['build_id'])
1218 project = build.project
1219 build.delete()
1220 except Build.DoesNotExist:
1221 return error_response("Build %s does not exist" %
1222 kwargs['build_id'])
1223 return JsonResponse({
1224 "error": "ok",
1225 "gotoUrl": reverse("projectbuilds", args=(project.pk,))
1226 })