blob: ab6ba69e0e14ee9a2b0dd46d34f9a78531753d28 [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
Brad Bishopd7bf8c12018-02-25 22:55:05 -050021import os
Patrick Williamsc0f7c042017-02-23 20:41:17 -060022import re
23import logging
Brad Bishop6e60e8b2018-02-01 10:27:11 -050024import json
Patrick Williamsc0f7c042017-02-23 20:41:17 -060025from collections import Counter
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050026
Patrick Williamsc0f7c042017-02-23 20:41:17 -060027from orm.models import Project, ProjectTarget, Build, Layer_Version
28from orm.models import LayerVersionDependency, LayerSource, ProjectLayer
29from orm.models import Recipe, CustomImageRecipe, CustomImagePackage
30from orm.models import Layer, Target, Package, Package_Dependency
31from orm.models import ProjectVariable
Brad Bishopd7bf8c12018-02-25 22:55:05 -050032from bldcontrol.models import BuildRequest, BuildEnvironment
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050033from bldcontrol import bbcontroller
Patrick Williamsc0f7c042017-02-23 20:41:17 -060034
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050035from django.http import HttpResponse, JsonResponse
36from django.views.generic import View
Patrick Williamsc0f7c042017-02-23 20:41:17 -060037from django.core.urlresolvers import reverse
38from django.db.models import Q, F
39from django.db import Error
40from toastergui.templatetags.projecttags import filtered_filesizeformat
41
42logger = logging.getLogger("toaster")
43
44
45def error_response(error):
46 return JsonResponse({"error": error})
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050047
48
49class XhrBuildRequest(View):
50
51 def get(self, request, *args, **kwargs):
52 return HttpResponse()
53
Patrick Williamsc0f7c042017-02-23 20:41:17 -060054 @staticmethod
55 def cancel_build(br):
56 """Cancel a build request"""
57 try:
58 bbctrl = bbcontroller.BitbakeController(br.environment)
59 bbctrl.forceShutDown()
60 except:
61 # We catch a bunch of exceptions here because
62 # this is where the server has not had time to start up
63 # and the build request or build is in transit between
64 # processes.
65 # We can safely just set the build as cancelled
66 # already as it never got started
67 build = br.build
68 build.outcome = Build.CANCELLED
69 build.save()
70
71 # We now hand over to the buildinfohelper to update the
72 # build state once we've finished cancelling
73 br.state = BuildRequest.REQ_CANCELLING
74 br.save()
75
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050076 def post(self, request, *args, **kwargs):
77 """
78 Build control
79
80 Entry point: /xhr_buildrequest/<project_id>
81 Method: POST
82
83 Args:
84 id: id of build to change
85 buildCancel = build_request_id ...
86 buildDelete = id ...
87 targets = recipe_name ...
88
89 Returns:
90 {"error": "ok"}
91 or
92 {"error": <error message>}
93 """
94
95 project = Project.objects.get(pk=kwargs['pid'])
96
97 if 'buildCancel' in request.POST:
98 for i in request.POST['buildCancel'].strip().split(" "):
99 try:
100 br = BuildRequest.objects.get(project=project, pk=i)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600101 self.cancel_build(br)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500102 except BuildRequest.DoesNotExist:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600103 return error_response('No such build request id %s' % i)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500104
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600105 return error_response('ok')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500106
107 if 'buildDelete' in request.POST:
108 for i in request.POST['buildDelete'].strip().split(" "):
109 try:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600110 BuildRequest.objects.select_for_update().get(
111 project=project,
112 pk=i,
113 state__lte=BuildRequest.REQ_DELETED).delete()
114
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500115 except BuildRequest.DoesNotExist:
116 pass
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600117 return error_response("ok")
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500118
119 if 'targets' in request.POST:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600120 ProjectTarget.objects.filter(project=project).delete()
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500121 s = str(request.POST['targets'])
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600122 for t in re.sub(r'[;%|"]', '', s).split(" "):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500123 if ":" in t:
124 target, task = t.split(":")
125 else:
126 target = t
127 task = ""
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600128 ProjectTarget.objects.create(project=project,
129 target=target,
130 task=task)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500131 project.schedule_build()
132
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600133 return error_response('ok')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500134
135 response = HttpResponse()
136 response.status_code = 500
137 return response
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600138
139
140class XhrLayer(View):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500141 """ Delete, Get, Add and Update Layer information
142
143 Methods: GET POST DELETE PUT
144 """
145
146 def get(self, request, *args, **kwargs):
147 """
148 Get layer information
149
150 Method: GET
151 Entry point: /xhr_layer/<project id>/<layerversion_id>
152 """
153
154 try:
155 layer_version = Layer_Version.objects.get(
156 pk=kwargs['layerversion_id'])
157
158 project = Project.objects.get(pk=kwargs['pid'])
159
160 project_layers = ProjectLayer.objects.filter(
161 project=project).values_list("layercommit_id",
162 flat=True)
163
164 ret = {
165 'error': 'ok',
166 'id': layer_version.pk,
167 'name': layer_version.layer.name,
168 'layerdetailurl':
169 layer_version.get_detailspage_url(project.pk),
170 'vcs_ref': layer_version.get_vcs_reference(),
171 'vcs_url': layer_version.layer.vcs_url,
172 'local_source_dir': layer_version.layer.local_source_dir,
173 'layerdeps': {
174 "list": [
175 {
176 "id": dep.id,
177 "name": dep.layer.name,
178 "layerdetailurl":
179 dep.get_detailspage_url(project.pk),
180 "vcs_url": dep.layer.vcs_url,
181 "vcs_reference": dep.get_vcs_reference()
182 }
183 for dep in layer_version.get_alldeps(project.id)]
184 },
185 'projectlayers': list(project_layers)
186 }
187
188 return JsonResponse(ret)
189 except Layer_Version.DoesNotExist:
190 error_response("No such layer")
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600191
192 def post(self, request, *args, **kwargs):
193 """
194 Update a layer
195
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600196 Method: POST
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500197 Entry point: /xhr_layer/<layerversion_id>
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600198
199 Args:
200 vcs_url, dirpath, commit, up_branch, summary, description,
201 local_source_dir
202
203 add_dep = append a layerversion_id as a dependency
204 rm_dep = remove a layerversion_id as a depedency
205 Returns:
206 {"error": "ok"}
207 or
208 {"error": <error message>}
209 """
210
211 try:
212 # We currently only allow Imported layers to be edited
213 layer_version = Layer_Version.objects.get(
214 id=kwargs['layerversion_id'],
215 project=kwargs['pid'],
216 layer_source=LayerSource.TYPE_IMPORTED)
217
218 except Layer_Version.DoesNotExist:
219 return error_response("Cannot find imported layer to update")
220
221 if "vcs_url" in request.POST:
222 layer_version.layer.vcs_url = request.POST["vcs_url"]
223 if "dirpath" in request.POST:
224 layer_version.dirpath = request.POST["dirpath"]
225 if "commit" in request.POST:
226 layer_version.commit = request.POST["commit"]
227 layer_version.branch = request.POST["commit"]
228 if "summary" in request.POST:
229 layer_version.layer.summary = request.POST["summary"]
230 if "description" in request.POST:
231 layer_version.layer.description = request.POST["description"]
232 if "local_source_dir" in request.POST:
233 layer_version.layer.local_source_dir = \
234 request.POST["local_source_dir"]
235
236 if "add_dep" in request.POST:
237 lvd = LayerVersionDependency(
238 layer_version=layer_version,
239 depends_on_id=request.POST["add_dep"])
240 lvd.save()
241
242 if "rm_dep" in request.POST:
243 rm_dep = LayerVersionDependency.objects.get(
244 layer_version=layer_version,
245 depends_on_id=request.POST["rm_dep"])
246 rm_dep.delete()
247
248 try:
249 layer_version.layer.save()
250 layer_version.save()
251 except Exception as e:
252 return error_response("Could not update layer version entry: %s"
253 % e)
254
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500255 return error_response("ok")
256
257 def put(self, request, *args, **kwargs):
258 """ Add a new layer
259
260 Method: PUT
261 Entry point: /xhr_layer/<project id>/
262 Args:
263 project_id, name,
264 [vcs_url, dir_path, git_ref], [local_source_dir], [layer_deps
265 (csv)]
266
267 """
268 try:
269 project = Project.objects.get(pk=kwargs['pid'])
270
271 layer_data = json.loads(request.body.decode('utf-8'))
272
273 # We require a unique layer name as otherwise the lists of layers
274 # becomes very confusing
275 existing_layers = \
276 project.get_all_compatible_layer_versions().values_list(
277 "layer__name",
278 flat=True)
279
280 add_to_project = False
281 layer_deps_added = []
282 if 'add_to_project' in layer_data:
283 add_to_project = True
284
285 if layer_data['name'] in existing_layers:
286 return JsonResponse({"error": "layer-name-exists"})
287
288 layer = Layer.objects.create(name=layer_data['name'])
289
290 layer_version = Layer_Version.objects.create(
291 layer=layer,
292 project=project,
293 layer_source=LayerSource.TYPE_IMPORTED)
294
295 # Local layer
296 if ('local_source_dir' in layer_data) and layer.local_source_dir:
297 layer.local_source_dir = layer_data['local_source_dir']
298 # git layer
299 elif 'vcs_url' in layer_data:
300 layer.vcs_url = layer_data['vcs_url']
301 layer_version.dirpath = layer_data['dir_path']
302 layer_version.commit = layer_data['git_ref']
303 layer_version.branch = layer_data['git_ref']
304
305 layer.save()
306 layer_version.save()
307
308 if add_to_project:
309 ProjectLayer.objects.get_or_create(
310 layercommit=layer_version, project=project)
311
312 # Add the layer dependencies
313 if 'layer_deps' in layer_data:
314 for layer_dep_id in layer_data['layer_deps'].split(","):
315 layer_dep = Layer_Version.objects.get(pk=layer_dep_id)
316 LayerVersionDependency.objects.get_or_create(
317 layer_version=layer_version, depends_on=layer_dep)
318
319 # Add layer deps to the project if specified
320 if add_to_project:
321 created, pl = ProjectLayer.objects.get_or_create(
322 layercommit=layer_dep, project=project)
323 layer_deps_added.append(
324 {'name': layer_dep.layer.name,
325 'layerdetailurl':
326 layer_dep.get_detailspage_url(project.pk)})
327
328 except Layer_Version.DoesNotExist:
329 return error_response("layer-dep-not-found")
330 except Project.DoesNotExist:
331 return error_response("project-not-found")
332 except KeyError:
333 return error_response("incorrect-parameters")
334
335 return JsonResponse({'error': "ok",
336 'imported_layer': {
337 'name': layer.name,
338 'layerdetailurl':
339 layer_version.get_detailspage_url()},
340 'deps_added': layer_deps_added})
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600341
342 def delete(self, request, *args, **kwargs):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500343 """ Delete an imported layer
344
345 Method: DELETE
346 Entry point: /xhr_layer/<projed id>/<layerversion_id>
347
348 """
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600349 try:
350 # We currently only allow Imported layers to be deleted
351 layer_version = Layer_Version.objects.get(
352 id=kwargs['layerversion_id'],
353 project=kwargs['pid'],
354 layer_source=LayerSource.TYPE_IMPORTED)
355 except Layer_Version.DoesNotExist:
356 return error_response("Cannot find imported layer to delete")
357
358 try:
359 ProjectLayer.objects.get(project=kwargs['pid'],
360 layercommit=layer_version).delete()
361 except ProjectLayer.DoesNotExist:
362 pass
363
364 layer_version.layer.delete()
365 layer_version.delete()
366
367 return JsonResponse({
368 "error": "ok",
369 "gotoUrl": reverse('projectlayers', args=(kwargs['pid'],))
370 })
371
372
373class XhrCustomRecipe(View):
374 """ Create a custom image recipe """
375
376 def post(self, request, *args, **kwargs):
377 """
378 Custom image recipe REST API
379
380 Entry point: /xhr_customrecipe/
381 Method: POST
382
383 Args:
384 name: name of custom recipe to create
385 project: target project id of orm.models.Project
386 base: base recipe id of orm.models.Recipe
387
388 Returns:
389 {"error": "ok",
390 "url": <url of the created recipe>}
391 or
392 {"error": <error message>}
393 """
394 # check if request has all required parameters
395 for param in ('name', 'project', 'base'):
396 if param not in request.POST:
397 return error_response("Missing parameter '%s'" % param)
398
399 # get project and baserecipe objects
400 params = {}
401 for name, model in [("project", Project),
402 ("base", Recipe)]:
403 value = request.POST[name]
404 try:
405 params[name] = model.objects.get(id=value)
406 except model.DoesNotExist:
407 return error_response("Invalid %s id %s" % (name, value))
408
409 # create custom recipe
410 try:
411
412 # Only allowed chars in name are a-z, 0-9 and -
413 if re.search(r'[^a-z|0-9|-]', request.POST["name"]):
414 return error_response("invalid-name")
415
416 custom_images = CustomImageRecipe.objects.all()
417
418 # Are there any recipes with this name already in our project?
419 existing_image_recipes_in_project = custom_images.filter(
420 name=request.POST["name"], project=params["project"])
421
422 if existing_image_recipes_in_project.count() > 0:
423 return error_response("image-already-exists")
424
425 # Are there any recipes with this name which aren't custom
426 # image recipes?
427 custom_image_ids = custom_images.values_list('id', flat=True)
428 existing_non_image_recipes = Recipe.objects.filter(
429 Q(name=request.POST["name"]) & ~Q(pk__in=custom_image_ids)
430 )
431
432 if existing_non_image_recipes.count() > 0:
433 return error_response("recipe-already-exists")
434
435 # create layer 'Custom layer' and verion if needed
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500436 layer, l_created = Layer.objects.get_or_create(
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600437 name=CustomImageRecipe.LAYER_NAME,
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500438 summary="Layer for custom recipes")
439
440 if l_created:
441 layer.local_source_dir = "toaster_created_layer"
442 layer.save()
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600443
444 # Check if we have a layer version already
445 # We don't use get_or_create here because the dirpath will change
446 # and is a required field
447 lver = Layer_Version.objects.filter(Q(project=params['project']) &
448 Q(layer=layer) &
449 Q(build=None)).last()
450 if lver is None:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500451 lver, lv_created = Layer_Version.objects.get_or_create(
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600452 project=params['project'],
453 layer=layer,
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500454 layer_source=LayerSource.TYPE_LOCAL,
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600455 dirpath="toaster_created_layer")
456
457 # Add a dependency on our layer to the base recipe's layer
458 LayerVersionDependency.objects.get_or_create(
459 layer_version=lver,
460 depends_on=params["base"].layer_version)
461
462 # Add it to our current project if needed
463 ProjectLayer.objects.get_or_create(project=params['project'],
464 layercommit=lver,
465 optional=False)
466
467 # Create the actual recipe
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500468 recipe, r_created = CustomImageRecipe.objects.get_or_create(
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600469 name=request.POST["name"],
470 base_recipe=params["base"],
471 project=params["project"],
472 layer_version=lver,
473 is_image=True)
474
475 # If we created the object then setup these fields. They may get
476 # overwritten later on and cause the get_or_create to create a
477 # duplicate if they've changed.
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500478 if r_created:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600479 recipe.file_path = request.POST["name"]
480 recipe.license = "MIT"
481 recipe.version = "0.1"
482 recipe.save()
483
484 except Error as err:
485 return error_response("Can't create custom recipe: %s" % err)
486
487 # Find the package list from the last build of this recipe/target
488 target = Target.objects.filter(Q(build__outcome=Build.SUCCEEDED) &
489 Q(build__project=params['project']) &
490 (Q(target=params['base'].name) |
491 Q(target=recipe.name))).last()
492 if target:
493 # Copy in every package
494 # We don't want these packages to be linked to anything because
495 # that underlying data may change e.g. delete a build
496 for tpackage in target.target_installed_package_set.all():
497 try:
498 built_package = tpackage.package
499 # The package had no recipe information so is a ghost
500 # package skip it
501 if built_package.recipe is None:
502 continue
503
504 config_package = CustomImagePackage.objects.get(
505 name=built_package.name)
506
507 recipe.includes_set.add(config_package)
508 except Exception as e:
509 logger.warning("Error adding package %s %s" %
510 (tpackage.package.name, e))
511 pass
512
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500513 # pre-create layer directory structure, so that other builds
514 # are not blocked by this new recipe dependecy
515 # NOTE: this is parallel code to 'localhostbecontroller.py'
516 be = BuildEnvironment.objects.all()[0]
517 layerpath = os.path.join(be.builddir,
518 CustomImageRecipe.LAYER_NAME)
519 for name in ("conf", "recipes"):
520 path = os.path.join(layerpath, name)
521 if not os.path.isdir(path):
522 os.makedirs(path)
523 # pre-create layer.conf
524 config = os.path.join(layerpath, "conf", "layer.conf")
525 if not os.path.isfile(config):
526 with open(config, "w") as conf:
527 conf.write('BBPATH .= ":${LAYERDIR}"\nBBFILES += "${LAYERDIR}/recipes/*.bb"\n')
528 # pre-create new image's recipe file
529 recipe_path = os.path.join(layerpath, "recipes", "%s.bb" %
530 recipe.name)
531 with open(recipe_path, "w") as recipef:
532 recipef.write(recipe.generate_recipe_file_contents())
533
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600534 return JsonResponse(
535 {"error": "ok",
536 "packages": recipe.get_all_packages().count(),
537 "url": reverse('customrecipe', args=(params['project'].pk,
538 recipe.id))})
539
540
541class XhrCustomRecipeId(View):
542 """
543 Set of ReST API processors working with recipe id.
544
545 Entry point: /xhr_customrecipe/<recipe_id>
546
547 Methods:
548 GET - Get details of custom image recipe
549 DELETE - Delete custom image recipe
550
551 Returns:
552 GET:
553 {"error": "ok",
554 "info": dictionary of field name -> value pairs
555 of the CustomImageRecipe model}
556 DELETE:
557 {"error": "ok"}
558 or
559 {"error": <error message>}
560 """
561 @staticmethod
562 def _get_ci_recipe(recipe_id):
563 """ Get Custom Image recipe or return an error response"""
564 try:
565 custom_recipe = \
566 CustomImageRecipe.objects.get(pk=recipe_id)
567 return custom_recipe, None
568
569 except CustomImageRecipe.DoesNotExist:
570 return None, error_response("Custom recipe with id=%s "
571 "not found" % recipe_id)
572
573 def get(self, request, *args, **kwargs):
574 custom_recipe, error = self._get_ci_recipe(kwargs['recipe_id'])
575 if error:
576 return error
577
578 if request.method == 'GET':
579 info = {"id": custom_recipe.id,
580 "name": custom_recipe.name,
581 "base_recipe_id": custom_recipe.base_recipe.id,
582 "project_id": custom_recipe.project.id}
583
584 return JsonResponse({"error": "ok", "info": info})
585
586 def delete(self, request, *args, **kwargs):
587 custom_recipe, error = self._get_ci_recipe(kwargs['recipe_id'])
588 if error:
589 return error
590
591 project = custom_recipe.project
592
593 custom_recipe.delete()
594 return JsonResponse({"error": "ok",
595 "gotoUrl": reverse("projectcustomimages",
596 args=(project.pk,))})
597
598
599class XhrCustomRecipePackages(View):
600 """
601 ReST API to add/remove packages to/from custom recipe.
602
603 Entry point: /xhr_customrecipe/<recipe_id>/packages/<package_id>
604 Methods:
605 PUT - Add package to the recipe
606 DELETE - Delete package from the recipe
607 GET - Get package information
608
609 Returns:
610 {"error": "ok"}
611 or
612 {"error": <error message>}
613 """
614 @staticmethod
615 def _get_package(package_id):
616 try:
617 package = CustomImagePackage.objects.get(pk=package_id)
618 return package, None
619 except Package.DoesNotExist:
620 return None, error_response("Package with id=%s "
621 "not found" % package_id)
622
623 def _traverse_dependents(self, next_package_id,
624 rev_deps, all_current_packages, tree_level=0):
625 """
626 Recurse through reverse dependency tree for next_package_id.
627 Limit the reverse dependency search to packages not already scanned,
628 that is, not already in rev_deps.
629 Limit the scan to a depth (tree_level) not exceeding the count of
630 all packages in the custom image, and if that depth is exceeded
631 return False, pop out of the recursion, and write a warning
632 to the log, but this is unlikely, suggesting a dependency loop
633 not caught by bitbake.
634 On return, the input/output arg rev_deps is appended with queryset
635 dictionary elements, annotated for use in the customimage template.
636 The list has unsorted, but unique elements.
637 """
638 max_dependency_tree_depth = all_current_packages.count()
639 if tree_level >= max_dependency_tree_depth:
640 logger.warning(
641 "The number of reverse dependencies "
642 "for this package exceeds " + max_dependency_tree_depth +
643 " and the remaining reverse dependencies will not be removed")
644 return True
645
646 package = CustomImagePackage.objects.get(id=next_package_id)
647 dependents = \
648 package.package_dependencies_target.annotate(
649 name=F('package__name'),
650 pk=F('package__pk'),
651 size=F('package__size'),
652 ).values("name", "pk", "size").exclude(
653 ~Q(pk__in=all_current_packages)
654 )
655
656 for pkg in dependents:
657 if pkg in rev_deps:
658 # already seen, skip dependent search
659 continue
660
661 rev_deps.append(pkg)
662 if (self._traverse_dependents(pkg["pk"], rev_deps,
663 all_current_packages,
664 tree_level+1)):
665 return True
666
667 return False
668
669 def _get_all_dependents(self, package_id, all_current_packages):
670 """
671 Returns sorted list of recursive reverse dependencies for package_id,
672 as a list of dictionary items, by recursing through dependency
673 relationships.
674 """
675 rev_deps = []
676 self._traverse_dependents(package_id, rev_deps, all_current_packages)
677 rev_deps = sorted(rev_deps, key=lambda x: x["name"])
678 return rev_deps
679
680 def get(self, request, *args, **kwargs):
681 recipe, error = XhrCustomRecipeId._get_ci_recipe(
682 kwargs['recipe_id'])
683 if error:
684 return error
685
686 # If no package_id then list all the current packages
687 if not kwargs['package_id']:
688 total_size = 0
689 packages = recipe.get_all_packages().values("id",
690 "name",
691 "version",
692 "size")
693 for package in packages:
694 package['size_formatted'] = \
695 filtered_filesizeformat(package['size'])
696 total_size += package['size']
697
698 return JsonResponse({"error": "ok",
699 "packages": list(packages),
700 "total": len(packages),
701 "total_size": total_size,
702 "total_size_formatted":
703 filtered_filesizeformat(total_size)})
704 else:
705 package, error = XhrCustomRecipePackages._get_package(
706 kwargs['package_id'])
707 if error:
708 return error
709
710 all_current_packages = recipe.get_all_packages()
711
712 # Dependencies for package which aren't satisfied by the
713 # current packages in the custom image recipe
714 deps = package.package_dependencies_source.for_target_or_none(
715 recipe.name)['packages'].annotate(
716 name=F('depends_on__name'),
717 pk=F('depends_on__pk'),
718 size=F('depends_on__size'),
719 ).values("name", "pk", "size").filter(
720 # There are two depends types we don't know why
721 (Q(dep_type=Package_Dependency.TYPE_TRDEPENDS) |
722 Q(dep_type=Package_Dependency.TYPE_RDEPENDS)) &
723 ~Q(pk__in=all_current_packages)
724 )
725
726 # Reverse dependencies which are needed by packages that are
727 # in the image. Recursive search providing all dependents,
728 # not just immediate dependents.
729 reverse_deps = self._get_all_dependents(kwargs['package_id'],
730 all_current_packages)
731 total_size_deps = 0
732 total_size_reverse_deps = 0
733
734 for dep in deps:
735 dep['size_formatted'] = \
736 filtered_filesizeformat(dep['size'])
737 total_size_deps += dep['size']
738
739 for dep in reverse_deps:
740 dep['size_formatted'] = \
741 filtered_filesizeformat(dep['size'])
742 total_size_reverse_deps += dep['size']
743
744 return JsonResponse(
745 {"error": "ok",
746 "id": package.pk,
747 "name": package.name,
748 "version": package.version,
749 "unsatisfied_dependencies": list(deps),
750 "unsatisfied_dependencies_size": total_size_deps,
751 "unsatisfied_dependencies_size_formatted":
752 filtered_filesizeformat(total_size_deps),
753 "reverse_dependencies": list(reverse_deps),
754 "reverse_dependencies_size": total_size_reverse_deps,
755 "reverse_dependencies_size_formatted":
756 filtered_filesizeformat(total_size_reverse_deps)})
757
758 def put(self, request, *args, **kwargs):
759 recipe, error = XhrCustomRecipeId._get_ci_recipe(kwargs['recipe_id'])
760 package, error = self._get_package(kwargs['package_id'])
761 if error:
762 return error
763
764 included_packages = recipe.includes_set.values_list('pk',
765 flat=True)
766
767 # If we're adding back a package which used to be included in this
768 # image all we need to do is remove it from the excludes
769 if package.pk in included_packages:
770 try:
771 recipe.excludes_set.remove(package)
772 return {"error": "ok"}
773 except Package.DoesNotExist:
774 return error_response("Package %s not found in excludes"
775 " but was in included list" %
776 package.name)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600777 else:
778 recipe.appends_set.add(package)
779 # Make sure that package is not in the excludes set
780 try:
781 recipe.excludes_set.remove(package)
782 except:
783 pass
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600784
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500785 # Add the dependencies we think will be added to the recipe
786 # as a result of appending this package.
787 # TODO this should recurse down the entire deps tree
788 for dep in package.package_dependencies_source.all_depends():
789 try:
790 cust_package = CustomImagePackage.objects.get(
791 name=dep.depends_on.name)
792
793 recipe.includes_set.add(cust_package)
794 try:
795 # When adding the pre-requisite package, make
796 # sure it's not in the excluded list from a
797 # prior removal.
798 recipe.excludes_set.remove(cust_package)
799 except package.DoesNotExist:
800 # Don't care if the package had never been excluded
801 pass
802 except:
803 logger.warning("Could not add package's suggested"
804 "dependencies to the list")
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600805 return JsonResponse({"error": "ok"})
806
807 def delete(self, request, *args, **kwargs):
808 recipe, error = XhrCustomRecipeId._get_ci_recipe(kwargs['recipe_id'])
809 package, error = self._get_package(kwargs['package_id'])
810 if error:
811 return error
812
813 try:
814 included_packages = recipe.includes_set.values_list('pk',
815 flat=True)
816 # If we're deleting a package which is included we need to
817 # Add it to the excludes list.
818 if package.pk in included_packages:
819 recipe.excludes_set.add(package)
820 else:
821 recipe.appends_set.remove(package)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600822
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500823 # remove dependencies as well
824 all_current_packages = recipe.get_all_packages()
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600825
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500826 reverse_deps_dictlist = self._get_all_dependents(
827 package.pk,
828 all_current_packages)
829
830 ids = [entry['pk'] for entry in reverse_deps_dictlist]
831 reverse_deps = CustomImagePackage.objects.filter(id__in=ids)
832 for r in reverse_deps:
833 try:
834 if r.id in included_packages:
835 recipe.excludes_set.add(r)
836 else:
837 recipe.appends_set.remove(r)
838 except:
839 pass
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600840
841 return JsonResponse({"error": "ok"})
842 except CustomImageRecipe.DoesNotExist:
843 return error_response("Tried to remove package that wasn't"
844 " present")
845
846
847class XhrProject(View):
848 """ Create, delete or edit a project
849
850 Entry point: /xhr_project/<project_id>
851 """
852 def post(self, request, *args, **kwargs):
853 """
854 Edit project control
855
856 Args:
857 layerAdd = layer_version_id layer_version_id ...
858 layerDel = layer_version_id layer_version_id ...
859 projectName = new_project_name
860 machineName = new_machine_name
861
862 Returns:
863 {"error": "ok"}
864 or
865 {"error": <error message>}
866 """
867 try:
868 prj = Project.objects.get(pk=kwargs['project_id'])
869 except Project.DoesNotExist:
870 return error_response("No such project")
871
872 # Add layers
873 if 'layerAdd' in request.POST and len(request.POST['layerAdd']) > 0:
874 for layer_version_id in request.POST['layerAdd'].split(','):
875 try:
876 lv = Layer_Version.objects.get(pk=int(layer_version_id))
877 ProjectLayer.objects.get_or_create(project=prj,
878 layercommit=lv)
879 except Layer_Version.DoesNotExist:
880 return error_response("Layer version %s asked to add "
881 "doesn't exist" % layer_version_id)
882
883 # Remove layers
884 if 'layerDel' in request.POST and len(request.POST['layerDel']) > 0:
885 layer_version_ids = request.POST['layerDel'].split(',')
886 ProjectLayer.objects.filter(
887 project=prj,
888 layercommit_id__in=layer_version_ids).delete()
889
890 # Project name change
891 if 'projectName' in request.POST:
892 prj.name = request.POST['projectName']
893 prj.save()
894
895 # Machine name change
896 if 'machineName' in request.POST:
897 machinevar = prj.projectvariable_set.get(name="MACHINE")
898 machinevar.value = request.POST['machineName']
899 machinevar.save()
900
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500901 # Distro name change
902 if 'distroName' in request.POST:
903 distrovar = prj.projectvariable_set.get(name="DISTRO")
904 distrovar.value = request.POST['distroName']
905 distrovar.save()
906
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600907 return JsonResponse({"error": "ok"})
908
909 def get(self, request, *args, **kwargs):
910 """
911 Returns:
912 json object representing the current project
913 or:
914 {"error": <error message>}
915 """
916
917 try:
918 project = Project.objects.get(pk=kwargs['project_id'])
919 except Project.DoesNotExist:
920 return error_response("Project %s does not exist" %
921 kwargs['project_id'])
922
923 # Create the frequently built targets list
924
925 freqtargets = Counter(Target.objects.filter(
926 Q(build__project=project),
927 ~Q(build__outcome=Build.IN_PROGRESS)
928 ).order_by("target").values_list("target", flat=True))
929
930 freqtargets = freqtargets.most_common(5)
931
932 # We now have the targets in order of frequency but if there are two
933 # with the same frequency then we need to make sure those are in
934 # alphabetical order without losing the frequency ordering
935
936 tmp = []
937 switch = None
938 for i, freqtartget in enumerate(freqtargets):
939 target, count = freqtartget
940 try:
941 target_next, count_next = freqtargets[i+1]
942 if count == count_next and target > target_next:
943 switch = target
944 continue
945 except IndexError:
946 pass
947
948 tmp.append(target)
949
950 if switch:
951 tmp.append(switch)
952 switch = None
953
954 freqtargets = tmp
955
956 layers = []
957 for layer in project.projectlayer_set.all():
958 layers.append({
959 "id": layer.layercommit.pk,
960 "name": layer.layercommit.layer.name,
961 "vcs_url": layer.layercommit.layer.vcs_url,
962 "local_source_dir": layer.layercommit.layer.local_source_dir,
963 "vcs_reference": layer.layercommit.get_vcs_reference(),
964 "url": layer.layercommit.layer.layer_index_url,
965 "layerdetailurl": layer.layercommit.get_detailspage_url(
966 project.pk),
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500967 "xhrLayerUrl": reverse("xhr_layer",
968 args=(project.pk,
969 layer.layercommit.pk)),
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600970 "layersource": layer.layercommit.layer_source
971 })
972
973 data = {
974 "name": project.name,
975 "layers": layers,
976 "freqtargets": freqtargets,
977 }
978
979 if project.release is not None:
980 data['release'] = {
981 "id": project.release.pk,
982 "name": project.release.name,
983 "description": project.release.description
984 }
985
986 try:
987 data["machine"] = {"name":
988 project.projectvariable_set.get(
989 name="MACHINE").value}
990 except ProjectVariable.DoesNotExist:
991 data["machine"] = None
992 try:
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500993 data["distro"] = {"name":
994 project.projectvariable_set.get(
995 name="DISTRO").value}
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600996 except ProjectVariable.DoesNotExist:
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500997 data["distro"] = None
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600998
999 data['error'] = "ok"
1000
1001 return JsonResponse(data)
1002
1003 def put(self, request, *args, **kwargs):
1004 # TODO create new project api
1005 return HttpResponse()
1006
1007 def delete(self, request, *args, **kwargs):
1008 """Delete a project. Cancels any builds in progress"""
1009 try:
1010 project = Project.objects.get(pk=kwargs['project_id'])
1011 # Cancel any builds in progress
1012 for br in BuildRequest.objects.filter(
1013 project=project,
1014 state=BuildRequest.REQ_INPROGRESS):
1015 XhrBuildRequest.cancel_build(br)
1016
1017 project.delete()
1018
1019 except Project.DoesNotExist:
1020 return error_response("Project %s does not exist" %
1021 kwargs['project_id'])
1022
1023 return JsonResponse({
1024 "error": "ok",
1025 "gotoUrl": reverse("all-projects", args=[])
1026 })
1027
1028
1029class XhrBuild(View):
1030 """ Delete a build object
1031
1032 Entry point: /xhr_build/<build_id>
1033 """
1034 def delete(self, request, *args, **kwargs):
1035 """
1036 Delete build data
1037
1038 Args:
1039 build_id = build_id
1040
1041 Returns:
1042 {"error": "ok"}
1043 or
1044 {"error": <error message>}
1045 """
1046 try:
1047 build = Build.objects.get(pk=kwargs['build_id'])
1048 project = build.project
1049 build.delete()
1050 except Build.DoesNotExist:
1051 return error_response("Build %s does not exist" %
1052 kwargs['build_id'])
1053 return JsonResponse({
1054 "error": "ok",
1055 "gotoUrl": reverse("projectbuilds", args=(project.pk,))
1056 })