summaryrefslogtreecommitdiffstats
path: root/bitbake/lib/toaster/toastergui/api.py
diff options
context:
space:
mode:
authorMichael Wood <michael.g.wood@intel.com>2016-08-22 16:42:33 +0100
committerRichard Purdie <richard.purdie@linuxfoundation.org>2016-09-02 18:09:50 +0100
commit2318f92580eeb7042bf9d4d6dd2f514856b69d4a (patch)
tree335f577e66b538d3de73d7bdd8bd3885a1a7af2f /bitbake/lib/toaster/toastergui/api.py
parent3b87f2895add3944bffa430e209446defed57afa (diff)
downloadpoky-2318f92580eeb7042bf9d4d6dd2f514856b69d4a.tar.gz
bitbake: toaster: Move Custom image recipe rest api to api file
We now have a dedicated file for the rest API so move and rework for class based views. Also clean up all flake8 identified warnings. Remove unused imports from toastergui views. The original work for this API was done by Elliot Smith, Ed Bartosh, Michael Wood and Dave Lerner (Bitbake rev: 37c2b4f105d7334cdd83d9675af787f4327e7fe7) Signed-off-by: Michael Wood <michael.g.wood@intel.com> Signed-off-by: Elliot Smith <elliot.smith@intel.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'bitbake/lib/toaster/toastergui/api.py')
-rw-r--r--bitbake/lib/toaster/toastergui/api.py469
1 files changed, 459 insertions, 10 deletions
diff --git a/bitbake/lib/toaster/toastergui/api.py b/bitbake/lib/toaster/toastergui/api.py
index 09fb02b8fc..be18090daf 100644
--- a/bitbake/lib/toaster/toastergui/api.py
+++ b/bitbake/lib/toaster/toastergui/api.py
@@ -16,21 +16,29 @@
16# with this program; if not, write to the Free Software Foundation, Inc., 16# with this program; if not, write to the Free Software Foundation, Inc.,
17# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 18
19# Please run flake8 on this file before sending patches
19 20
20# Temporary home for the UI's misc API
21import re 21import re
22import logging
22 23
23from orm.models import Project, ProjectTarget, Build, Layer_Version 24from orm.models import Project, ProjectTarget, Build, Layer_Version
24from orm.models import LayerVersionDependency, LayerSource, ProjectLayer 25from orm.models import LayerVersionDependency, LayerSource, ProjectLayer
26from orm.models import Recipe, CustomImageRecipe, CustomImagePackage
27from orm.models import Layer, Target, Package, Package_Dependency
25from bldcontrol.models import BuildRequest 28from bldcontrol.models import BuildRequest
26from bldcontrol import bbcontroller 29from bldcontrol import bbcontroller
30
27from django.http import HttpResponse, JsonResponse 31from django.http import HttpResponse, JsonResponse
28from django.views.generic import View 32from django.views.generic import View
29from django.core.urlresolvers import reverse 33from django.core.urlresolvers import reverse
30from django.core import serializers
31from django.utils import timezone 34from django.utils import timezone
32from django.template.defaultfilters import date 35from django.db.models import Q, F
36from django.db import Error
33from toastergui.templatetags.projecttags import json, sectohms, get_tasks 37from toastergui.templatetags.projecttags import json, sectohms, get_tasks
38from toastergui.templatetags.projecttags import filtered_filesizeformat
39
40logger = logging.getLogger("toaster")
41
34 42
35def error_response(error): 43def error_response(error):
36 return JsonResponse({"error": error}) 44 return JsonResponse({"error": error})
@@ -216,6 +224,7 @@ class XhrLayer(View):
216 "redirect": reverse('project', args=(kwargs['pid'],)) 224 "redirect": reverse('project', args=(kwargs['pid'],))
217 }) 225 })
218 226
227
219class MostRecentBuildsView(View): 228class MostRecentBuildsView(View):
220 def _was_yesterday_or_earlier(self, completed_on): 229 def _was_yesterday_or_earlier(self, completed_on):
221 now = timezone.now() 230 now = timezone.now()
@@ -230,13 +239,11 @@ class MostRecentBuildsView(View):
230 """ 239 """
231 Returns a list of builds in JSON format. 240 Returns a list of builds in JSON format.
232 """ 241 """
233 mrb_type = 'all'
234 project = None 242 project = None
235 243
236 project_id = request.GET.get('project_id', None) 244 project_id = request.GET.get('project_id', None)
237 if project_id: 245 if project_id:
238 try: 246 try:
239 mrb_type = 'project'
240 project = Project.objects.get(pk=project_id) 247 project = Project.objects.get(pk=project_id)
241 except: 248 except:
242 # if project lookup fails, assume no project 249 # if project lookup fails, assume no project
@@ -245,9 +252,6 @@ class MostRecentBuildsView(View):
245 recent_build_objs = Build.get_recent(project) 252 recent_build_objs = Build.get_recent(project)
246 recent_builds = [] 253 recent_builds = []
247 254
248 # for timezone conversion
249 tz = timezone.get_current_timezone()
250
251 for build_obj in recent_build_objs: 255 for build_obj in recent_build_objs:
252 dashboard_url = reverse('builddashboard', args=(build_obj.pk,)) 256 dashboard_url = reverse('builddashboard', args=(build_obj.pk,))
253 buildtime_url = reverse('buildtime', args=(build_obj.pk,)) 257 buildtime_url = reverse('buildtime', args=(build_obj.pk,))
@@ -266,7 +270,8 @@ class MostRecentBuildsView(View):
266 build['buildrequest_id'] = buildrequest_id 270 build['buildrequest_id'] = buildrequest_id
267 271
268 build['recipes_parsed_percentage'] = \ 272 build['recipes_parsed_percentage'] = \
269 int((build_obj.recipes_parsed / build_obj.recipes_to_parse) * 100) 273 int((build_obj.recipes_parsed /
274 build_obj.recipes_to_parse) * 100)
270 275
271 tasks_complete_percentage = 0 276 tasks_complete_percentage = 0
272 if build_obj.outcome in (Build.SUCCEEDED, Build.FAILED): 277 if build_obj.outcome in (Build.SUCCEEDED, Build.FAILED):
@@ -300,7 +305,8 @@ class MostRecentBuildsView(View):
300 completed_on_template = '%H:%M' 305 completed_on_template = '%H:%M'
301 if self._was_yesterday_or_earlier(completed_on): 306 if self._was_yesterday_or_earlier(completed_on):
302 completed_on_template = '%d/%m/%Y ' + completed_on_template 307 completed_on_template = '%d/%m/%Y ' + completed_on_template
303 build['completed_on'] = completed_on.strftime(completed_on_template) 308 build['completed_on'] = completed_on.strftime(
309 completed_on_template)
304 310
305 targets = [] 311 targets = []
306 target_objs = build_obj.get_sorted_target_list() 312 target_objs = build_obj.get_sorted_target_list()
@@ -323,3 +329,446 @@ class MostRecentBuildsView(View):
323 recent_builds.append(build) 329 recent_builds.append(build)
324 330
325 return JsonResponse(recent_builds, safe=False) 331 return JsonResponse(recent_builds, safe=False)
332
333
334class XhrCustomRecipe(View):
335 """ Create a custom image recipe """
336
337 def post(self, request, *args, **kwargs):
338 """
339 Custom image recipe REST API
340
341 Entry point: /xhr_customrecipe/
342 Method: POST
343
344 Args:
345 name: name of custom recipe to create
346 project: target project id of orm.models.Project
347 base: base recipe id of orm.models.Recipe
348
349 Returns:
350 {"error": "ok",
351 "url": <url of the created recipe>}
352 or
353 {"error": <error message>}
354 """
355 # check if request has all required parameters
356 for param in ('name', 'project', 'base'):
357 if param not in request.POST:
358 return error_response("Missing parameter '%s'" % param)
359
360 # get project and baserecipe objects
361 params = {}
362 for name, model in [("project", Project),
363 ("base", Recipe)]:
364 value = request.POST[name]
365 try:
366 params[name] = model.objects.get(id=value)
367 except model.DoesNotExist:
368 return error_response("Invalid %s id %s" % (name, value))
369
370 # create custom recipe
371 try:
372
373 # Only allowed chars in name are a-z, 0-9 and -
374 if re.search(r'[^a-z|0-9|-]', request.POST["name"]):
375 return error_response("invalid-name")
376
377 custom_images = CustomImageRecipe.objects.all()
378
379 # Are there any recipes with this name already in our project?
380 existing_image_recipes_in_project = custom_images.filter(
381 name=request.POST["name"], project=params["project"])
382
383 if existing_image_recipes_in_project.count() > 0:
384 return error_response("image-already-exists")
385
386 # Are there any recipes with this name which aren't custom
387 # image recipes?
388 custom_image_ids = custom_images.values_list('id', flat=True)
389 existing_non_image_recipes = Recipe.objects.filter(
390 Q(name=request.POST["name"]) & ~Q(pk__in=custom_image_ids)
391 )
392
393 if existing_non_image_recipes.count() > 0:
394 return error_response("recipe-already-exists")
395
396 # create layer 'Custom layer' and verion if needed
397 layer = Layer.objects.get_or_create(
398 name=CustomImageRecipe.LAYER_NAME,
399 summary="Layer for custom recipes",
400 vcs_url="file:///toaster_created_layer")[0]
401
402 # Check if we have a layer version already
403 # We don't use get_or_create here because the dirpath will change
404 # and is a required field
405 lver = Layer_Version.objects.filter(Q(project=params['project']) &
406 Q(layer=layer) &
407 Q(build=None)).last()
408 if lver is None:
409 lver, created = Layer_Version.objects.get_or_create(
410 project=params['project'],
411 layer=layer,
412 dirpath="toaster_created_layer")
413
414 # Add a dependency on our layer to the base recipe's layer
415 LayerVersionDependency.objects.get_or_create(
416 layer_version=lver,
417 depends_on=params["base"].layer_version)
418
419 # Add it to our current project if needed
420 ProjectLayer.objects.get_or_create(project=params['project'],
421 layercommit=lver,
422 optional=False)
423
424 # Create the actual recipe
425 recipe, created = CustomImageRecipe.objects.get_or_create(
426 name=request.POST["name"],
427 base_recipe=params["base"],
428 project=params["project"],
429 layer_version=lver,
430 is_image=True)
431
432 # If we created the object then setup these fields. They may get
433 # overwritten later on and cause the get_or_create to create a
434 # duplicate if they've changed.
435 if created:
436 recipe.file_path = request.POST["name"]
437 recipe.license = "MIT"
438 recipe.version = "0.1"
439 recipe.save()
440
441 except Error as err:
442 return error_response("Can't create custom recipe: %s" % err)
443
444 # Find the package list from the last build of this recipe/target
445 target = Target.objects.filter(Q(build__outcome=Build.SUCCEEDED) &
446 Q(build__project=params['project']) &
447 (Q(target=params['base'].name) |
448 Q(target=recipe.name))).last()
449 if target:
450 # Copy in every package
451 # We don't want these packages to be linked to anything because
452 # that underlying data may change e.g. delete a build
453 for tpackage in target.target_installed_package_set.all():
454 try:
455 built_package = tpackage.package
456 # The package had no recipe information so is a ghost
457 # package skip it
458 if built_package.recipe is None:
459 continue
460
461 config_package = CustomImagePackage.objects.get(
462 name=built_package.name)
463
464 recipe.includes_set.add(config_package)
465 except Exception as e:
466 logger.warning("Error adding package %s %s" %
467 (tpackage.package.name, e))
468 pass
469
470 return JsonResponse(
471 {"error": "ok",
472 "packages": recipe.get_all_packages().count(),
473 "url": reverse('customrecipe', args=(params['project'].pk,
474 recipe.id))})
475
476
477class XhrCustomRecipeId(View):
478 """
479 Set of ReST API processors working with recipe id.
480
481 Entry point: /xhr_customrecipe/<recipe_id>
482
483 Methods:
484 GET - Get details of custom image recipe
485 DELETE - Delete custom image recipe
486
487 Returns:
488 GET:
489 {"error": "ok",
490 "info": dictionary of field name -> value pairs
491 of the CustomImageRecipe model}
492 DELETE:
493 {"error": "ok"}
494 or
495 {"error": <error message>}
496 """
497 @staticmethod
498 def _get_ci_recipe(recipe_id):
499 """ Get Custom Image recipe or return an error response"""
500 try:
501 custom_recipe = \
502 CustomImageRecipe.objects.get(pk=recipe_id)
503 return custom_recipe, None
504
505 except CustomImageRecipe.DoesNotExist:
506 return None, error_response("Custom recipe with id=%s "
507 "not found" % recipe_id)
508
509 def get(self, request, *args, **kwargs):
510 custom_recipe, error = self._get_ci_recipe(kwargs['recipe_id'])
511 if error:
512 return error
513
514 if request.method == 'GET':
515 info = {"id": custom_recipe.id,
516 "name": custom_recipe.name,
517 "base_recipe_id": custom_recipe.base_recipe.id,
518 "project_id": custom_recipe.project.id}
519
520 return JsonResponse({"error": "ok", "info": info})
521
522 def delete(self, request, *args, **kwargs):
523 custom_recipe, error = self._get_ci_recipe(kwargs['recipe_id'])
524 if error:
525 return error
526
527 custom_recipe.delete()
528 return JsonResponse({"error": "ok"})
529
530
531class XhrCustomRecipePackages(View):
532 """
533 ReST API to add/remove packages to/from custom recipe.
534
535 Entry point: /xhr_customrecipe/<recipe_id>/packages/<package_id>
536 Methods:
537 PUT - Add package to the recipe
538 DELETE - Delete package from the recipe
539 GET - Get package information
540
541 Returns:
542 {"error": "ok"}
543 or
544 {"error": <error message>}
545 """
546 @staticmethod
547 def _get_package(package_id):
548 try:
549 package = CustomImagePackage.objects.get(pk=package_id)
550 return package, None
551 except Package.DoesNotExist:
552 return None, error_response("Package with id=%s "
553 "not found" % package_id)
554
555 def _traverse_dependents(self, next_package_id,
556 rev_deps, all_current_packages, tree_level=0):
557 """
558 Recurse through reverse dependency tree for next_package_id.
559 Limit the reverse dependency search to packages not already scanned,
560 that is, not already in rev_deps.
561 Limit the scan to a depth (tree_level) not exceeding the count of
562 all packages in the custom image, and if that depth is exceeded
563 return False, pop out of the recursion, and write a warning
564 to the log, but this is unlikely, suggesting a dependency loop
565 not caught by bitbake.
566 On return, the input/output arg rev_deps is appended with queryset
567 dictionary elements, annotated for use in the customimage template.
568 The list has unsorted, but unique elements.
569 """
570 max_dependency_tree_depth = all_current_packages.count()
571 if tree_level >= max_dependency_tree_depth:
572 logger.warning(
573 "The number of reverse dependencies "
574 "for this package exceeds " + max_dependency_tree_depth +
575 " and the remaining reverse dependencies will not be removed")
576 return True
577
578 package = CustomImagePackage.objects.get(id=next_package_id)
579 dependents = \
580 package.package_dependencies_target.annotate(
581 name=F('package__name'),
582 pk=F('package__pk'),
583 size=F('package__size'),
584 ).values("name", "pk", "size").exclude(
585 ~Q(pk__in=all_current_packages)
586 )
587
588 for pkg in dependents:
589 if pkg in rev_deps:
590 # already seen, skip dependent search
591 continue
592
593 rev_deps.append(pkg)
594 if (self._traverse_dependents(pkg["pk"], rev_deps,
595 all_current_packages,
596 tree_level+1)):
597 return True
598
599 return False
600
601 def _get_all_dependents(self, package_id, all_current_packages):
602 """
603 Returns sorted list of recursive reverse dependencies for package_id,
604 as a list of dictionary items, by recursing through dependency
605 relationships.
606 """
607 rev_deps = []
608 self._traverse_dependents(package_id, rev_deps, all_current_packages)
609 rev_deps = sorted(rev_deps, key=lambda x: x["name"])
610 return rev_deps
611
612 def get(self, request, *args, **kwargs):
613 recipe, error = XhrCustomRecipeId._get_ci_recipe(
614 kwargs['recipe_id'])
615 if error:
616 return error
617
618 # If no package_id then list all the current packages
619 if not kwargs['package_id']:
620 total_size = 0
621 packages = recipe.get_all_packages().values("id",
622 "name",
623 "version",
624 "size")
625 for package in packages:
626 package['size_formatted'] = \
627 filtered_filesizeformat(package['size'])
628 total_size += package['size']
629
630 return JsonResponse({"error": "ok",
631 "packages": list(packages),
632 "total": len(packages),
633 "total_size": total_size,
634 "total_size_formatted":
635 filtered_filesizeformat(total_size)})
636 else:
637 package, error = XhrCustomRecipePackages._get_package(
638 kwargs['package_id'])
639 if error:
640 return error
641
642 all_current_packages = recipe.get_all_packages()
643
644 # Dependencies for package which aren't satisfied by the
645 # current packages in the custom image recipe
646 deps = package.package_dependencies_source.for_target_or_none(
647 recipe.name)['packages'].annotate(
648 name=F('depends_on__name'),
649 pk=F('depends_on__pk'),
650 size=F('depends_on__size'),
651 ).values("name", "pk", "size").filter(
652 # There are two depends types we don't know why
653 (Q(dep_type=Package_Dependency.TYPE_TRDEPENDS) |
654 Q(dep_type=Package_Dependency.TYPE_RDEPENDS)) &
655 ~Q(pk__in=all_current_packages)
656 )
657
658 # Reverse dependencies which are needed by packages that are
659 # in the image. Recursive search providing all dependents,
660 # not just immediate dependents.
661 reverse_deps = self._get_all_dependents(kwargs['package_id'],
662 all_current_packages)
663 total_size_deps = 0
664 total_size_reverse_deps = 0
665
666 for dep in deps:
667 dep['size_formatted'] = \
668 filtered_filesizeformat(dep['size'])
669 total_size_deps += dep['size']
670
671 for dep in reverse_deps:
672 dep['size_formatted'] = \
673 filtered_filesizeformat(dep['size'])
674 total_size_reverse_deps += dep['size']
675
676 return JsonResponse(
677 {"error": "ok",
678 "id": package.pk,
679 "name": package.name,
680 "version": package.version,
681 "unsatisfied_dependencies": list(deps),
682 "unsatisfied_dependencies_size": total_size_deps,
683 "unsatisfied_dependencies_size_formatted":
684 filtered_filesizeformat(total_size_deps),
685 "reverse_dependencies": list(reverse_deps),
686 "reverse_dependencies_size": total_size_reverse_deps,
687 "reverse_dependencies_size_formatted":
688 filtered_filesizeformat(total_size_reverse_deps)})
689
690 def put(self, request, *args, **kwargs):
691 recipe, error = XhrCustomRecipeId._get_ci_recipe(kwargs['recipe_id'])
692 package, error = self._get_package(kwargs['package_id'])
693 if error:
694 return error
695
696 included_packages = recipe.includes_set.values_list('pk',
697 flat=True)
698
699 # If we're adding back a package which used to be included in this
700 # image all we need to do is remove it from the excludes
701 if package.pk in included_packages:
702 try:
703 recipe.excludes_set.remove(package)
704 return {"error": "ok"}
705 except Package.DoesNotExist:
706 return error_response("Package %s not found in excludes"
707 " but was in included list" %
708 package.name)
709
710 else:
711 recipe.appends_set.add(package)
712 # Make sure that package is not in the excludes set
713 try:
714 recipe.excludes_set.remove(package)
715 except:
716 pass
717 # Add the dependencies we think will be added to the recipe
718 # as a result of appending this package.
719 # TODO this should recurse down the entire deps tree
720 for dep in package.package_dependencies_source.all_depends():
721 try:
722 cust_package = CustomImagePackage.objects.get(
723 name=dep.depends_on.name)
724
725 recipe.includes_set.add(cust_package)
726 try:
727 # When adding the pre-requisite package, make
728 # sure it's not in the excluded list from a
729 # prior removal.
730 recipe.excludes_set.remove(cust_package)
731 except package.DoesNotExist:
732 # Don't care if the package had never been excluded
733 pass
734 except:
735 logger.warning("Could not add package's suggested"
736 "dependencies to the list")
737 return JsonResponse({"error": "ok"})
738
739 def delete(self, request, *args, **kwargs):
740 recipe, error = XhrCustomRecipeId._get_ci_recipe(kwargs['recipe_id'])
741 package, error = self._get_package(kwargs['package_id'])
742 if error:
743 return error
744
745 try:
746 included_packages = recipe.includes_set.values_list('pk',
747 flat=True)
748 # If we're deleting a package which is included we need to
749 # Add it to the excludes list.
750 if package.pk in included_packages:
751 recipe.excludes_set.add(package)
752 else:
753 recipe.appends_set.remove(package)
754 all_current_packages = recipe.get_all_packages()
755
756 reverse_deps_dictlist = self._get_all_dependents(
757 package.pk,
758 all_current_packages)
759
760 ids = [entry['pk'] for entry in reverse_deps_dictlist]
761 reverse_deps = CustomImagePackage.objects.filter(id__in=ids)
762 for r in reverse_deps:
763 try:
764 if r.id in included_packages:
765 recipe.excludes_set.add(r)
766 else:
767 recipe.appends_set.remove(r)
768 except:
769 pass
770
771 return JsonResponse({"error": "ok"})
772 except CustomImageRecipe.DoesNotExist:
773 return error_response("Tried to remove package that wasn't"
774 " present")