xref: /OK3568_Linux_fs/yocto/poky/bitbake/lib/toaster/toastergui/api.py (revision 4882a59341e53eb6f0b4789bf948001014eff981)
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