summaryrefslogtreecommitdiffstats
path: root/bitbake
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
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')
-rw-r--r--bitbake/lib/toaster/toastergui/api.py469
-rw-r--r--bitbake/lib/toaster/toastergui/urls.py18
-rwxr-xr-xbitbake/lib/toaster/toastergui/views.py447
3 files changed, 482 insertions, 452 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")
diff --git a/bitbake/lib/toaster/toastergui/urls.py b/bitbake/lib/toaster/toastergui/urls.py
index 9892d2ab93..9509cd5928 100644
--- a/bitbake/lib/toaster/toastergui/urls.py
+++ b/bitbake/lib/toaster/toastergui/urls.py
@@ -199,19 +199,25 @@ urlpatterns = patterns('toastergui.views',
199 url(r'^js-unit-tests/$', 'jsunittests', name='js-unit-tests'), 199 url(r'^js-unit-tests/$', 'jsunittests', name='js-unit-tests'),
200 200
201 # image customisation functionality 201 # image customisation functionality
202 url(r'^xhr_customrecipe/(?P<recipe_id>\d+)/packages/(?P<package_id>\d+|)$', 202 url(r'^xhr_customrecipe/(?P<recipe_id>\d+)'
203 'xhr_customrecipe_packages', name='xhr_customrecipe_packages'), 203 '/packages/(?P<package_id>\d+|)$',
204 api.XhrCustomRecipePackages.as_view(),
205 name='xhr_customrecipe_packages'),
204 206
205 url(r'^xhr_customrecipe/(?P<recipe_id>\d+)/packages/$', 207 url(r'^xhr_customrecipe/(?P<recipe_id>\d+)/packages/$',
206 'xhr_customrecipe_packages', name='xhr_customrecipe_packages'), 208 api.XhrCustomRecipePackages.as_view(),
209 name='xhr_customrecipe_packages'),
207 210
208 url(r'^xhr_customrecipe/(?P<recipe_id>\d+)$', 'xhr_customrecipe_id', 211 url(r'^xhr_customrecipe/(?P<recipe_id>\d+)$',
212 api.XhrCustomRecipeId.as_view(),
209 name='xhr_customrecipe_id'), 213 name='xhr_customrecipe_id'),
210 url(r'^xhr_customrecipe/', 'xhr_customrecipe', 214
215 url(r'^xhr_customrecipe/',
216 api.XhrCustomRecipe.as_view(),
211 name='xhr_customrecipe'), 217 name='xhr_customrecipe'),
212 218
213 url(r'^xhr_buildrequest/project/(?P<pid>\d+)$', 219 url(r'^xhr_buildrequest/project/(?P<pid>\d+)$',
214 api.XhrBuildRequest.as_view(), 220 api.XhrBuildRequest.as_view(),
215 name='xhr_buildrequest'), 221 name='xhr_buildrequest'),
216 222
217 url(r'^mostrecentbuilds$', api.MostRecentBuildsView.as_view(), 223 url(r'^mostrecentbuilds$', api.MostRecentBuildsView.as_view(),
diff --git a/bitbake/lib/toaster/toastergui/views.py b/bitbake/lib/toaster/toastergui/views.py
index 940ea255fd..365a1e88ff 100755
--- a/bitbake/lib/toaster/toastergui/views.py
+++ b/bitbake/lib/toaster/toastergui/views.py
@@ -19,43 +19,37 @@
19# with this program; if not, write to the Free Software Foundation, Inc., 19# with this program; if not, write to the Free Software Foundation, Inc.,
20# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 21
22# pylint: disable=method-hidden
23# Gives E:848, 4: An attribute defined in json.encoder line 162 hides this method (method-hidden)
24# which is an invalid warning
25 22
26import operator,re 23import re
27 24
28from django.db.models import F, Q, Sum, Count, Max 25from django.db.models import F, Q, Sum
29from django.db import IntegrityError, Error 26from django.db import IntegrityError
30from django.shortcuts import render, redirect, get_object_or_404 27from django.shortcuts import render, redirect, get_object_or_404
31from orm.models import Build, Target, Task, Layer, Layer_Version, Recipe, LogMessage, Variable 28from orm.models import Build, Target, Task, Layer, Layer_Version, Recipe
32from orm.models import Task_Dependency, Recipe_Dependency, Package, Package_File, Package_Dependency 29from orm.models import LogMessage, Variable, Package_Dependency, Package
33from orm.models import Target_Installed_Package, Target_File, Target_Image_File, CustomImagePackage 30from orm.models import Task_Dependency, Package_File
34from orm.models import TargetKernelFile, TargetSDKFile 31from orm.models import Target_Installed_Package, Target_File
32from orm.models import TargetKernelFile, TargetSDKFile, Target_Image_File
35from orm.models import BitbakeVersion, CustomImageRecipe 33from orm.models import BitbakeVersion, CustomImageRecipe
36from bldcontrol import bbcontroller 34
37from django.views.decorators.cache import cache_control
38from django.core.urlresolvers import reverse, resolve 35from django.core.urlresolvers import reverse, resolve
39from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist 36from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
40from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger 37from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
41from django.http import HttpResponseBadRequest, HttpResponseNotFound 38from django.http import HttpResponseNotFound
42from django.utils import timezone 39from django.utils import timezone
43from django.utils.html import escape
44from datetime import timedelta, datetime 40from datetime import timedelta, datetime
45from django.utils import formats
46from toastergui.templatetags.projecttags import json as jsonfilter 41from toastergui.templatetags.projecttags import json as jsonfilter
47from decimal import Decimal 42from decimal import Decimal
48import json 43import json
49import os 44import os
50from os.path import dirname 45from os.path import dirname
51from functools import wraps
52import itertools
53import mimetypes 46import mimetypes
54 47
55import logging 48import logging
56 49
57logger = logging.getLogger("toaster") 50logger = logging.getLogger("toaster")
58 51
52
59class MimeTypeFinder(object): 53class MimeTypeFinder(object):
60 # setting this to False enables additional non-standard mimetypes 54 # setting this to False enables additional non-standard mimetypes
61 # to be included in the guess 55 # to be included in the guess
@@ -1498,18 +1492,6 @@ if True:
1498 1492
1499 return context 1493 return context
1500 1494
1501 def xhr_response(fun):
1502 """
1503 Decorator for REST methods.
1504 calls jsonfilter on the returned dictionary and returns result
1505 as HttpResponse object of content_type application/json
1506 """
1507 @wraps(fun)
1508 def wrapper(*args, **kwds):
1509 return HttpResponse(jsonfilter(fun(*args, **kwds)),
1510 content_type="application/json")
1511 return wrapper
1512
1513 def jsunittests(request): 1495 def jsunittests(request):
1514 """ Provides a page for the js unit tests """ 1496 """ Provides a page for the js unit tests """
1515 bbv = BitbakeVersion.objects.filter(branch="master").first() 1497 bbv = BitbakeVersion.objects.filter(branch="master").first()
@@ -1767,187 +1749,6 @@ if True:
1767 1749
1768 return HttpResponse(jsonfilter(json_response), content_type = "application/json") 1750 return HttpResponse(jsonfilter(json_response), content_type = "application/json")
1769 1751
1770 @xhr_response
1771 def xhr_customrecipe(request):
1772 """
1773 Custom image recipe REST API
1774
1775 Entry point: /xhr_customrecipe/
1776 Method: POST
1777
1778 Args:
1779 name: name of custom recipe to create
1780 project: target project id of orm.models.Project
1781 base: base recipe id of orm.models.Recipe
1782
1783 Returns:
1784 {"error": "ok",
1785 "url": <url of the created recipe>}
1786 or
1787 {"error": <error message>}
1788 """
1789 # check if request has all required parameters
1790 for param in ('name', 'project', 'base'):
1791 if param not in request.POST:
1792 return {"error": "Missing parameter '%s'" % param}
1793
1794 # get project and baserecipe objects
1795 params = {}
1796 for name, model in [("project", Project),
1797 ("base", Recipe)]:
1798 value = request.POST[name]
1799 try:
1800 params[name] = model.objects.get(id=value)
1801 except model.DoesNotExist:
1802 return {"error": "Invalid %s id %s" % (name, value)}
1803
1804 # create custom recipe
1805 try:
1806
1807 # Only allowed chars in name are a-z, 0-9 and -
1808 if re.search(r'[^a-z|0-9|-]', request.POST["name"]):
1809 return {"error": "invalid-name"}
1810
1811 custom_images = CustomImageRecipe.objects.all()
1812
1813 # Are there any recipes with this name already in our project?
1814 existing_image_recipes_in_project = custom_images.filter(
1815 name=request.POST["name"], project=params["project"])
1816
1817 if existing_image_recipes_in_project.count() > 0:
1818 return {"error": "image-already-exists"}
1819
1820 # Are there any recipes with this name which aren't custom
1821 # image recipes?
1822 custom_image_ids = custom_images.values_list('id', flat=True)
1823 existing_non_image_recipes = Recipe.objects.filter(
1824 Q(name=request.POST["name"]) & ~Q(pk__in=custom_image_ids)
1825 )
1826
1827 if existing_non_image_recipes.count() > 0:
1828 return {"error": "recipe-already-exists"}
1829
1830 # create layer 'Custom layer' and verion if needed
1831 layer = Layer.objects.get_or_create(
1832 name=CustomImageRecipe.LAYER_NAME,
1833 summary="Layer for custom recipes",
1834 vcs_url="file:///toaster_created_layer")[0]
1835
1836 # Check if we have a layer version already
1837 # We don't use get_or_create here because the dirpath will change
1838 # and is a required field
1839 lver = Layer_Version.objects.filter(Q(project=params['project']) &
1840 Q(layer=layer) &
1841 Q(build=None)).last()
1842 if lver == None:
1843 lver, created = Layer_Version.objects.get_or_create(
1844 project=params['project'],
1845 layer=layer,
1846 dirpath="toaster_created_layer")
1847
1848 # Add a dependency on our layer to the base recipe's layer
1849 LayerVersionDependency.objects.get_or_create(
1850 layer_version=lver,
1851 depends_on=params["base"].layer_version)
1852
1853 # Add it to our current project if needed
1854 ProjectLayer.objects.get_or_create(project=params['project'],
1855 layercommit=lver,
1856 optional=False)
1857
1858 # Create the actual recipe
1859 recipe, created = CustomImageRecipe.objects.get_or_create(
1860 name=request.POST["name"],
1861 base_recipe=params["base"],
1862 project=params["project"],
1863 layer_version=lver,
1864 is_image=True)
1865
1866 # If we created the object then setup these fields. They may get
1867 # overwritten later on and cause the get_or_create to create a
1868 # duplicate if they've changed.
1869 if created:
1870 recipe.file_path = request.POST["name"]
1871 recipe.license = "MIT"
1872 recipe.version = "0.1"
1873 recipe.save()
1874
1875 except Error as err:
1876 return {"error": "Can't create custom recipe: %s" % err}
1877
1878 # Find the package list from the last build of this recipe/target
1879 target = Target.objects.filter(Q(build__outcome=Build.SUCCEEDED) &
1880 Q(build__project=params['project']) &
1881 (Q(target=params['base'].name) |
1882 Q(target=recipe.name))).last()
1883 if target:
1884 # Copy in every package
1885 # We don't want these packages to be linked to anything because
1886 # that underlying data may change e.g. delete a build
1887 for tpackage in target.target_installed_package_set.all():
1888 try:
1889 built_package = tpackage.package
1890 # The package had no recipe information so is a ghost
1891 # package skip it
1892 if built_package.recipe == None:
1893 continue;
1894
1895 config_package = CustomImagePackage.objects.get(
1896 name=built_package.name)
1897
1898 recipe.includes_set.add(config_package)
1899 except Exception as e:
1900 logger.warning("Error adding package %s %s" %
1901 (tpackage.package.name, e))
1902 pass
1903
1904 return {"error": "ok",
1905 "packages" : recipe.get_all_packages().count(),
1906 "url": reverse('customrecipe', args=(params['project'].pk,
1907 recipe.id))}
1908
1909 @xhr_response
1910 def xhr_customrecipe_id(request, recipe_id):
1911 """
1912 Set of ReST API processors working with recipe id.
1913
1914 Entry point: /xhr_customrecipe/<recipe_id>
1915
1916 Methods:
1917 GET - Get details of custom image recipe
1918 DELETE - Delete custom image recipe
1919
1920 Returns:
1921 GET:
1922 {"error": "ok",
1923 "info": dictionary of field name -> value pairs
1924 of the CustomImageRecipe model}
1925 DELETE:
1926 {"error": "ok"}
1927 or
1928 {"error": <error message>}
1929 """
1930 try:
1931 custom_recipe = CustomImageRecipe.objects.get(id=recipe_id)
1932 except CustomImageRecipe.DoesNotExist:
1933 return {"error": "Custom recipe with id=%s "
1934 "not found" % recipe_id}
1935
1936 if request.method == 'GET':
1937 info = {"id" : custom_recipe.id,
1938 "name" : custom_recipe.name,
1939 "base_recipe_id": custom_recipe.base_recipe.id,
1940 "project_id": custom_recipe.project.id,
1941 }
1942
1943 return {"error": "ok", "info": info}
1944
1945 elif request.method == 'DELETE':
1946 custom_recipe.delete()
1947 return {"error": "ok"}
1948 else:
1949 return {"error": "Method %s is not supported" % request.method}
1950
1951 def customrecipe_download(request, pid, recipe_id): 1752 def customrecipe_download(request, pid, recipe_id):
1952 recipe = get_object_or_404(CustomImageRecipe, pk=recipe_id) 1753 recipe = get_object_or_404(CustomImageRecipe, pk=recipe_id)
1953 1754
@@ -1960,232 +1761,6 @@ if True:
1960 1761
1961 return response 1762 return response
1962 1763
1963 def _traverse_dependents(next_package_id, rev_deps, all_current_packages, tree_level=0):
1964 """
1965 Recurse through reverse dependency tree for next_package_id.
1966 Limit the reverse dependency search to packages not already scanned,
1967 that is, not already in rev_deps.
1968 Limit the scan to a depth (tree_level) not exceeding the count of
1969 all packages in the custom image, and if that depth is exceeded
1970 return False, pop out of the recursion, and write a warning
1971 to the log, but this is unlikely, suggesting a dependency loop
1972 not caught by bitbake.
1973 On return, the input/output arg rev_deps is appended with queryset
1974 dictionary elements, annotated for use in the customimage template.
1975 The list has unsorted, but unique elements.
1976 """
1977 max_dependency_tree_depth = all_current_packages.count()
1978 if tree_level >= max_dependency_tree_depth:
1979 logger.warning(
1980 "The number of reverse dependencies "
1981 "for this package exceeds " + max_dependency_tree_depth +
1982 " and the remaining reverse dependencies will not be removed")
1983 return True
1984
1985 package = CustomImagePackage.objects.get(id=next_package_id)
1986 dependents = \
1987 package.package_dependencies_target.annotate(
1988 name=F('package__name'),
1989 pk=F('package__pk'),
1990 size=F('package__size'),
1991 ).values("name", "pk", "size").exclude(
1992 ~Q(pk__in=all_current_packages)
1993 )
1994
1995 for pkg in dependents:
1996 if pkg in rev_deps:
1997 # already seen, skip dependent search
1998 continue
1999
2000 rev_deps.append(pkg)
2001 if (_traverse_dependents(
2002 pkg["pk"], rev_deps, all_current_packages, tree_level+1)):
2003 return True
2004
2005 return False
2006
2007 def _get_all_dependents(package_id, all_current_packages):
2008 """
2009 Returns sorted list of recursive reverse dependencies for package_id,
2010 as a list of dictionary items, by recursing through dependency
2011 relationships.
2012 """
2013 rev_deps = []
2014 _traverse_dependents(package_id, rev_deps, all_current_packages)
2015 rev_deps = sorted(rev_deps, key=lambda x: x["name"])
2016 return rev_deps
2017
2018 @xhr_response
2019 def xhr_customrecipe_packages(request, recipe_id, package_id):
2020 """
2021 ReST API to add/remove packages to/from custom recipe.
2022
2023 Entry point: /xhr_customrecipe/<recipe_id>/packages/<package_id>
2024
2025 Methods:
2026 PUT - Add package to the recipe
2027 DELETE - Delete package from the recipe
2028 GET - Get package information
2029
2030 Returns:
2031 {"error": "ok"}
2032 or
2033 {"error": <error message>}
2034 """
2035 try:
2036 recipe = CustomImageRecipe.objects.get(id=recipe_id)
2037 except CustomImageRecipe.DoesNotExist:
2038 return {"error": "Custom recipe with id=%s "
2039 "not found" % recipe_id}
2040
2041 if package_id:
2042 try:
2043 package = CustomImagePackage.objects.get(id=package_id)
2044 except Package.DoesNotExist:
2045 return {"error": "Package with id=%s "
2046 "not found" % package_id}
2047
2048 if request.method == 'GET':
2049 # If no package_id then list the current packages
2050 if not package_id:
2051 total_size = 0
2052 packages = recipe.get_all_packages().values("id",
2053 "name",
2054 "version",
2055 "size")
2056 for package in packages:
2057 package['size_formatted'] = \
2058 filtered_filesizeformat(package['size'])
2059 total_size += package['size']
2060
2061 return {"error": "ok",
2062 "packages" : list(packages),
2063 "total" : len(packages),
2064 "total_size" : total_size,
2065 "total_size_formatted" :
2066 filtered_filesizeformat(total_size)
2067 }
2068 else:
2069 all_current_packages = recipe.get_all_packages()
2070
2071 # Dependencies for package which aren't satisfied by the
2072 # current packages in the custom image recipe
2073 deps =\
2074 package.package_dependencies_source.for_target_or_none(
2075 recipe.name)['packages'].annotate(
2076 name=F('depends_on__name'),
2077 pk=F('depends_on__pk'),
2078 size=F('depends_on__size'),
2079 ).values("name", "pk", "size").filter(
2080 # There are two depends types we don't know why
2081 (Q(dep_type=Package_Dependency.TYPE_TRDEPENDS) |
2082 Q(dep_type=Package_Dependency.TYPE_RDEPENDS)) &
2083 ~Q(pk__in=all_current_packages)
2084 )
2085
2086 # Reverse dependencies which are needed by packages that are
2087 # in the image. Recursive search providing all dependents,
2088 # not just immediate dependents.
2089 reverse_deps = _get_all_dependents(package_id, all_current_packages)
2090 total_size_deps = 0
2091 total_size_reverse_deps = 0
2092
2093 for dep in deps:
2094 dep['size_formatted'] = \
2095 filtered_filesizeformat(dep['size'])
2096 total_size_deps += dep['size']
2097
2098 for dep in reverse_deps:
2099 dep['size_formatted'] = \
2100 filtered_filesizeformat(dep['size'])
2101 total_size_reverse_deps += dep['size']
2102
2103
2104 return {"error": "ok",
2105 "id": package.pk,
2106 "name": package.name,
2107 "version": package.version,
2108 "unsatisfied_dependencies": list(deps),
2109 "unsatisfied_dependencies_size": total_size_deps,
2110 "unsatisfied_dependencies_size_formatted":
2111 filtered_filesizeformat(total_size_deps),
2112 "reverse_dependencies": list(reverse_deps),
2113 "reverse_dependencies_size": total_size_reverse_deps,
2114 "reverse_dependencies_size_formatted":
2115 filtered_filesizeformat(total_size_reverse_deps)}
2116
2117 included_packages = recipe.includes_set.values_list('pk', flat=True)
2118
2119 if request.method == 'PUT':
2120 # If we're adding back a package which used to be included in this
2121 # image all we need to do is remove it from the excludes
2122 if package.pk in included_packages:
2123 try:
2124 recipe.excludes_set.remove(package)
2125 return {"error": "ok"}
2126 except Package.DoesNotExist:
2127 return {"error":
2128 "Package %s not found in excludes but was in "
2129 "included list" % package.name}
2130
2131 else:
2132 recipe.appends_set.add(package)
2133 # Make sure that package is not in the excludes set
2134 try:
2135 recipe.excludes_set.remove(package)
2136 except:
2137 pass
2138 # Add the dependencies we think will be added to the recipe
2139 # as a result of appending this package.
2140 # TODO this should recurse down the entire deps tree
2141 for dep in package.package_dependencies_source.all_depends():
2142 try:
2143 cust_package = CustomImagePackage.objects.get(
2144 name=dep.depends_on.name)
2145
2146 recipe.includes_set.add(cust_package)
2147 try:
2148 # When adding the pre-requisite package, make
2149 # sure it's not in the excluded list from a
2150 # prior removal.
2151 recipe.excludes_set.remove(cust_package)
2152 except Package.DoesNotExist:
2153 # Don't care if the package had never been excluded
2154 pass
2155 except:
2156 logger.warning("Could not add package's suggested"
2157 "dependencies to the list")
2158
2159 return {"error": "ok"}
2160
2161 elif request.method == 'DELETE':
2162 try:
2163 # If we're deleting a package which is included we need to
2164 # Add it to the excludes list.
2165 if package.pk in included_packages:
2166 recipe.excludes_set.add(package)
2167 else:
2168 recipe.appends_set.remove(package)
2169 all_current_packages = recipe.get_all_packages()
2170 reverse_deps_dictlist = _get_all_dependents(package.pk, all_current_packages)
2171 ids = [entry['pk'] for entry in reverse_deps_dictlist]
2172 reverse_deps = CustomImagePackage.objects.filter(id__in=ids)
2173 for r in reverse_deps:
2174 try:
2175 if r.id in included_packages:
2176 recipe.excludes_set.add(r)
2177 else:
2178 recipe.appends_set.remove(r)
2179 except:
2180 pass
2181
2182 return {"error": "ok"}
2183 except CustomImageRecipe.DoesNotExist:
2184 return {"error": "Tried to remove package that wasn't present"}
2185
2186 else:
2187 return {"error": "Method %s is not supported" % request.method}
2188
2189 def importlayer(request, pid): 1764 def importlayer(request, pid):
2190 template = "importlayer.html" 1765 template = "importlayer.html"
2191 context = { 1766 context = {