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