blob: 8b49b3ee3f86e80f38d0125bef3e529fd3d386af [file] [log] [blame]
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001#
2# BitBake Toaster Implementation
3#
4# Copyright (C) 2016 Intel Corporation
5#
Brad Bishopc342db32019-05-15 21:57:59 -04006# SPDX-License-Identifier: GPL-2.0-only
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05007#
Patrick Williamsc0f7c042017-02-23 20:41:17 -06008# Please run flake8 on this file before sending patches
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05009
Brad Bishopd7bf8c12018-02-25 22:55:05 -050010import os
Patrick Williamsc0f7c042017-02-23 20:41:17 -060011import re
12import logging
Brad Bishop6e60e8b2018-02-01 10:27:11 -050013import json
Brad Bishop1a4b7ee2018-12-16 17:11:34 -080014import subprocess
Patrick Williamsc0f7c042017-02-23 20:41:17 -060015from collections import Counter
Brad Bishop1a4b7ee2018-12-16 17:11:34 -080016from shutil import copyfile
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050017
Patrick Williamsc0f7c042017-02-23 20:41:17 -060018from orm.models import Project, ProjectTarget, Build, Layer_Version
19from orm.models import LayerVersionDependency, LayerSource, ProjectLayer
20from orm.models import Recipe, CustomImageRecipe, CustomImagePackage
21from orm.models import Layer, Target, Package, Package_Dependency
22from orm.models import ProjectVariable
Brad Bishopd7bf8c12018-02-25 22:55:05 -050023from bldcontrol.models import BuildRequest, BuildEnvironment
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050024from bldcontrol import bbcontroller
Patrick Williamsc0f7c042017-02-23 20:41:17 -060025
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050026from django.http import HttpResponse, JsonResponse
27from django.views.generic import View
Patrick Williamsc0f7c042017-02-23 20:41:17 -060028from django.core.urlresolvers import reverse
29from django.db.models import Q, F
30from django.db import Error
31from toastergui.templatetags.projecttags import filtered_filesizeformat
Brad Bishop1a4b7ee2018-12-16 17:11:34 -080032from django.utils import timezone
33import pytz
34
35# development/debugging support
36verbose = 2
37def _log(msg):
38 if 1 == verbose:
39 print(msg)
40 elif 2 == verbose:
41 f1=open('/tmp/toaster.log', 'a')
42 f1.write("|" + msg + "|\n" )
43 f1.close()
Patrick Williamsc0f7c042017-02-23 20:41:17 -060044
45logger = logging.getLogger("toaster")
46
47
48def error_response(error):
49 return JsonResponse({"error": error})
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050050
51
52class XhrBuildRequest(View):
53
54 def get(self, request, *args, **kwargs):
55 return HttpResponse()
56
Patrick Williamsc0f7c042017-02-23 20:41:17 -060057 @staticmethod
58 def cancel_build(br):
59 """Cancel a build request"""
60 try:
61 bbctrl = bbcontroller.BitbakeController(br.environment)
62 bbctrl.forceShutDown()
63 except:
64 # We catch a bunch of exceptions here because
65 # this is where the server has not had time to start up
66 # and the build request or build is in transit between
67 # processes.
68 # We can safely just set the build as cancelled
69 # already as it never got started
70 build = br.build
71 build.outcome = Build.CANCELLED
72 build.save()
73
74 # We now hand over to the buildinfohelper to update the
75 # build state once we've finished cancelling
76 br.state = BuildRequest.REQ_CANCELLING
77 br.save()
78
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050079 def post(self, request, *args, **kwargs):
80 """
81 Build control
82
83 Entry point: /xhr_buildrequest/<project_id>
84 Method: POST
85
86 Args:
87 id: id of build to change
88 buildCancel = build_request_id ...
89 buildDelete = id ...
90 targets = recipe_name ...
91
92 Returns:
93 {"error": "ok"}
94 or
95 {"error": <error message>}
96 """
97
98 project = Project.objects.get(pk=kwargs['pid'])
99
100 if 'buildCancel' in request.POST:
101 for i in request.POST['buildCancel'].strip().split(" "):
102 try:
103 br = BuildRequest.objects.get(project=project, pk=i)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600104 self.cancel_build(br)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500105 except BuildRequest.DoesNotExist:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600106 return error_response('No such build request id %s' % i)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500107
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600108 return error_response('ok')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500109
110 if 'buildDelete' in request.POST:
111 for i in request.POST['buildDelete'].strip().split(" "):
112 try:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600113 BuildRequest.objects.select_for_update().get(
114 project=project,
115 pk=i,
116 state__lte=BuildRequest.REQ_DELETED).delete()
117
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500118 except BuildRequest.DoesNotExist:
119 pass
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600120 return error_response("ok")
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500121
122 if 'targets' in request.POST:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600123 ProjectTarget.objects.filter(project=project).delete()
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500124 s = str(request.POST['targets'])
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600125 for t in re.sub(r'[;%|"]', '', s).split(" "):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500126 if ":" in t:
127 target, task = t.split(":")
128 else:
129 target = t
130 task = ""
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600131 ProjectTarget.objects.create(project=project,
132 target=target,
133 task=task)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500134 project.schedule_build()
135
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600136 return error_response('ok')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500137
138 response = HttpResponse()
139 response.status_code = 500
140 return response
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600141
142
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800143class XhrProjectUpdate(View):
144
145 def get(self, request, *args, **kwargs):
146 return HttpResponse()
147
148 def post(self, request, *args, **kwargs):
149 """
150 Project Update
151
152 Entry point: /xhr_projectupdate/<project_id>
153 Method: POST
154
155 Args:
156 pid: pid of project to update
157
158 Returns:
159 {"error": "ok"}
160 or
161 {"error": <error message>}
162 """
163
164 project = Project.objects.get(pk=kwargs['pid'])
165 logger.debug("ProjectUpdateCallback:project.pk=%d,project.builddir=%s" % (project.pk,project.builddir))
166
167 if 'do_update' in request.POST:
168
169 # Extract any default image recipe
170 if 'default_image' in request.POST:
171 project.set_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE,str(request.POST['default_image']))
172 else:
173 project.set_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE,'')
174
175 logger.debug("ProjectUpdateCallback:Chain to the build request")
176
177 # Chain to the build request
178 xhrBuildRequest = XhrBuildRequest()
179 return xhrBuildRequest.post(request, *args, **kwargs)
180
181 logger.warning("ERROR:XhrProjectUpdate")
182 response = HttpResponse()
183 response.status_code = 500
184 return response
185
186class XhrSetDefaultImageUrl(View):
187
188 def get(self, request, *args, **kwargs):
189 return HttpResponse()
190
191 def post(self, request, *args, **kwargs):
192 """
193 Project Update
194
195 Entry point: /xhr_setdefaultimage/<project_id>
196 Method: POST
197
198 Args:
199 pid: pid of project to update default image
200
201 Returns:
202 {"error": "ok"}
203 or
204 {"error": <error message>}
205 """
206
207 project = Project.objects.get(pk=kwargs['pid'])
208 logger.debug("XhrSetDefaultImageUrl:project.pk=%d" % (project.pk))
209
210 # set any default image recipe
211 if 'targets' in request.POST:
212 default_target = str(request.POST['targets'])
213 project.set_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE,default_target)
214 logger.debug("XhrSetDefaultImageUrl,project.pk=%d,project.builddir=%s" % (project.pk,project.builddir))
215 return error_response('ok')
216
217 logger.warning("ERROR:XhrSetDefaultImageUrl")
218 response = HttpResponse()
219 response.status_code = 500
220 return response
221
222
223#
224# Layer Management
225#
226# Rules for 'local_source_dir' layers
227# * Layers must have a unique name in the Layers table
228# * A 'local_source_dir' layer is supposed to be shared
229# by all projects that use it, so that it can have the
230# same logical name
231# * Each project that uses a layer will have its own
232# LayerVersion and Project Layer for it
233# * During the Paroject delete process, when the last
234# LayerVersion for a 'local_source_dir' layer is deleted
235# then the Layer record is deleted to remove orphans
236#
237
238def scan_layer_content(layer,layer_version):
239 # if this is a local layer directory, we can immediately scan its content
240 if layer.local_source_dir:
241 try:
242 # recipes-*/*/*.bb
243 cmd = '%s %s' % ('ls', os.path.join(layer.local_source_dir,'recipes-*/*/*.bb'))
244 recipes_list = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,stderr=subprocess.STDOUT).stdout.read()
245 recipes_list = recipes_list.decode("utf-8").strip()
246 if recipes_list and 'No such' not in recipes_list:
247 for recipe in recipes_list.split('\n'):
248 recipe_path = recipe[recipe.rfind('recipes-'):]
249 recipe_name = recipe[recipe.rfind('/')+1:].replace('.bb','')
250 recipe_ver = recipe_name.rfind('_')
251 if recipe_ver > 0:
252 recipe_name = recipe_name[0:recipe_ver]
253 if recipe_name:
254 ro, created = Recipe.objects.get_or_create(
255 layer_version=layer_version,
256 name=recipe_name
257 )
258 if created:
259 ro.file_path = recipe_path
260 ro.summary = 'Recipe %s from layer %s' % (recipe_name,layer.name)
261 ro.description = ro.summary
262 ro.save()
263
264 except Exception as e:
265 logger.warning("ERROR:scan_layer_content: %s" % e)
266
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600267class XhrLayer(View):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500268 """ Delete, Get, Add and Update Layer information
269
270 Methods: GET POST DELETE PUT
271 """
272
273 def get(self, request, *args, **kwargs):
274 """
275 Get layer information
276
277 Method: GET
278 Entry point: /xhr_layer/<project id>/<layerversion_id>
279 """
280
281 try:
282 layer_version = Layer_Version.objects.get(
283 pk=kwargs['layerversion_id'])
284
285 project = Project.objects.get(pk=kwargs['pid'])
286
287 project_layers = ProjectLayer.objects.filter(
288 project=project).values_list("layercommit_id",
289 flat=True)
290
291 ret = {
292 'error': 'ok',
293 'id': layer_version.pk,
294 'name': layer_version.layer.name,
295 'layerdetailurl':
296 layer_version.get_detailspage_url(project.pk),
297 'vcs_ref': layer_version.get_vcs_reference(),
298 'vcs_url': layer_version.layer.vcs_url,
299 'local_source_dir': layer_version.layer.local_source_dir,
300 'layerdeps': {
301 "list": [
302 {
303 "id": dep.id,
304 "name": dep.layer.name,
305 "layerdetailurl":
306 dep.get_detailspage_url(project.pk),
307 "vcs_url": dep.layer.vcs_url,
308 "vcs_reference": dep.get_vcs_reference()
309 }
310 for dep in layer_version.get_alldeps(project.id)]
311 },
312 'projectlayers': list(project_layers)
313 }
314
315 return JsonResponse(ret)
316 except Layer_Version.DoesNotExist:
317 error_response("No such layer")
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600318
319 def post(self, request, *args, **kwargs):
320 """
321 Update a layer
322
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600323 Method: POST
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500324 Entry point: /xhr_layer/<layerversion_id>
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600325
326 Args:
327 vcs_url, dirpath, commit, up_branch, summary, description,
328 local_source_dir
329
330 add_dep = append a layerversion_id as a dependency
331 rm_dep = remove a layerversion_id as a depedency
332 Returns:
333 {"error": "ok"}
334 or
335 {"error": <error message>}
336 """
337
338 try:
339 # We currently only allow Imported layers to be edited
340 layer_version = Layer_Version.objects.get(
341 id=kwargs['layerversion_id'],
342 project=kwargs['pid'],
343 layer_source=LayerSource.TYPE_IMPORTED)
344
345 except Layer_Version.DoesNotExist:
346 return error_response("Cannot find imported layer to update")
347
348 if "vcs_url" in request.POST:
349 layer_version.layer.vcs_url = request.POST["vcs_url"]
350 if "dirpath" in request.POST:
351 layer_version.dirpath = request.POST["dirpath"]
352 if "commit" in request.POST:
353 layer_version.commit = request.POST["commit"]
354 layer_version.branch = request.POST["commit"]
355 if "summary" in request.POST:
356 layer_version.layer.summary = request.POST["summary"]
357 if "description" in request.POST:
358 layer_version.layer.description = request.POST["description"]
359 if "local_source_dir" in request.POST:
360 layer_version.layer.local_source_dir = \
361 request.POST["local_source_dir"]
362
363 if "add_dep" in request.POST:
364 lvd = LayerVersionDependency(
365 layer_version=layer_version,
366 depends_on_id=request.POST["add_dep"])
367 lvd.save()
368
369 if "rm_dep" in request.POST:
370 rm_dep = LayerVersionDependency.objects.get(
371 layer_version=layer_version,
372 depends_on_id=request.POST["rm_dep"])
373 rm_dep.delete()
374
375 try:
376 layer_version.layer.save()
377 layer_version.save()
378 except Exception as e:
379 return error_response("Could not update layer version entry: %s"
380 % e)
381
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500382 return error_response("ok")
383
384 def put(self, request, *args, **kwargs):
385 """ Add a new layer
386
387 Method: PUT
388 Entry point: /xhr_layer/<project id>/
389 Args:
390 project_id, name,
391 [vcs_url, dir_path, git_ref], [local_source_dir], [layer_deps
392 (csv)]
393
394 """
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800395
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500396 try:
397 project = Project.objects.get(pk=kwargs['pid'])
398
399 layer_data = json.loads(request.body.decode('utf-8'))
400
401 # We require a unique layer name as otherwise the lists of layers
402 # becomes very confusing
403 existing_layers = \
404 project.get_all_compatible_layer_versions().values_list(
405 "layer__name",
406 flat=True)
407
408 add_to_project = False
409 layer_deps_added = []
410 if 'add_to_project' in layer_data:
411 add_to_project = True
412
413 if layer_data['name'] in existing_layers:
414 return JsonResponse({"error": "layer-name-exists"})
415
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800416 if ('local_source_dir' in layer_data):
417 # Local layer can be shared across projects. They have no 'release'
418 # and are not included in get_all_compatible_layer_versions() above
419 layer,created = Layer.objects.get_or_create(name=layer_data['name'])
420 _log("Local Layer created=%s" % created)
421 else:
422 layer = Layer.objects.create(name=layer_data['name'])
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500423
424 layer_version = Layer_Version.objects.create(
425 layer=layer,
426 project=project,
427 layer_source=LayerSource.TYPE_IMPORTED)
428
429 # Local layer
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800430 if ('local_source_dir' in layer_data): ### and layer.local_source_dir:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500431 layer.local_source_dir = layer_data['local_source_dir']
432 # git layer
433 elif 'vcs_url' in layer_data:
434 layer.vcs_url = layer_data['vcs_url']
435 layer_version.dirpath = layer_data['dir_path']
436 layer_version.commit = layer_data['git_ref']
437 layer_version.branch = layer_data['git_ref']
438
439 layer.save()
440 layer_version.save()
441
442 if add_to_project:
443 ProjectLayer.objects.get_or_create(
444 layercommit=layer_version, project=project)
445
446 # Add the layer dependencies
447 if 'layer_deps' in layer_data:
448 for layer_dep_id in layer_data['layer_deps'].split(","):
449 layer_dep = Layer_Version.objects.get(pk=layer_dep_id)
450 LayerVersionDependency.objects.get_or_create(
451 layer_version=layer_version, depends_on=layer_dep)
452
453 # Add layer deps to the project if specified
454 if add_to_project:
455 created, pl = ProjectLayer.objects.get_or_create(
456 layercommit=layer_dep, project=project)
457 layer_deps_added.append(
458 {'name': layer_dep.layer.name,
459 'layerdetailurl':
460 layer_dep.get_detailspage_url(project.pk)})
461
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800462 # Scan the layer's content and update components
463 scan_layer_content(layer,layer_version)
464
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500465 except Layer_Version.DoesNotExist:
466 return error_response("layer-dep-not-found")
467 except Project.DoesNotExist:
468 return error_response("project-not-found")
469 except KeyError:
470 return error_response("incorrect-parameters")
471
472 return JsonResponse({'error': "ok",
473 'imported_layer': {
474 'name': layer.name,
475 'layerdetailurl':
476 layer_version.get_detailspage_url()},
477 'deps_added': layer_deps_added})
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600478
479 def delete(self, request, *args, **kwargs):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500480 """ Delete an imported layer
481
482 Method: DELETE
483 Entry point: /xhr_layer/<projed id>/<layerversion_id>
484
485 """
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600486 try:
487 # We currently only allow Imported layers to be deleted
488 layer_version = Layer_Version.objects.get(
489 id=kwargs['layerversion_id'],
490 project=kwargs['pid'],
491 layer_source=LayerSource.TYPE_IMPORTED)
492 except Layer_Version.DoesNotExist:
493 return error_response("Cannot find imported layer to delete")
494
495 try:
496 ProjectLayer.objects.get(project=kwargs['pid'],
497 layercommit=layer_version).delete()
498 except ProjectLayer.DoesNotExist:
499 pass
500
501 layer_version.layer.delete()
502 layer_version.delete()
503
504 return JsonResponse({
505 "error": "ok",
506 "gotoUrl": reverse('projectlayers', args=(kwargs['pid'],))
507 })
508
509
510class XhrCustomRecipe(View):
511 """ Create a custom image recipe """
512
513 def post(self, request, *args, **kwargs):
514 """
515 Custom image recipe REST API
516
517 Entry point: /xhr_customrecipe/
518 Method: POST
519
520 Args:
521 name: name of custom recipe to create
522 project: target project id of orm.models.Project
523 base: base recipe id of orm.models.Recipe
524
525 Returns:
526 {"error": "ok",
527 "url": <url of the created recipe>}
528 or
529 {"error": <error message>}
530 """
531 # check if request has all required parameters
532 for param in ('name', 'project', 'base'):
533 if param not in request.POST:
534 return error_response("Missing parameter '%s'" % param)
535
536 # get project and baserecipe objects
537 params = {}
538 for name, model in [("project", Project),
539 ("base", Recipe)]:
540 value = request.POST[name]
541 try:
542 params[name] = model.objects.get(id=value)
543 except model.DoesNotExist:
544 return error_response("Invalid %s id %s" % (name, value))
545
546 # create custom recipe
547 try:
548
549 # Only allowed chars in name are a-z, 0-9 and -
550 if re.search(r'[^a-z|0-9|-]', request.POST["name"]):
551 return error_response("invalid-name")
552
553 custom_images = CustomImageRecipe.objects.all()
554
555 # Are there any recipes with this name already in our project?
556 existing_image_recipes_in_project = custom_images.filter(
557 name=request.POST["name"], project=params["project"])
558
559 if existing_image_recipes_in_project.count() > 0:
560 return error_response("image-already-exists")
561
562 # Are there any recipes with this name which aren't custom
563 # image recipes?
564 custom_image_ids = custom_images.values_list('id', flat=True)
565 existing_non_image_recipes = Recipe.objects.filter(
566 Q(name=request.POST["name"]) & ~Q(pk__in=custom_image_ids)
567 )
568
569 if existing_non_image_recipes.count() > 0:
570 return error_response("recipe-already-exists")
571
572 # create layer 'Custom layer' and verion if needed
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500573 layer, l_created = Layer.objects.get_or_create(
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600574 name=CustomImageRecipe.LAYER_NAME,
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500575 summary="Layer for custom recipes")
576
577 if l_created:
578 layer.local_source_dir = "toaster_created_layer"
579 layer.save()
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600580
581 # Check if we have a layer version already
582 # We don't use get_or_create here because the dirpath will change
583 # and is a required field
584 lver = Layer_Version.objects.filter(Q(project=params['project']) &
585 Q(layer=layer) &
586 Q(build=None)).last()
587 if lver is None:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500588 lver, lv_created = Layer_Version.objects.get_or_create(
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600589 project=params['project'],
590 layer=layer,
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500591 layer_source=LayerSource.TYPE_LOCAL,
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600592 dirpath="toaster_created_layer")
593
594 # Add a dependency on our layer to the base recipe's layer
595 LayerVersionDependency.objects.get_or_create(
596 layer_version=lver,
597 depends_on=params["base"].layer_version)
598
599 # Add it to our current project if needed
600 ProjectLayer.objects.get_or_create(project=params['project'],
601 layercommit=lver,
602 optional=False)
603
604 # Create the actual recipe
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500605 recipe, r_created = CustomImageRecipe.objects.get_or_create(
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600606 name=request.POST["name"],
607 base_recipe=params["base"],
608 project=params["project"],
609 layer_version=lver,
610 is_image=True)
611
612 # If we created the object then setup these fields. They may get
613 # overwritten later on and cause the get_or_create to create a
614 # duplicate if they've changed.
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500615 if r_created:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600616 recipe.file_path = request.POST["name"]
617 recipe.license = "MIT"
618 recipe.version = "0.1"
619 recipe.save()
620
621 except Error as err:
622 return error_response("Can't create custom recipe: %s" % err)
623
624 # Find the package list from the last build of this recipe/target
625 target = Target.objects.filter(Q(build__outcome=Build.SUCCEEDED) &
626 Q(build__project=params['project']) &
627 (Q(target=params['base'].name) |
628 Q(target=recipe.name))).last()
629 if target:
630 # Copy in every package
631 # We don't want these packages to be linked to anything because
632 # that underlying data may change e.g. delete a build
633 for tpackage in target.target_installed_package_set.all():
634 try:
635 built_package = tpackage.package
636 # The package had no recipe information so is a ghost
637 # package skip it
638 if built_package.recipe is None:
639 continue
640
641 config_package = CustomImagePackage.objects.get(
642 name=built_package.name)
643
644 recipe.includes_set.add(config_package)
645 except Exception as e:
646 logger.warning("Error adding package %s %s" %
647 (tpackage.package.name, e))
648 pass
649
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500650 # pre-create layer directory structure, so that other builds
651 # are not blocked by this new recipe dependecy
652 # NOTE: this is parallel code to 'localhostbecontroller.py'
653 be = BuildEnvironment.objects.all()[0]
654 layerpath = os.path.join(be.builddir,
655 CustomImageRecipe.LAYER_NAME)
656 for name in ("conf", "recipes"):
657 path = os.path.join(layerpath, name)
658 if not os.path.isdir(path):
659 os.makedirs(path)
660 # pre-create layer.conf
661 config = os.path.join(layerpath, "conf", "layer.conf")
662 if not os.path.isfile(config):
663 with open(config, "w") as conf:
664 conf.write('BBPATH .= ":${LAYERDIR}"\nBBFILES += "${LAYERDIR}/recipes/*.bb"\n')
665 # pre-create new image's recipe file
666 recipe_path = os.path.join(layerpath, "recipes", "%s.bb" %
667 recipe.name)
668 with open(recipe_path, "w") as recipef:
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800669 content = recipe.generate_recipe_file_contents()
670 if not content:
671 # Delete this incomplete image recipe object
672 recipe.delete()
673 return error_response("recipe-parent-not-exist")
674 else:
675 recipef.write(recipe.generate_recipe_file_contents())
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500676
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600677 return JsonResponse(
678 {"error": "ok",
679 "packages": recipe.get_all_packages().count(),
680 "url": reverse('customrecipe', args=(params['project'].pk,
681 recipe.id))})
682
683
684class XhrCustomRecipeId(View):
685 """
686 Set of ReST API processors working with recipe id.
687
688 Entry point: /xhr_customrecipe/<recipe_id>
689
690 Methods:
691 GET - Get details of custom image recipe
692 DELETE - Delete custom image recipe
693
694 Returns:
695 GET:
696 {"error": "ok",
697 "info": dictionary of field name -> value pairs
698 of the CustomImageRecipe model}
699 DELETE:
700 {"error": "ok"}
701 or
702 {"error": <error message>}
703 """
704 @staticmethod
705 def _get_ci_recipe(recipe_id):
706 """ Get Custom Image recipe or return an error response"""
707 try:
708 custom_recipe = \
709 CustomImageRecipe.objects.get(pk=recipe_id)
710 return custom_recipe, None
711
712 except CustomImageRecipe.DoesNotExist:
713 return None, error_response("Custom recipe with id=%s "
714 "not found" % recipe_id)
715
716 def get(self, request, *args, **kwargs):
717 custom_recipe, error = self._get_ci_recipe(kwargs['recipe_id'])
718 if error:
719 return error
720
721 if request.method == 'GET':
722 info = {"id": custom_recipe.id,
723 "name": custom_recipe.name,
724 "base_recipe_id": custom_recipe.base_recipe.id,
725 "project_id": custom_recipe.project.id}
726
727 return JsonResponse({"error": "ok", "info": info})
728
729 def delete(self, request, *args, **kwargs):
730 custom_recipe, error = self._get_ci_recipe(kwargs['recipe_id'])
731 if error:
732 return error
733
734 project = custom_recipe.project
735
736 custom_recipe.delete()
737 return JsonResponse({"error": "ok",
738 "gotoUrl": reverse("projectcustomimages",
739 args=(project.pk,))})
740
741
742class XhrCustomRecipePackages(View):
743 """
744 ReST API to add/remove packages to/from custom recipe.
745
746 Entry point: /xhr_customrecipe/<recipe_id>/packages/<package_id>
747 Methods:
748 PUT - Add package to the recipe
749 DELETE - Delete package from the recipe
750 GET - Get package information
751
752 Returns:
753 {"error": "ok"}
754 or
755 {"error": <error message>}
756 """
757 @staticmethod
758 def _get_package(package_id):
759 try:
760 package = CustomImagePackage.objects.get(pk=package_id)
761 return package, None
762 except Package.DoesNotExist:
763 return None, error_response("Package with id=%s "
764 "not found" % package_id)
765
766 def _traverse_dependents(self, next_package_id,
767 rev_deps, all_current_packages, tree_level=0):
768 """
769 Recurse through reverse dependency tree for next_package_id.
770 Limit the reverse dependency search to packages not already scanned,
771 that is, not already in rev_deps.
772 Limit the scan to a depth (tree_level) not exceeding the count of
773 all packages in the custom image, and if that depth is exceeded
774 return False, pop out of the recursion, and write a warning
775 to the log, but this is unlikely, suggesting a dependency loop
776 not caught by bitbake.
777 On return, the input/output arg rev_deps is appended with queryset
778 dictionary elements, annotated for use in the customimage template.
779 The list has unsorted, but unique elements.
780 """
781 max_dependency_tree_depth = all_current_packages.count()
782 if tree_level >= max_dependency_tree_depth:
783 logger.warning(
784 "The number of reverse dependencies "
785 "for this package exceeds " + max_dependency_tree_depth +
786 " and the remaining reverse dependencies will not be removed")
787 return True
788
789 package = CustomImagePackage.objects.get(id=next_package_id)
790 dependents = \
791 package.package_dependencies_target.annotate(
792 name=F('package__name'),
793 pk=F('package__pk'),
794 size=F('package__size'),
795 ).values("name", "pk", "size").exclude(
796 ~Q(pk__in=all_current_packages)
797 )
798
799 for pkg in dependents:
800 if pkg in rev_deps:
801 # already seen, skip dependent search
802 continue
803
804 rev_deps.append(pkg)
805 if (self._traverse_dependents(pkg["pk"], rev_deps,
806 all_current_packages,
807 tree_level+1)):
808 return True
809
810 return False
811
812 def _get_all_dependents(self, package_id, all_current_packages):
813 """
814 Returns sorted list of recursive reverse dependencies for package_id,
815 as a list of dictionary items, by recursing through dependency
816 relationships.
817 """
818 rev_deps = []
819 self._traverse_dependents(package_id, rev_deps, all_current_packages)
820 rev_deps = sorted(rev_deps, key=lambda x: x["name"])
821 return rev_deps
822
823 def get(self, request, *args, **kwargs):
824 recipe, error = XhrCustomRecipeId._get_ci_recipe(
825 kwargs['recipe_id'])
826 if error:
827 return error
828
829 # If no package_id then list all the current packages
830 if not kwargs['package_id']:
831 total_size = 0
832 packages = recipe.get_all_packages().values("id",
833 "name",
834 "version",
835 "size")
836 for package in packages:
837 package['size_formatted'] = \
838 filtered_filesizeformat(package['size'])
839 total_size += package['size']
840
841 return JsonResponse({"error": "ok",
842 "packages": list(packages),
843 "total": len(packages),
844 "total_size": total_size,
845 "total_size_formatted":
846 filtered_filesizeformat(total_size)})
847 else:
848 package, error = XhrCustomRecipePackages._get_package(
849 kwargs['package_id'])
850 if error:
851 return error
852
853 all_current_packages = recipe.get_all_packages()
854
855 # Dependencies for package which aren't satisfied by the
856 # current packages in the custom image recipe
857 deps = package.package_dependencies_source.for_target_or_none(
858 recipe.name)['packages'].annotate(
859 name=F('depends_on__name'),
860 pk=F('depends_on__pk'),
861 size=F('depends_on__size'),
862 ).values("name", "pk", "size").filter(
863 # There are two depends types we don't know why
864 (Q(dep_type=Package_Dependency.TYPE_TRDEPENDS) |
865 Q(dep_type=Package_Dependency.TYPE_RDEPENDS)) &
866 ~Q(pk__in=all_current_packages)
867 )
868
869 # Reverse dependencies which are needed by packages that are
870 # in the image. Recursive search providing all dependents,
871 # not just immediate dependents.
872 reverse_deps = self._get_all_dependents(kwargs['package_id'],
873 all_current_packages)
874 total_size_deps = 0
875 total_size_reverse_deps = 0
876
877 for dep in deps:
878 dep['size_formatted'] = \
879 filtered_filesizeformat(dep['size'])
880 total_size_deps += dep['size']
881
882 for dep in reverse_deps:
883 dep['size_formatted'] = \
884 filtered_filesizeformat(dep['size'])
885 total_size_reverse_deps += dep['size']
886
887 return JsonResponse(
888 {"error": "ok",
889 "id": package.pk,
890 "name": package.name,
891 "version": package.version,
892 "unsatisfied_dependencies": list(deps),
893 "unsatisfied_dependencies_size": total_size_deps,
894 "unsatisfied_dependencies_size_formatted":
895 filtered_filesizeformat(total_size_deps),
896 "reverse_dependencies": list(reverse_deps),
897 "reverse_dependencies_size": total_size_reverse_deps,
898 "reverse_dependencies_size_formatted":
899 filtered_filesizeformat(total_size_reverse_deps)})
900
901 def put(self, request, *args, **kwargs):
902 recipe, error = XhrCustomRecipeId._get_ci_recipe(kwargs['recipe_id'])
903 package, error = self._get_package(kwargs['package_id'])
904 if error:
905 return error
906
907 included_packages = recipe.includes_set.values_list('pk',
908 flat=True)
909
910 # If we're adding back a package which used to be included in this
911 # image all we need to do is remove it from the excludes
912 if package.pk in included_packages:
913 try:
914 recipe.excludes_set.remove(package)
915 return {"error": "ok"}
916 except Package.DoesNotExist:
917 return error_response("Package %s not found in excludes"
918 " but was in included list" %
919 package.name)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600920 else:
921 recipe.appends_set.add(package)
922 # Make sure that package is not in the excludes set
923 try:
924 recipe.excludes_set.remove(package)
925 except:
926 pass
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600927
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500928 # Add the dependencies we think will be added to the recipe
929 # as a result of appending this package.
930 # TODO this should recurse down the entire deps tree
931 for dep in package.package_dependencies_source.all_depends():
932 try:
933 cust_package = CustomImagePackage.objects.get(
934 name=dep.depends_on.name)
935
936 recipe.includes_set.add(cust_package)
937 try:
938 # When adding the pre-requisite package, make
939 # sure it's not in the excluded list from a
940 # prior removal.
941 recipe.excludes_set.remove(cust_package)
942 except package.DoesNotExist:
943 # Don't care if the package had never been excluded
944 pass
945 except:
946 logger.warning("Could not add package's suggested"
947 "dependencies to the list")
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600948 return JsonResponse({"error": "ok"})
949
950 def delete(self, request, *args, **kwargs):
951 recipe, error = XhrCustomRecipeId._get_ci_recipe(kwargs['recipe_id'])
952 package, error = self._get_package(kwargs['package_id'])
953 if error:
954 return error
955
956 try:
957 included_packages = recipe.includes_set.values_list('pk',
958 flat=True)
959 # If we're deleting a package which is included we need to
960 # Add it to the excludes list.
961 if package.pk in included_packages:
962 recipe.excludes_set.add(package)
963 else:
964 recipe.appends_set.remove(package)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600965
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500966 # remove dependencies as well
967 all_current_packages = recipe.get_all_packages()
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600968
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500969 reverse_deps_dictlist = self._get_all_dependents(
970 package.pk,
971 all_current_packages)
972
973 ids = [entry['pk'] for entry in reverse_deps_dictlist]
974 reverse_deps = CustomImagePackage.objects.filter(id__in=ids)
975 for r in reverse_deps:
976 try:
977 if r.id in included_packages:
978 recipe.excludes_set.add(r)
979 else:
980 recipe.appends_set.remove(r)
981 except:
982 pass
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600983
984 return JsonResponse({"error": "ok"})
985 except CustomImageRecipe.DoesNotExist:
986 return error_response("Tried to remove package that wasn't"
987 " present")
988
989
990class XhrProject(View):
991 """ Create, delete or edit a project
992
993 Entry point: /xhr_project/<project_id>
994 """
995 def post(self, request, *args, **kwargs):
996 """
997 Edit project control
998
999 Args:
1000 layerAdd = layer_version_id layer_version_id ...
1001 layerDel = layer_version_id layer_version_id ...
1002 projectName = new_project_name
1003 machineName = new_machine_name
1004
1005 Returns:
1006 {"error": "ok"}
1007 or
1008 {"error": <error message>}
1009 """
1010 try:
1011 prj = Project.objects.get(pk=kwargs['project_id'])
1012 except Project.DoesNotExist:
1013 return error_response("No such project")
1014
1015 # Add layers
1016 if 'layerAdd' in request.POST and len(request.POST['layerAdd']) > 0:
1017 for layer_version_id in request.POST['layerAdd'].split(','):
1018 try:
1019 lv = Layer_Version.objects.get(pk=int(layer_version_id))
1020 ProjectLayer.objects.get_or_create(project=prj,
1021 layercommit=lv)
1022 except Layer_Version.DoesNotExist:
1023 return error_response("Layer version %s asked to add "
1024 "doesn't exist" % layer_version_id)
1025
1026 # Remove layers
1027 if 'layerDel' in request.POST and len(request.POST['layerDel']) > 0:
1028 layer_version_ids = request.POST['layerDel'].split(',')
1029 ProjectLayer.objects.filter(
1030 project=prj,
1031 layercommit_id__in=layer_version_ids).delete()
1032
1033 # Project name change
1034 if 'projectName' in request.POST:
1035 prj.name = request.POST['projectName']
1036 prj.save()
1037
1038 # Machine name change
1039 if 'machineName' in request.POST:
1040 machinevar = prj.projectvariable_set.get(name="MACHINE")
1041 machinevar.value = request.POST['machineName']
1042 machinevar.save()
1043
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001044 # Distro name change
1045 if 'distroName' in request.POST:
1046 distrovar = prj.projectvariable_set.get(name="DISTRO")
1047 distrovar.value = request.POST['distroName']
1048 distrovar.save()
1049
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001050 return JsonResponse({"error": "ok"})
1051
1052 def get(self, request, *args, **kwargs):
1053 """
1054 Returns:
1055 json object representing the current project
1056 or:
1057 {"error": <error message>}
1058 """
1059
1060 try:
1061 project = Project.objects.get(pk=kwargs['project_id'])
1062 except Project.DoesNotExist:
1063 return error_response("Project %s does not exist" %
1064 kwargs['project_id'])
1065
1066 # Create the frequently built targets list
1067
1068 freqtargets = Counter(Target.objects.filter(
1069 Q(build__project=project),
1070 ~Q(build__outcome=Build.IN_PROGRESS)
1071 ).order_by("target").values_list("target", flat=True))
1072
1073 freqtargets = freqtargets.most_common(5)
1074
1075 # We now have the targets in order of frequency but if there are two
1076 # with the same frequency then we need to make sure those are in
1077 # alphabetical order without losing the frequency ordering
1078
1079 tmp = []
1080 switch = None
1081 for i, freqtartget in enumerate(freqtargets):
1082 target, count = freqtartget
1083 try:
1084 target_next, count_next = freqtargets[i+1]
1085 if count == count_next and target > target_next:
1086 switch = target
1087 continue
1088 except IndexError:
1089 pass
1090
1091 tmp.append(target)
1092
1093 if switch:
1094 tmp.append(switch)
1095 switch = None
1096
1097 freqtargets = tmp
1098
1099 layers = []
1100 for layer in project.projectlayer_set.all():
1101 layers.append({
1102 "id": layer.layercommit.pk,
1103 "name": layer.layercommit.layer.name,
1104 "vcs_url": layer.layercommit.layer.vcs_url,
1105 "local_source_dir": layer.layercommit.layer.local_source_dir,
1106 "vcs_reference": layer.layercommit.get_vcs_reference(),
1107 "url": layer.layercommit.layer.layer_index_url,
1108 "layerdetailurl": layer.layercommit.get_detailspage_url(
1109 project.pk),
Brad Bishop6e60e8b2018-02-01 10:27:11 -05001110 "xhrLayerUrl": reverse("xhr_layer",
1111 args=(project.pk,
1112 layer.layercommit.pk)),
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001113 "layersource": layer.layercommit.layer_source
1114 })
1115
1116 data = {
1117 "name": project.name,
1118 "layers": layers,
1119 "freqtargets": freqtargets,
1120 }
1121
1122 if project.release is not None:
1123 data['release'] = {
1124 "id": project.release.pk,
1125 "name": project.release.name,
1126 "description": project.release.description
1127 }
1128
1129 try:
1130 data["machine"] = {"name":
1131 project.projectvariable_set.get(
1132 name="MACHINE").value}
1133 except ProjectVariable.DoesNotExist:
1134 data["machine"] = None
1135 try:
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001136 data["distro"] = {"name":
1137 project.projectvariable_set.get(
1138 name="DISTRO").value}
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001139 except ProjectVariable.DoesNotExist:
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001140 data["distro"] = None
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001141
1142 data['error'] = "ok"
1143
1144 return JsonResponse(data)
1145
1146 def put(self, request, *args, **kwargs):
1147 # TODO create new project api
1148 return HttpResponse()
1149
1150 def delete(self, request, *args, **kwargs):
1151 """Delete a project. Cancels any builds in progress"""
1152 try:
1153 project = Project.objects.get(pk=kwargs['project_id'])
1154 # Cancel any builds in progress
1155 for br in BuildRequest.objects.filter(
1156 project=project,
1157 state=BuildRequest.REQ_INPROGRESS):
1158 XhrBuildRequest.cancel_build(br)
1159
Brad Bishop1a4b7ee2018-12-16 17:11:34 -08001160 # gather potential orphaned local layers attached to this project
1161 project_local_layer_list = []
1162 for pl in ProjectLayer.objects.filter(project=project):
1163 if pl.layercommit.layer_source == LayerSource.TYPE_IMPORTED:
1164 project_local_layer_list.append(pl.layercommit.layer)
1165
1166 # deep delete the project and its dependencies
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001167 project.delete()
1168
Brad Bishop1a4b7ee2018-12-16 17:11:34 -08001169 # delete any local layers now orphaned
1170 _log("LAYER_ORPHAN_CHECK:Check for orphaned layers")
1171 for layer in project_local_layer_list:
1172 layer_refs = Layer_Version.objects.filter(layer=layer)
1173 _log("LAYER_ORPHAN_CHECK:Ref Count for '%s' = %d" % (layer.name,len(layer_refs)))
1174 if 0 == len(layer_refs):
1175 _log("LAYER_ORPHAN_CHECK:DELETE orpahned '%s'" % (layer.name))
1176 Layer.objects.filter(pk=layer.id).delete()
1177
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001178 except Project.DoesNotExist:
1179 return error_response("Project %s does not exist" %
1180 kwargs['project_id'])
1181
1182 return JsonResponse({
1183 "error": "ok",
1184 "gotoUrl": reverse("all-projects", args=[])
1185 })
1186
1187
1188class XhrBuild(View):
1189 """ Delete a build object
1190
1191 Entry point: /xhr_build/<build_id>
1192 """
1193 def delete(self, request, *args, **kwargs):
1194 """
1195 Delete build data
1196
1197 Args:
1198 build_id = build_id
1199
1200 Returns:
1201 {"error": "ok"}
1202 or
1203 {"error": <error message>}
1204 """
1205 try:
1206 build = Build.objects.get(pk=kwargs['build_id'])
1207 project = build.project
1208 build.delete()
1209 except Build.DoesNotExist:
1210 return error_response("Build %s does not exist" %
1211 kwargs['build_id'])
1212 return JsonResponse({
1213 "error": "ok",
1214 "gotoUrl": reverse("projectbuilds", args=(project.pk,))
1215 })