blob: ae1f150770569835f692298d619ab9e824cfba92 [file] [log] [blame]
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001#
2# BitBake Toaster Implementation
3#
4# Copyright (C) 2016 Intel Corporation
5#
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License version 2 as
8# published by the Free Software Foundation.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License along
16# with this program; if not, write to the Free Software Foundation, Inc.,
17# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
Patrick Williamsc0f7c042017-02-23 20:41:17 -060019# Please run flake8 on this file before sending patches
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050020
Patrick Williamsc0f7c042017-02-23 20:41:17 -060021import re
22import logging
23from collections import Counter
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050024
Patrick Williamsc0f7c042017-02-23 20:41:17 -060025from orm.models import Project, ProjectTarget, Build, Layer_Version
26from orm.models import LayerVersionDependency, LayerSource, ProjectLayer
27from orm.models import Recipe, CustomImageRecipe, CustomImagePackage
28from orm.models import Layer, Target, Package, Package_Dependency
29from orm.models import ProjectVariable
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050030from bldcontrol.models import BuildRequest
31from bldcontrol import bbcontroller
Patrick Williamsc0f7c042017-02-23 20:41:17 -060032
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050033from django.http import HttpResponse, JsonResponse
34from django.views.generic import View
Patrick Williamsc0f7c042017-02-23 20:41:17 -060035from django.core.urlresolvers import reverse
36from django.db.models import Q, F
37from django.db import Error
38from toastergui.templatetags.projecttags import filtered_filesizeformat
39
40logger = logging.getLogger("toaster")
41
42
43def error_response(error):
44 return JsonResponse({"error": error})
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050045
46
47class XhrBuildRequest(View):
48
49 def get(self, request, *args, **kwargs):
50 return HttpResponse()
51
Patrick Williamsc0f7c042017-02-23 20:41:17 -060052 @staticmethod
53 def cancel_build(br):
54 """Cancel a build request"""
55 try:
56 bbctrl = bbcontroller.BitbakeController(br.environment)
57 bbctrl.forceShutDown()
58 except:
59 # We catch a bunch of exceptions here because
60 # this is where the server has not had time to start up
61 # and the build request or build is in transit between
62 # processes.
63 # We can safely just set the build as cancelled
64 # already as it never got started
65 build = br.build
66 build.outcome = Build.CANCELLED
67 build.save()
68
69 # We now hand over to the buildinfohelper to update the
70 # build state once we've finished cancelling
71 br.state = BuildRequest.REQ_CANCELLING
72 br.save()
73
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050074 def post(self, request, *args, **kwargs):
75 """
76 Build control
77
78 Entry point: /xhr_buildrequest/<project_id>
79 Method: POST
80
81 Args:
82 id: id of build to change
83 buildCancel = build_request_id ...
84 buildDelete = id ...
85 targets = recipe_name ...
86
87 Returns:
88 {"error": "ok"}
89 or
90 {"error": <error message>}
91 """
92
93 project = Project.objects.get(pk=kwargs['pid'])
94
95 if 'buildCancel' in request.POST:
96 for i in request.POST['buildCancel'].strip().split(" "):
97 try:
98 br = BuildRequest.objects.get(project=project, pk=i)
Patrick Williamsc0f7c042017-02-23 20:41:17 -060099 self.cancel_build(br)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500100 except BuildRequest.DoesNotExist:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600101 return error_response('No such build request id %s' % i)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500102
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600103 return error_response('ok')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500104
105 if 'buildDelete' in request.POST:
106 for i in request.POST['buildDelete'].strip().split(" "):
107 try:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600108 BuildRequest.objects.select_for_update().get(
109 project=project,
110 pk=i,
111 state__lte=BuildRequest.REQ_DELETED).delete()
112
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500113 except BuildRequest.DoesNotExist:
114 pass
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600115 return error_response("ok")
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500116
117 if 'targets' in request.POST:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600118 ProjectTarget.objects.filter(project=project).delete()
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500119 s = str(request.POST['targets'])
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600120 for t in re.sub(r'[;%|"]', '', s).split(" "):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500121 if ":" in t:
122 target, task = t.split(":")
123 else:
124 target = t
125 task = ""
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600126 ProjectTarget.objects.create(project=project,
127 target=target,
128 task=task)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500129 project.schedule_build()
130
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600131 return error_response('ok')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500132
133 response = HttpResponse()
134 response.status_code = 500
135 return response
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600136
137
138class XhrLayer(View):
139 """ Get and Update Layer information """
140
141 def post(self, request, *args, **kwargs):
142 """
143 Update a layer
144
145 Entry point: /xhr_layer/<layerversion_id>
146 Method: POST
147
148 Args:
149 vcs_url, dirpath, commit, up_branch, summary, description,
150 local_source_dir
151
152 add_dep = append a layerversion_id as a dependency
153 rm_dep = remove a layerversion_id as a depedency
154 Returns:
155 {"error": "ok"}
156 or
157 {"error": <error message>}
158 """
159
160 try:
161 # We currently only allow Imported layers to be edited
162 layer_version = Layer_Version.objects.get(
163 id=kwargs['layerversion_id'],
164 project=kwargs['pid'],
165 layer_source=LayerSource.TYPE_IMPORTED)
166
167 except Layer_Version.DoesNotExist:
168 return error_response("Cannot find imported layer to update")
169
170 if "vcs_url" in request.POST:
171 layer_version.layer.vcs_url = request.POST["vcs_url"]
172 if "dirpath" in request.POST:
173 layer_version.dirpath = request.POST["dirpath"]
174 if "commit" in request.POST:
175 layer_version.commit = request.POST["commit"]
176 layer_version.branch = request.POST["commit"]
177 if "summary" in request.POST:
178 layer_version.layer.summary = request.POST["summary"]
179 if "description" in request.POST:
180 layer_version.layer.description = request.POST["description"]
181 if "local_source_dir" in request.POST:
182 layer_version.layer.local_source_dir = \
183 request.POST["local_source_dir"]
184
185 if "add_dep" in request.POST:
186 lvd = LayerVersionDependency(
187 layer_version=layer_version,
188 depends_on_id=request.POST["add_dep"])
189 lvd.save()
190
191 if "rm_dep" in request.POST:
192 rm_dep = LayerVersionDependency.objects.get(
193 layer_version=layer_version,
194 depends_on_id=request.POST["rm_dep"])
195 rm_dep.delete()
196
197 try:
198 layer_version.layer.save()
199 layer_version.save()
200 except Exception as e:
201 return error_response("Could not update layer version entry: %s"
202 % e)
203
204 return JsonResponse({"error": "ok"})
205
206 def delete(self, request, *args, **kwargs):
207 try:
208 # We currently only allow Imported layers to be deleted
209 layer_version = Layer_Version.objects.get(
210 id=kwargs['layerversion_id'],
211 project=kwargs['pid'],
212 layer_source=LayerSource.TYPE_IMPORTED)
213 except Layer_Version.DoesNotExist:
214 return error_response("Cannot find imported layer to delete")
215
216 try:
217 ProjectLayer.objects.get(project=kwargs['pid'],
218 layercommit=layer_version).delete()
219 except ProjectLayer.DoesNotExist:
220 pass
221
222 layer_version.layer.delete()
223 layer_version.delete()
224
225 return JsonResponse({
226 "error": "ok",
227 "gotoUrl": reverse('projectlayers', args=(kwargs['pid'],))
228 })
229
230
231class XhrCustomRecipe(View):
232 """ Create a custom image recipe """
233
234 def post(self, request, *args, **kwargs):
235 """
236 Custom image recipe REST API
237
238 Entry point: /xhr_customrecipe/
239 Method: POST
240
241 Args:
242 name: name of custom recipe to create
243 project: target project id of orm.models.Project
244 base: base recipe id of orm.models.Recipe
245
246 Returns:
247 {"error": "ok",
248 "url": <url of the created recipe>}
249 or
250 {"error": <error message>}
251 """
252 # check if request has all required parameters
253 for param in ('name', 'project', 'base'):
254 if param not in request.POST:
255 return error_response("Missing parameter '%s'" % param)
256
257 # get project and baserecipe objects
258 params = {}
259 for name, model in [("project", Project),
260 ("base", Recipe)]:
261 value = request.POST[name]
262 try:
263 params[name] = model.objects.get(id=value)
264 except model.DoesNotExist:
265 return error_response("Invalid %s id %s" % (name, value))
266
267 # create custom recipe
268 try:
269
270 # Only allowed chars in name are a-z, 0-9 and -
271 if re.search(r'[^a-z|0-9|-]', request.POST["name"]):
272 return error_response("invalid-name")
273
274 custom_images = CustomImageRecipe.objects.all()
275
276 # Are there any recipes with this name already in our project?
277 existing_image_recipes_in_project = custom_images.filter(
278 name=request.POST["name"], project=params["project"])
279
280 if existing_image_recipes_in_project.count() > 0:
281 return error_response("image-already-exists")
282
283 # Are there any recipes with this name which aren't custom
284 # image recipes?
285 custom_image_ids = custom_images.values_list('id', flat=True)
286 existing_non_image_recipes = Recipe.objects.filter(
287 Q(name=request.POST["name"]) & ~Q(pk__in=custom_image_ids)
288 )
289
290 if existing_non_image_recipes.count() > 0:
291 return error_response("recipe-already-exists")
292
293 # create layer 'Custom layer' and verion if needed
294 layer = Layer.objects.get_or_create(
295 name=CustomImageRecipe.LAYER_NAME,
296 summary="Layer for custom recipes",
297 vcs_url="file:///toaster_created_layer")[0]
298
299 # Check if we have a layer version already
300 # We don't use get_or_create here because the dirpath will change
301 # and is a required field
302 lver = Layer_Version.objects.filter(Q(project=params['project']) &
303 Q(layer=layer) &
304 Q(build=None)).last()
305 if lver is None:
306 lver, created = Layer_Version.objects.get_or_create(
307 project=params['project'],
308 layer=layer,
309 dirpath="toaster_created_layer")
310
311 # Add a dependency on our layer to the base recipe's layer
312 LayerVersionDependency.objects.get_or_create(
313 layer_version=lver,
314 depends_on=params["base"].layer_version)
315
316 # Add it to our current project if needed
317 ProjectLayer.objects.get_or_create(project=params['project'],
318 layercommit=lver,
319 optional=False)
320
321 # Create the actual recipe
322 recipe, created = CustomImageRecipe.objects.get_or_create(
323 name=request.POST["name"],
324 base_recipe=params["base"],
325 project=params["project"],
326 layer_version=lver,
327 is_image=True)
328
329 # If we created the object then setup these fields. They may get
330 # overwritten later on and cause the get_or_create to create a
331 # duplicate if they've changed.
332 if created:
333 recipe.file_path = request.POST["name"]
334 recipe.license = "MIT"
335 recipe.version = "0.1"
336 recipe.save()
337
338 except Error as err:
339 return error_response("Can't create custom recipe: %s" % err)
340
341 # Find the package list from the last build of this recipe/target
342 target = Target.objects.filter(Q(build__outcome=Build.SUCCEEDED) &
343 Q(build__project=params['project']) &
344 (Q(target=params['base'].name) |
345 Q(target=recipe.name))).last()
346 if target:
347 # Copy in every package
348 # We don't want these packages to be linked to anything because
349 # that underlying data may change e.g. delete a build
350 for tpackage in target.target_installed_package_set.all():
351 try:
352 built_package = tpackage.package
353 # The package had no recipe information so is a ghost
354 # package skip it
355 if built_package.recipe is None:
356 continue
357
358 config_package = CustomImagePackage.objects.get(
359 name=built_package.name)
360
361 recipe.includes_set.add(config_package)
362 except Exception as e:
363 logger.warning("Error adding package %s %s" %
364 (tpackage.package.name, e))
365 pass
366
367 return JsonResponse(
368 {"error": "ok",
369 "packages": recipe.get_all_packages().count(),
370 "url": reverse('customrecipe', args=(params['project'].pk,
371 recipe.id))})
372
373
374class XhrCustomRecipeId(View):
375 """
376 Set of ReST API processors working with recipe id.
377
378 Entry point: /xhr_customrecipe/<recipe_id>
379
380 Methods:
381 GET - Get details of custom image recipe
382 DELETE - Delete custom image recipe
383
384 Returns:
385 GET:
386 {"error": "ok",
387 "info": dictionary of field name -> value pairs
388 of the CustomImageRecipe model}
389 DELETE:
390 {"error": "ok"}
391 or
392 {"error": <error message>}
393 """
394 @staticmethod
395 def _get_ci_recipe(recipe_id):
396 """ Get Custom Image recipe or return an error response"""
397 try:
398 custom_recipe = \
399 CustomImageRecipe.objects.get(pk=recipe_id)
400 return custom_recipe, None
401
402 except CustomImageRecipe.DoesNotExist:
403 return None, error_response("Custom recipe with id=%s "
404 "not found" % recipe_id)
405
406 def get(self, request, *args, **kwargs):
407 custom_recipe, error = self._get_ci_recipe(kwargs['recipe_id'])
408 if error:
409 return error
410
411 if request.method == 'GET':
412 info = {"id": custom_recipe.id,
413 "name": custom_recipe.name,
414 "base_recipe_id": custom_recipe.base_recipe.id,
415 "project_id": custom_recipe.project.id}
416
417 return JsonResponse({"error": "ok", "info": info})
418
419 def delete(self, request, *args, **kwargs):
420 custom_recipe, error = self._get_ci_recipe(kwargs['recipe_id'])
421 if error:
422 return error
423
424 project = custom_recipe.project
425
426 custom_recipe.delete()
427 return JsonResponse({"error": "ok",
428 "gotoUrl": reverse("projectcustomimages",
429 args=(project.pk,))})
430
431
432class XhrCustomRecipePackages(View):
433 """
434 ReST API to add/remove packages to/from custom recipe.
435
436 Entry point: /xhr_customrecipe/<recipe_id>/packages/<package_id>
437 Methods:
438 PUT - Add package to the recipe
439 DELETE - Delete package from the recipe
440 GET - Get package information
441
442 Returns:
443 {"error": "ok"}
444 or
445 {"error": <error message>}
446 """
447 @staticmethod
448 def _get_package(package_id):
449 try:
450 package = CustomImagePackage.objects.get(pk=package_id)
451 return package, None
452 except Package.DoesNotExist:
453 return None, error_response("Package with id=%s "
454 "not found" % package_id)
455
456 def _traverse_dependents(self, next_package_id,
457 rev_deps, all_current_packages, tree_level=0):
458 """
459 Recurse through reverse dependency tree for next_package_id.
460 Limit the reverse dependency search to packages not already scanned,
461 that is, not already in rev_deps.
462 Limit the scan to a depth (tree_level) not exceeding the count of
463 all packages in the custom image, and if that depth is exceeded
464 return False, pop out of the recursion, and write a warning
465 to the log, but this is unlikely, suggesting a dependency loop
466 not caught by bitbake.
467 On return, the input/output arg rev_deps is appended with queryset
468 dictionary elements, annotated for use in the customimage template.
469 The list has unsorted, but unique elements.
470 """
471 max_dependency_tree_depth = all_current_packages.count()
472 if tree_level >= max_dependency_tree_depth:
473 logger.warning(
474 "The number of reverse dependencies "
475 "for this package exceeds " + max_dependency_tree_depth +
476 " and the remaining reverse dependencies will not be removed")
477 return True
478
479 package = CustomImagePackage.objects.get(id=next_package_id)
480 dependents = \
481 package.package_dependencies_target.annotate(
482 name=F('package__name'),
483 pk=F('package__pk'),
484 size=F('package__size'),
485 ).values("name", "pk", "size").exclude(
486 ~Q(pk__in=all_current_packages)
487 )
488
489 for pkg in dependents:
490 if pkg in rev_deps:
491 # already seen, skip dependent search
492 continue
493
494 rev_deps.append(pkg)
495 if (self._traverse_dependents(pkg["pk"], rev_deps,
496 all_current_packages,
497 tree_level+1)):
498 return True
499
500 return False
501
502 def _get_all_dependents(self, package_id, all_current_packages):
503 """
504 Returns sorted list of recursive reverse dependencies for package_id,
505 as a list of dictionary items, by recursing through dependency
506 relationships.
507 """
508 rev_deps = []
509 self._traverse_dependents(package_id, rev_deps, all_current_packages)
510 rev_deps = sorted(rev_deps, key=lambda x: x["name"])
511 return rev_deps
512
513 def get(self, request, *args, **kwargs):
514 recipe, error = XhrCustomRecipeId._get_ci_recipe(
515 kwargs['recipe_id'])
516 if error:
517 return error
518
519 # If no package_id then list all the current packages
520 if not kwargs['package_id']:
521 total_size = 0
522 packages = recipe.get_all_packages().values("id",
523 "name",
524 "version",
525 "size")
526 for package in packages:
527 package['size_formatted'] = \
528 filtered_filesizeformat(package['size'])
529 total_size += package['size']
530
531 return JsonResponse({"error": "ok",
532 "packages": list(packages),
533 "total": len(packages),
534 "total_size": total_size,
535 "total_size_formatted":
536 filtered_filesizeformat(total_size)})
537 else:
538 package, error = XhrCustomRecipePackages._get_package(
539 kwargs['package_id'])
540 if error:
541 return error
542
543 all_current_packages = recipe.get_all_packages()
544
545 # Dependencies for package which aren't satisfied by the
546 # current packages in the custom image recipe
547 deps = package.package_dependencies_source.for_target_or_none(
548 recipe.name)['packages'].annotate(
549 name=F('depends_on__name'),
550 pk=F('depends_on__pk'),
551 size=F('depends_on__size'),
552 ).values("name", "pk", "size").filter(
553 # There are two depends types we don't know why
554 (Q(dep_type=Package_Dependency.TYPE_TRDEPENDS) |
555 Q(dep_type=Package_Dependency.TYPE_RDEPENDS)) &
556 ~Q(pk__in=all_current_packages)
557 )
558
559 # Reverse dependencies which are needed by packages that are
560 # in the image. Recursive search providing all dependents,
561 # not just immediate dependents.
562 reverse_deps = self._get_all_dependents(kwargs['package_id'],
563 all_current_packages)
564 total_size_deps = 0
565 total_size_reverse_deps = 0
566
567 for dep in deps:
568 dep['size_formatted'] = \
569 filtered_filesizeformat(dep['size'])
570 total_size_deps += dep['size']
571
572 for dep in reverse_deps:
573 dep['size_formatted'] = \
574 filtered_filesizeformat(dep['size'])
575 total_size_reverse_deps += dep['size']
576
577 return JsonResponse(
578 {"error": "ok",
579 "id": package.pk,
580 "name": package.name,
581 "version": package.version,
582 "unsatisfied_dependencies": list(deps),
583 "unsatisfied_dependencies_size": total_size_deps,
584 "unsatisfied_dependencies_size_formatted":
585 filtered_filesizeformat(total_size_deps),
586 "reverse_dependencies": list(reverse_deps),
587 "reverse_dependencies_size": total_size_reverse_deps,
588 "reverse_dependencies_size_formatted":
589 filtered_filesizeformat(total_size_reverse_deps)})
590
591 def put(self, request, *args, **kwargs):
592 recipe, error = XhrCustomRecipeId._get_ci_recipe(kwargs['recipe_id'])
593 package, error = self._get_package(kwargs['package_id'])
594 if error:
595 return error
596
597 included_packages = recipe.includes_set.values_list('pk',
598 flat=True)
599
600 # If we're adding back a package which used to be included in this
601 # image all we need to do is remove it from the excludes
602 if package.pk in included_packages:
603 try:
604 recipe.excludes_set.remove(package)
605 return {"error": "ok"}
606 except Package.DoesNotExist:
607 return error_response("Package %s not found in excludes"
608 " but was in included list" %
609 package.name)
610
611 else:
612 recipe.appends_set.add(package)
613 # Make sure that package is not in the excludes set
614 try:
615 recipe.excludes_set.remove(package)
616 except:
617 pass
618 # Add the dependencies we think will be added to the recipe
619 # as a result of appending this package.
620 # TODO this should recurse down the entire deps tree
621 for dep in package.package_dependencies_source.all_depends():
622 try:
623 cust_package = CustomImagePackage.objects.get(
624 name=dep.depends_on.name)
625
626 recipe.includes_set.add(cust_package)
627 try:
628 # When adding the pre-requisite package, make
629 # sure it's not in the excluded list from a
630 # prior removal.
631 recipe.excludes_set.remove(cust_package)
632 except package.DoesNotExist:
633 # Don't care if the package had never been excluded
634 pass
635 except:
636 logger.warning("Could not add package's suggested"
637 "dependencies to the list")
638 return JsonResponse({"error": "ok"})
639
640 def delete(self, request, *args, **kwargs):
641 recipe, error = XhrCustomRecipeId._get_ci_recipe(kwargs['recipe_id'])
642 package, error = self._get_package(kwargs['package_id'])
643 if error:
644 return error
645
646 try:
647 included_packages = recipe.includes_set.values_list('pk',
648 flat=True)
649 # If we're deleting a package which is included we need to
650 # Add it to the excludes list.
651 if package.pk in included_packages:
652 recipe.excludes_set.add(package)
653 else:
654 recipe.appends_set.remove(package)
655 all_current_packages = recipe.get_all_packages()
656
657 reverse_deps_dictlist = self._get_all_dependents(
658 package.pk,
659 all_current_packages)
660
661 ids = [entry['pk'] for entry in reverse_deps_dictlist]
662 reverse_deps = CustomImagePackage.objects.filter(id__in=ids)
663 for r in reverse_deps:
664 try:
665 if r.id in included_packages:
666 recipe.excludes_set.add(r)
667 else:
668 recipe.appends_set.remove(r)
669 except:
670 pass
671
672 return JsonResponse({"error": "ok"})
673 except CustomImageRecipe.DoesNotExist:
674 return error_response("Tried to remove package that wasn't"
675 " present")
676
677
678class XhrProject(View):
679 """ Create, delete or edit a project
680
681 Entry point: /xhr_project/<project_id>
682 """
683 def post(self, request, *args, **kwargs):
684 """
685 Edit project control
686
687 Args:
688 layerAdd = layer_version_id layer_version_id ...
689 layerDel = layer_version_id layer_version_id ...
690 projectName = new_project_name
691 machineName = new_machine_name
692
693 Returns:
694 {"error": "ok"}
695 or
696 {"error": <error message>}
697 """
698 try:
699 prj = Project.objects.get(pk=kwargs['project_id'])
700 except Project.DoesNotExist:
701 return error_response("No such project")
702
703 # Add layers
704 if 'layerAdd' in request.POST and len(request.POST['layerAdd']) > 0:
705 for layer_version_id in request.POST['layerAdd'].split(','):
706 try:
707 lv = Layer_Version.objects.get(pk=int(layer_version_id))
708 ProjectLayer.objects.get_or_create(project=prj,
709 layercommit=lv)
710 except Layer_Version.DoesNotExist:
711 return error_response("Layer version %s asked to add "
712 "doesn't exist" % layer_version_id)
713
714 # Remove layers
715 if 'layerDel' in request.POST and len(request.POST['layerDel']) > 0:
716 layer_version_ids = request.POST['layerDel'].split(',')
717 ProjectLayer.objects.filter(
718 project=prj,
719 layercommit_id__in=layer_version_ids).delete()
720
721 # Project name change
722 if 'projectName' in request.POST:
723 prj.name = request.POST['projectName']
724 prj.save()
725
726 # Machine name change
727 if 'machineName' in request.POST:
728 machinevar = prj.projectvariable_set.get(name="MACHINE")
729 machinevar.value = request.POST['machineName']
730 machinevar.save()
731
732 return JsonResponse({"error": "ok"})
733
734 def get(self, request, *args, **kwargs):
735 """
736 Returns:
737 json object representing the current project
738 or:
739 {"error": <error message>}
740 """
741
742 try:
743 project = Project.objects.get(pk=kwargs['project_id'])
744 except Project.DoesNotExist:
745 return error_response("Project %s does not exist" %
746 kwargs['project_id'])
747
748 # Create the frequently built targets list
749
750 freqtargets = Counter(Target.objects.filter(
751 Q(build__project=project),
752 ~Q(build__outcome=Build.IN_PROGRESS)
753 ).order_by("target").values_list("target", flat=True))
754
755 freqtargets = freqtargets.most_common(5)
756
757 # We now have the targets in order of frequency but if there are two
758 # with the same frequency then we need to make sure those are in
759 # alphabetical order without losing the frequency ordering
760
761 tmp = []
762 switch = None
763 for i, freqtartget in enumerate(freqtargets):
764 target, count = freqtartget
765 try:
766 target_next, count_next = freqtargets[i+1]
767 if count == count_next and target > target_next:
768 switch = target
769 continue
770 except IndexError:
771 pass
772
773 tmp.append(target)
774
775 if switch:
776 tmp.append(switch)
777 switch = None
778
779 freqtargets = tmp
780
781 layers = []
782 for layer in project.projectlayer_set.all():
783 layers.append({
784 "id": layer.layercommit.pk,
785 "name": layer.layercommit.layer.name,
786 "vcs_url": layer.layercommit.layer.vcs_url,
787 "local_source_dir": layer.layercommit.layer.local_source_dir,
788 "vcs_reference": layer.layercommit.get_vcs_reference(),
789 "url": layer.layercommit.layer.layer_index_url,
790 "layerdetailurl": layer.layercommit.get_detailspage_url(
791 project.pk),
792 "layersource": layer.layercommit.layer_source
793 })
794
795 data = {
796 "name": project.name,
797 "layers": layers,
798 "freqtargets": freqtargets,
799 }
800
801 if project.release is not None:
802 data['release'] = {
803 "id": project.release.pk,
804 "name": project.release.name,
805 "description": project.release.description
806 }
807
808 try:
809 data["machine"] = {"name":
810 project.projectvariable_set.get(
811 name="MACHINE").value}
812 except ProjectVariable.DoesNotExist:
813 data["machine"] = None
814 try:
815 data["distro"] = project.projectvariable_set.get(
816 name="DISTRO").value
817 except ProjectVariable.DoesNotExist:
818 data["distro"] = "-- not set yet"
819
820 data['error'] = "ok"
821
822 return JsonResponse(data)
823
824 def put(self, request, *args, **kwargs):
825 # TODO create new project api
826 return HttpResponse()
827
828 def delete(self, request, *args, **kwargs):
829 """Delete a project. Cancels any builds in progress"""
830 try:
831 project = Project.objects.get(pk=kwargs['project_id'])
832 # Cancel any builds in progress
833 for br in BuildRequest.objects.filter(
834 project=project,
835 state=BuildRequest.REQ_INPROGRESS):
836 XhrBuildRequest.cancel_build(br)
837
838 project.delete()
839
840 except Project.DoesNotExist:
841 return error_response("Project %s does not exist" %
842 kwargs['project_id'])
843
844 return JsonResponse({
845 "error": "ok",
846 "gotoUrl": reverse("all-projects", args=[])
847 })
848
849
850class XhrBuild(View):
851 """ Delete a build object
852
853 Entry point: /xhr_build/<build_id>
854 """
855 def delete(self, request, *args, **kwargs):
856 """
857 Delete build data
858
859 Args:
860 build_id = build_id
861
862 Returns:
863 {"error": "ok"}
864 or
865 {"error": <error message>}
866 """
867 try:
868 build = Build.objects.get(pk=kwargs['build_id'])
869 project = build.project
870 build.delete()
871 except Build.DoesNotExist:
872 return error_response("Build %s does not exist" %
873 kwargs['build_id'])
874 return JsonResponse({
875 "error": "ok",
876 "gotoUrl": reverse("projectbuilds", args=(project.pk,))
877 })