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