blob: e367bd910e24ee874a45a97fe52dc2fbd7ca3398 [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
Patrick Williams169d7bc2024-01-05 11:33:25 -060014import glob
Patrick Williamsc0f7c042017-02-23 20:41:17 -060015from collections import Counter
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050016
Patrick Williamsc0f7c042017-02-23 20:41:17 -060017from orm.models import Project, ProjectTarget, Build, Layer_Version
18from orm.models import LayerVersionDependency, LayerSource, ProjectLayer
19from orm.models import Recipe, CustomImageRecipe, CustomImagePackage
20from orm.models import Layer, Target, Package, Package_Dependency
21from orm.models import ProjectVariable
Brad Bishopd7bf8c12018-02-25 22:55:05 -050022from bldcontrol.models import BuildRequest, BuildEnvironment
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050023from bldcontrol import bbcontroller
Patrick Williamsc0f7c042017-02-23 20:41:17 -060024
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050025from django.http import HttpResponse, JsonResponse
26from django.views.generic import View
Andrew Geissler82c905d2020-04-13 13:39:40 -050027from django.urls import reverse
Patrick Williamsc0f7c042017-02-23 20:41:17 -060028from django.db.models import Q, F
29from django.db import Error
30from toastergui.templatetags.projecttags import filtered_filesizeformat
Brad Bishop1a4b7ee2018-12-16 17:11:34 -080031
32# development/debugging support
33verbose = 2
34def _log(msg):
35 if 1 == verbose:
36 print(msg)
37 elif 2 == verbose:
38 f1=open('/tmp/toaster.log', 'a')
39 f1.write("|" + msg + "|\n" )
40 f1.close()
Patrick Williamsc0f7c042017-02-23 20:41:17 -060041
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
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800140class XhrProjectUpdate(View):
141
142 def get(self, request, *args, **kwargs):
143 return HttpResponse()
144
145 def post(self, request, *args, **kwargs):
146 """
147 Project Update
148
149 Entry point: /xhr_projectupdate/<project_id>
150 Method: POST
151
152 Args:
153 pid: pid of project to update
154
155 Returns:
156 {"error": "ok"}
157 or
158 {"error": <error message>}
159 """
160
161 project = Project.objects.get(pk=kwargs['pid'])
162 logger.debug("ProjectUpdateCallback:project.pk=%d,project.builddir=%s" % (project.pk,project.builddir))
163
164 if 'do_update' in request.POST:
165
166 # Extract any default image recipe
167 if 'default_image' in request.POST:
168 project.set_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE,str(request.POST['default_image']))
169 else:
170 project.set_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE,'')
171
172 logger.debug("ProjectUpdateCallback:Chain to the build request")
173
174 # Chain to the build request
175 xhrBuildRequest = XhrBuildRequest()
176 return xhrBuildRequest.post(request, *args, **kwargs)
177
178 logger.warning("ERROR:XhrProjectUpdate")
179 response = HttpResponse()
180 response.status_code = 500
181 return response
182
183class XhrSetDefaultImageUrl(View):
184
185 def get(self, request, *args, **kwargs):
186 return HttpResponse()
187
188 def post(self, request, *args, **kwargs):
189 """
190 Project Update
191
192 Entry point: /xhr_setdefaultimage/<project_id>
193 Method: POST
194
195 Args:
196 pid: pid of project to update default image
197
198 Returns:
199 {"error": "ok"}
200 or
201 {"error": <error message>}
202 """
203
204 project = Project.objects.get(pk=kwargs['pid'])
205 logger.debug("XhrSetDefaultImageUrl:project.pk=%d" % (project.pk))
206
207 # set any default image recipe
208 if 'targets' in request.POST:
209 default_target = str(request.POST['targets'])
210 project.set_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE,default_target)
211 logger.debug("XhrSetDefaultImageUrl,project.pk=%d,project.builddir=%s" % (project.pk,project.builddir))
212 return error_response('ok')
213
214 logger.warning("ERROR:XhrSetDefaultImageUrl")
215 response = HttpResponse()
216 response.status_code = 500
217 return response
218
219
220#
221# Layer Management
222#
223# Rules for 'local_source_dir' layers
224# * Layers must have a unique name in the Layers table
225# * A 'local_source_dir' layer is supposed to be shared
226# by all projects that use it, so that it can have the
227# same logical name
228# * Each project that uses a layer will have its own
229# LayerVersion and Project Layer for it
Patrick Williams169d7bc2024-01-05 11:33:25 -0600230# * During the Project delete process, when the last
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800231# LayerVersion for a 'local_source_dir' layer is deleted
232# then the Layer record is deleted to remove orphans
233#
234
235def scan_layer_content(layer,layer_version):
236 # if this is a local layer directory, we can immediately scan its content
Patrick Williams169d7bc2024-01-05 11:33:25 -0600237 if os.path.isdir(layer.local_source_dir):
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800238 try:
239 # recipes-*/*/*.bb
Patrick Williams169d7bc2024-01-05 11:33:25 -0600240 recipes_list = glob.glob(os.path.join(layer.local_source_dir, 'recipes-*/*/*.bb'))
241 for recipe in recipes_list:
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800242 for recipe in recipes_list.split('\n'):
243 recipe_path = recipe[recipe.rfind('recipes-'):]
244 recipe_name = recipe[recipe.rfind('/')+1:].replace('.bb','')
245 recipe_ver = recipe_name.rfind('_')
246 if recipe_ver > 0:
247 recipe_name = recipe_name[0:recipe_ver]
248 if recipe_name:
249 ro, created = Recipe.objects.get_or_create(
250 layer_version=layer_version,
251 name=recipe_name
252 )
253 if created:
254 ro.file_path = recipe_path
255 ro.summary = 'Recipe %s from layer %s' % (recipe_name,layer.name)
256 ro.description = ro.summary
257 ro.save()
258
259 except Exception as e:
260 logger.warning("ERROR:scan_layer_content: %s" % e)
Patrick Williams169d7bc2024-01-05 11:33:25 -0600261 else:
262 logger.warning("ERROR: wrong path given")
263 raise KeyError("local_source_dir")
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800264
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600265class XhrLayer(View):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500266 """ Delete, Get, Add and Update Layer information
267
268 Methods: GET POST DELETE PUT
269 """
270
271 def get(self, request, *args, **kwargs):
272 """
273 Get layer information
274
275 Method: GET
276 Entry point: /xhr_layer/<project id>/<layerversion_id>
277 """
278
279 try:
280 layer_version = Layer_Version.objects.get(
281 pk=kwargs['layerversion_id'])
282
283 project = Project.objects.get(pk=kwargs['pid'])
284
285 project_layers = ProjectLayer.objects.filter(
286 project=project).values_list("layercommit_id",
287 flat=True)
288
289 ret = {
290 'error': 'ok',
291 'id': layer_version.pk,
292 'name': layer_version.layer.name,
293 'layerdetailurl':
294 layer_version.get_detailspage_url(project.pk),
295 'vcs_ref': layer_version.get_vcs_reference(),
296 'vcs_url': layer_version.layer.vcs_url,
297 'local_source_dir': layer_version.layer.local_source_dir,
298 'layerdeps': {
299 "list": [
300 {
301 "id": dep.id,
302 "name": dep.layer.name,
303 "layerdetailurl":
304 dep.get_detailspage_url(project.pk),
305 "vcs_url": dep.layer.vcs_url,
306 "vcs_reference": dep.get_vcs_reference()
307 }
308 for dep in layer_version.get_alldeps(project.id)]
309 },
310 'projectlayers': list(project_layers)
311 }
312
313 return JsonResponse(ret)
314 except Layer_Version.DoesNotExist:
315 error_response("No such layer")
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600316
317 def post(self, request, *args, **kwargs):
318 """
319 Update a layer
320
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600321 Method: POST
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500322 Entry point: /xhr_layer/<layerversion_id>
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600323
324 Args:
325 vcs_url, dirpath, commit, up_branch, summary, description,
326 local_source_dir
327
328 add_dep = append a layerversion_id as a dependency
329 rm_dep = remove a layerversion_id as a depedency
330 Returns:
331 {"error": "ok"}
332 or
333 {"error": <error message>}
334 """
335
336 try:
337 # We currently only allow Imported layers to be edited
338 layer_version = Layer_Version.objects.get(
339 id=kwargs['layerversion_id'],
340 project=kwargs['pid'],
341 layer_source=LayerSource.TYPE_IMPORTED)
342
343 except Layer_Version.DoesNotExist:
344 return error_response("Cannot find imported layer to update")
345
346 if "vcs_url" in request.POST:
347 layer_version.layer.vcs_url = request.POST["vcs_url"]
348 if "dirpath" in request.POST:
349 layer_version.dirpath = request.POST["dirpath"]
350 if "commit" in request.POST:
351 layer_version.commit = request.POST["commit"]
352 layer_version.branch = request.POST["commit"]
353 if "summary" in request.POST:
354 layer_version.layer.summary = request.POST["summary"]
355 if "description" in request.POST:
356 layer_version.layer.description = request.POST["description"]
357 if "local_source_dir" in request.POST:
358 layer_version.layer.local_source_dir = \
359 request.POST["local_source_dir"]
360
361 if "add_dep" in request.POST:
362 lvd = LayerVersionDependency(
363 layer_version=layer_version,
364 depends_on_id=request.POST["add_dep"])
365 lvd.save()
366
367 if "rm_dep" in request.POST:
368 rm_dep = LayerVersionDependency.objects.get(
369 layer_version=layer_version,
370 depends_on_id=request.POST["rm_dep"])
371 rm_dep.delete()
372
373 try:
374 layer_version.layer.save()
375 layer_version.save()
376 except Exception as e:
377 return error_response("Could not update layer version entry: %s"
378 % e)
379
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500380 return error_response("ok")
381
382 def put(self, request, *args, **kwargs):
383 """ Add a new layer
384
385 Method: PUT
386 Entry point: /xhr_layer/<project id>/
387 Args:
388 project_id, name,
389 [vcs_url, dir_path, git_ref], [local_source_dir], [layer_deps
390 (csv)]
391
392 """
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800393
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500394 try:
395 project = Project.objects.get(pk=kwargs['pid'])
396
397 layer_data = json.loads(request.body.decode('utf-8'))
398
399 # We require a unique layer name as otherwise the lists of layers
400 # becomes very confusing
401 existing_layers = \
402 project.get_all_compatible_layer_versions().values_list(
403 "layer__name",
404 flat=True)
405
406 add_to_project = False
407 layer_deps_added = []
408 if 'add_to_project' in layer_data:
409 add_to_project = True
410
411 if layer_data['name'] in existing_layers:
412 return JsonResponse({"error": "layer-name-exists"})
413
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800414 if ('local_source_dir' in layer_data):
415 # Local layer can be shared across projects. They have no 'release'
416 # and are not included in get_all_compatible_layer_versions() above
417 layer,created = Layer.objects.get_or_create(name=layer_data['name'])
418 _log("Local Layer created=%s" % created)
419 else:
420 layer = Layer.objects.create(name=layer_data['name'])
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500421
422 layer_version = Layer_Version.objects.create(
423 layer=layer,
424 project=project,
425 layer_source=LayerSource.TYPE_IMPORTED)
426
427 # Local layer
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800428 if ('local_source_dir' in layer_data): ### and layer.local_source_dir:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500429 layer.local_source_dir = layer_data['local_source_dir']
430 # git layer
431 elif 'vcs_url' in layer_data:
432 layer.vcs_url = layer_data['vcs_url']
433 layer_version.dirpath = layer_data['dir_path']
434 layer_version.commit = layer_data['git_ref']
435 layer_version.branch = layer_data['git_ref']
436
437 layer.save()
438 layer_version.save()
439
440 if add_to_project:
441 ProjectLayer.objects.get_or_create(
442 layercommit=layer_version, project=project)
443
444 # Add the layer dependencies
445 if 'layer_deps' in layer_data:
446 for layer_dep_id in layer_data['layer_deps'].split(","):
447 layer_dep = Layer_Version.objects.get(pk=layer_dep_id)
448 LayerVersionDependency.objects.get_or_create(
449 layer_version=layer_version, depends_on=layer_dep)
450
451 # Add layer deps to the project if specified
452 if add_to_project:
453 created, pl = ProjectLayer.objects.get_or_create(
454 layercommit=layer_dep, project=project)
455 layer_deps_added.append(
456 {'name': layer_dep.layer.name,
457 'layerdetailurl':
458 layer_dep.get_detailspage_url(project.pk)})
459
Patrick Williams169d7bc2024-01-05 11:33:25 -0600460 # Only scan_layer_content if layer is local
461 if layer_data.get('local_source_dir', None):
462 # Scan the layer's content and update components
463 scan_layer_content(layer,layer_version)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800464
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")
Patrick Williams169d7bc2024-01-05 11:33:25 -0600469 except KeyError as e:
470 _log("KeyError: %s" % e)
471 return error_response(f"incorrect-parameters")
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500472
473 return JsonResponse({'error': "ok",
474 'imported_layer': {
475 'name': layer.name,
476 'layerdetailurl':
477 layer_version.get_detailspage_url()},
478 'deps_added': layer_deps_added})
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600479
480 def delete(self, request, *args, **kwargs):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500481 """ Delete an imported layer
482
483 Method: DELETE
484 Entry point: /xhr_layer/<projed id>/<layerversion_id>
485
486 """
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600487 try:
488 # We currently only allow Imported layers to be deleted
489 layer_version = Layer_Version.objects.get(
490 id=kwargs['layerversion_id'],
491 project=kwargs['pid'],
492 layer_source=LayerSource.TYPE_IMPORTED)
493 except Layer_Version.DoesNotExist:
494 return error_response("Cannot find imported layer to delete")
495
496 try:
497 ProjectLayer.objects.get(project=kwargs['pid'],
498 layercommit=layer_version).delete()
499 except ProjectLayer.DoesNotExist:
500 pass
501
502 layer_version.layer.delete()
503 layer_version.delete()
504
505 return JsonResponse({
506 "error": "ok",
507 "gotoUrl": reverse('projectlayers', args=(kwargs['pid'],))
508 })
509
510
511class XhrCustomRecipe(View):
512 """ Create a custom image recipe """
513
514 def post(self, request, *args, **kwargs):
515 """
516 Custom image recipe REST API
517
518 Entry point: /xhr_customrecipe/
519 Method: POST
520
521 Args:
522 name: name of custom recipe to create
523 project: target project id of orm.models.Project
524 base: base recipe id of orm.models.Recipe
525
526 Returns:
527 {"error": "ok",
528 "url": <url of the created recipe>}
529 or
530 {"error": <error message>}
531 """
532 # check if request has all required parameters
533 for param in ('name', 'project', 'base'):
534 if param not in request.POST:
535 return error_response("Missing parameter '%s'" % param)
536
537 # get project and baserecipe objects
538 params = {}
539 for name, model in [("project", Project),
540 ("base", Recipe)]:
541 value = request.POST[name]
542 try:
543 params[name] = model.objects.get(id=value)
544 except model.DoesNotExist:
545 return error_response("Invalid %s id %s" % (name, value))
546
547 # create custom recipe
548 try:
549
550 # Only allowed chars in name are a-z, 0-9 and -
551 if re.search(r'[^a-z|0-9|-]', request.POST["name"]):
552 return error_response("invalid-name")
553
554 custom_images = CustomImageRecipe.objects.all()
555
556 # Are there any recipes with this name already in our project?
557 existing_image_recipes_in_project = custom_images.filter(
558 name=request.POST["name"], project=params["project"])
559
560 if existing_image_recipes_in_project.count() > 0:
561 return error_response("image-already-exists")
562
563 # Are there any recipes with this name which aren't custom
564 # image recipes?
565 custom_image_ids = custom_images.values_list('id', flat=True)
566 existing_non_image_recipes = Recipe.objects.filter(
567 Q(name=request.POST["name"]) & ~Q(pk__in=custom_image_ids)
568 )
569
570 if existing_non_image_recipes.count() > 0:
571 return error_response("recipe-already-exists")
572
573 # create layer 'Custom layer' and verion if needed
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500574 layer, l_created = Layer.objects.get_or_create(
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600575 name=CustomImageRecipe.LAYER_NAME,
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500576 summary="Layer for custom recipes")
577
578 if l_created:
579 layer.local_source_dir = "toaster_created_layer"
580 layer.save()
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600581
582 # Check if we have a layer version already
583 # We don't use get_or_create here because the dirpath will change
584 # and is a required field
585 lver = Layer_Version.objects.filter(Q(project=params['project']) &
586 Q(layer=layer) &
587 Q(build=None)).last()
588 if lver is None:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500589 lver, lv_created = Layer_Version.objects.get_or_create(
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600590 project=params['project'],
591 layer=layer,
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500592 layer_source=LayerSource.TYPE_LOCAL,
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600593 dirpath="toaster_created_layer")
594
595 # Add a dependency on our layer to the base recipe's layer
596 LayerVersionDependency.objects.get_or_create(
597 layer_version=lver,
598 depends_on=params["base"].layer_version)
599
600 # Add it to our current project if needed
601 ProjectLayer.objects.get_or_create(project=params['project'],
602 layercommit=lver,
603 optional=False)
604
605 # Create the actual recipe
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500606 recipe, r_created = CustomImageRecipe.objects.get_or_create(
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600607 name=request.POST["name"],
608 base_recipe=params["base"],
609 project=params["project"],
610 layer_version=lver,
611 is_image=True)
612
613 # If we created the object then setup these fields. They may get
614 # overwritten later on and cause the get_or_create to create a
615 # duplicate if they've changed.
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500616 if r_created:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600617 recipe.file_path = request.POST["name"]
618 recipe.license = "MIT"
619 recipe.version = "0.1"
620 recipe.save()
621
622 except Error as err:
623 return error_response("Can't create custom recipe: %s" % err)
624
625 # Find the package list from the last build of this recipe/target
626 target = Target.objects.filter(Q(build__outcome=Build.SUCCEEDED) &
627 Q(build__project=params['project']) &
628 (Q(target=params['base'].name) |
629 Q(target=recipe.name))).last()
630 if target:
631 # Copy in every package
632 # We don't want these packages to be linked to anything because
633 # that underlying data may change e.g. delete a build
634 for tpackage in target.target_installed_package_set.all():
635 try:
636 built_package = tpackage.package
637 # The package had no recipe information so is a ghost
638 # package skip it
639 if built_package.recipe is None:
640 continue
641
642 config_package = CustomImagePackage.objects.get(
643 name=built_package.name)
644
645 recipe.includes_set.add(config_package)
646 except Exception as e:
647 logger.warning("Error adding package %s %s" %
648 (tpackage.package.name, e))
649 pass
650
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500651 # pre-create layer directory structure, so that other builds
652 # are not blocked by this new recipe dependecy
653 # NOTE: this is parallel code to 'localhostbecontroller.py'
654 be = BuildEnvironment.objects.all()[0]
655 layerpath = os.path.join(be.builddir,
656 CustomImageRecipe.LAYER_NAME)
657 for name in ("conf", "recipes"):
658 path = os.path.join(layerpath, name)
659 if not os.path.isdir(path):
660 os.makedirs(path)
661 # pre-create layer.conf
662 config = os.path.join(layerpath, "conf", "layer.conf")
663 if not os.path.isfile(config):
664 with open(config, "w") as conf:
665 conf.write('BBPATH .= ":${LAYERDIR}"\nBBFILES += "${LAYERDIR}/recipes/*.bb"\n')
666 # pre-create new image's recipe file
667 recipe_path = os.path.join(layerpath, "recipes", "%s.bb" %
668 recipe.name)
669 with open(recipe_path, "w") as recipef:
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800670 content = recipe.generate_recipe_file_contents()
671 if not content:
672 # Delete this incomplete image recipe object
673 recipe.delete()
674 return error_response("recipe-parent-not-exist")
675 else:
676 recipef.write(recipe.generate_recipe_file_contents())
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500677
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600678 return JsonResponse(
679 {"error": "ok",
680 "packages": recipe.get_all_packages().count(),
681 "url": reverse('customrecipe', args=(params['project'].pk,
682 recipe.id))})
683
684
685class XhrCustomRecipeId(View):
686 """
687 Set of ReST API processors working with recipe id.
688
689 Entry point: /xhr_customrecipe/<recipe_id>
690
691 Methods:
692 GET - Get details of custom image recipe
693 DELETE - Delete custom image recipe
694
695 Returns:
696 GET:
697 {"error": "ok",
698 "info": dictionary of field name -> value pairs
699 of the CustomImageRecipe model}
700 DELETE:
701 {"error": "ok"}
702 or
703 {"error": <error message>}
704 """
705 @staticmethod
706 def _get_ci_recipe(recipe_id):
707 """ Get Custom Image recipe or return an error response"""
708 try:
709 custom_recipe = \
710 CustomImageRecipe.objects.get(pk=recipe_id)
711 return custom_recipe, None
712
713 except CustomImageRecipe.DoesNotExist:
714 return None, error_response("Custom recipe with id=%s "
715 "not found" % recipe_id)
716
717 def get(self, request, *args, **kwargs):
718 custom_recipe, error = self._get_ci_recipe(kwargs['recipe_id'])
719 if error:
720 return error
721
722 if request.method == 'GET':
723 info = {"id": custom_recipe.id,
724 "name": custom_recipe.name,
725 "base_recipe_id": custom_recipe.base_recipe.id,
726 "project_id": custom_recipe.project.id}
727
728 return JsonResponse({"error": "ok", "info": info})
729
730 def delete(self, request, *args, **kwargs):
731 custom_recipe, error = self._get_ci_recipe(kwargs['recipe_id'])
732 if error:
733 return error
734
735 project = custom_recipe.project
736
737 custom_recipe.delete()
738 return JsonResponse({"error": "ok",
739 "gotoUrl": reverse("projectcustomimages",
740 args=(project.pk,))})
741
742
743class XhrCustomRecipePackages(View):
744 """
745 ReST API to add/remove packages to/from custom recipe.
746
747 Entry point: /xhr_customrecipe/<recipe_id>/packages/<package_id>
748 Methods:
749 PUT - Add package to the recipe
750 DELETE - Delete package from the recipe
751 GET - Get package information
752
753 Returns:
754 {"error": "ok"}
755 or
756 {"error": <error message>}
757 """
758 @staticmethod
759 def _get_package(package_id):
760 try:
761 package = CustomImagePackage.objects.get(pk=package_id)
762 return package, None
763 except Package.DoesNotExist:
764 return None, error_response("Package with id=%s "
765 "not found" % package_id)
766
767 def _traverse_dependents(self, next_package_id,
768 rev_deps, all_current_packages, tree_level=0):
769 """
770 Recurse through reverse dependency tree for next_package_id.
771 Limit the reverse dependency search to packages not already scanned,
772 that is, not already in rev_deps.
773 Limit the scan to a depth (tree_level) not exceeding the count of
774 all packages in the custom image, and if that depth is exceeded
775 return False, pop out of the recursion, and write a warning
776 to the log, but this is unlikely, suggesting a dependency loop
777 not caught by bitbake.
778 On return, the input/output arg rev_deps is appended with queryset
779 dictionary elements, annotated for use in the customimage template.
780 The list has unsorted, but unique elements.
781 """
782 max_dependency_tree_depth = all_current_packages.count()
783 if tree_level >= max_dependency_tree_depth:
784 logger.warning(
785 "The number of reverse dependencies "
786 "for this package exceeds " + max_dependency_tree_depth +
787 " and the remaining reverse dependencies will not be removed")
788 return True
789
790 package = CustomImagePackage.objects.get(id=next_package_id)
791 dependents = \
792 package.package_dependencies_target.annotate(
793 name=F('package__name'),
794 pk=F('package__pk'),
795 size=F('package__size'),
796 ).values("name", "pk", "size").exclude(
797 ~Q(pk__in=all_current_packages)
798 )
799
800 for pkg in dependents:
801 if pkg in rev_deps:
802 # already seen, skip dependent search
803 continue
804
805 rev_deps.append(pkg)
806 if (self._traverse_dependents(pkg["pk"], rev_deps,
807 all_current_packages,
808 tree_level+1)):
809 return True
810
811 return False
812
813 def _get_all_dependents(self, package_id, all_current_packages):
814 """
815 Returns sorted list of recursive reverse dependencies for package_id,
816 as a list of dictionary items, by recursing through dependency
817 relationships.
818 """
819 rev_deps = []
820 self._traverse_dependents(package_id, rev_deps, all_current_packages)
821 rev_deps = sorted(rev_deps, key=lambda x: x["name"])
822 return rev_deps
823
824 def get(self, request, *args, **kwargs):
825 recipe, error = XhrCustomRecipeId._get_ci_recipe(
826 kwargs['recipe_id'])
827 if error:
828 return error
829
830 # If no package_id then list all the current packages
831 if not kwargs['package_id']:
832 total_size = 0
833 packages = recipe.get_all_packages().values("id",
834 "name",
835 "version",
836 "size")
837 for package in packages:
838 package['size_formatted'] = \
839 filtered_filesizeformat(package['size'])
840 total_size += package['size']
841
842 return JsonResponse({"error": "ok",
843 "packages": list(packages),
844 "total": len(packages),
845 "total_size": total_size,
846 "total_size_formatted":
847 filtered_filesizeformat(total_size)})
848 else:
849 package, error = XhrCustomRecipePackages._get_package(
850 kwargs['package_id'])
851 if error:
852 return error
853
854 all_current_packages = recipe.get_all_packages()
855
856 # Dependencies for package which aren't satisfied by the
857 # current packages in the custom image recipe
858 deps = package.package_dependencies_source.for_target_or_none(
859 recipe.name)['packages'].annotate(
860 name=F('depends_on__name'),
861 pk=F('depends_on__pk'),
862 size=F('depends_on__size'),
863 ).values("name", "pk", "size").filter(
864 # There are two depends types we don't know why
865 (Q(dep_type=Package_Dependency.TYPE_TRDEPENDS) |
866 Q(dep_type=Package_Dependency.TYPE_RDEPENDS)) &
867 ~Q(pk__in=all_current_packages)
868 )
869
870 # Reverse dependencies which are needed by packages that are
871 # in the image. Recursive search providing all dependents,
872 # not just immediate dependents.
873 reverse_deps = self._get_all_dependents(kwargs['package_id'],
874 all_current_packages)
875 total_size_deps = 0
876 total_size_reverse_deps = 0
877
878 for dep in deps:
879 dep['size_formatted'] = \
880 filtered_filesizeformat(dep['size'])
881 total_size_deps += dep['size']
882
883 for dep in reverse_deps:
884 dep['size_formatted'] = \
885 filtered_filesizeformat(dep['size'])
886 total_size_reverse_deps += dep['size']
887
888 return JsonResponse(
889 {"error": "ok",
890 "id": package.pk,
891 "name": package.name,
892 "version": package.version,
893 "unsatisfied_dependencies": list(deps),
894 "unsatisfied_dependencies_size": total_size_deps,
895 "unsatisfied_dependencies_size_formatted":
896 filtered_filesizeformat(total_size_deps),
897 "reverse_dependencies": list(reverse_deps),
898 "reverse_dependencies_size": total_size_reverse_deps,
899 "reverse_dependencies_size_formatted":
900 filtered_filesizeformat(total_size_reverse_deps)})
901
902 def put(self, request, *args, **kwargs):
903 recipe, error = XhrCustomRecipeId._get_ci_recipe(kwargs['recipe_id'])
904 package, error = self._get_package(kwargs['package_id'])
905 if error:
906 return error
907
908 included_packages = recipe.includes_set.values_list('pk',
909 flat=True)
910
911 # If we're adding back a package which used to be included in this
912 # image all we need to do is remove it from the excludes
913 if package.pk in included_packages:
914 try:
915 recipe.excludes_set.remove(package)
916 return {"error": "ok"}
917 except Package.DoesNotExist:
918 return error_response("Package %s not found in excludes"
919 " but was in included list" %
920 package.name)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600921 else:
922 recipe.appends_set.add(package)
923 # Make sure that package is not in the excludes set
924 try:
925 recipe.excludes_set.remove(package)
926 except:
927 pass
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600928
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500929 # Add the dependencies we think will be added to the recipe
930 # as a result of appending this package.
931 # TODO this should recurse down the entire deps tree
932 for dep in package.package_dependencies_source.all_depends():
933 try:
934 cust_package = CustomImagePackage.objects.get(
935 name=dep.depends_on.name)
936
937 recipe.includes_set.add(cust_package)
938 try:
939 # When adding the pre-requisite package, make
940 # sure it's not in the excluded list from a
941 # prior removal.
942 recipe.excludes_set.remove(cust_package)
943 except package.DoesNotExist:
944 # Don't care if the package had never been excluded
945 pass
946 except:
947 logger.warning("Could not add package's suggested"
948 "dependencies to the list")
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600949 return JsonResponse({"error": "ok"})
950
951 def delete(self, request, *args, **kwargs):
952 recipe, error = XhrCustomRecipeId._get_ci_recipe(kwargs['recipe_id'])
953 package, error = self._get_package(kwargs['package_id'])
954 if error:
955 return error
956
957 try:
958 included_packages = recipe.includes_set.values_list('pk',
959 flat=True)
960 # If we're deleting a package which is included we need to
961 # Add it to the excludes list.
962 if package.pk in included_packages:
963 recipe.excludes_set.add(package)
964 else:
965 recipe.appends_set.remove(package)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600966
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500967 # remove dependencies as well
968 all_current_packages = recipe.get_all_packages()
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600969
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500970 reverse_deps_dictlist = self._get_all_dependents(
971 package.pk,
972 all_current_packages)
973
974 ids = [entry['pk'] for entry in reverse_deps_dictlist]
975 reverse_deps = CustomImagePackage.objects.filter(id__in=ids)
976 for r in reverse_deps:
977 try:
978 if r.id in included_packages:
979 recipe.excludes_set.add(r)
980 else:
981 recipe.appends_set.remove(r)
982 except:
983 pass
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600984
985 return JsonResponse({"error": "ok"})
986 except CustomImageRecipe.DoesNotExist:
987 return error_response("Tried to remove package that wasn't"
988 " present")
989
990
991class XhrProject(View):
992 """ Create, delete or edit a project
993
994 Entry point: /xhr_project/<project_id>
995 """
996 def post(self, request, *args, **kwargs):
997 """
998 Edit project control
999
1000 Args:
1001 layerAdd = layer_version_id layer_version_id ...
1002 layerDel = layer_version_id layer_version_id ...
1003 projectName = new_project_name
1004 machineName = new_machine_name
1005
1006 Returns:
1007 {"error": "ok"}
1008 or
1009 {"error": <error message>}
1010 """
1011 try:
1012 prj = Project.objects.get(pk=kwargs['project_id'])
1013 except Project.DoesNotExist:
1014 return error_response("No such project")
1015
1016 # Add layers
1017 if 'layerAdd' in request.POST and len(request.POST['layerAdd']) > 0:
1018 for layer_version_id in request.POST['layerAdd'].split(','):
1019 try:
1020 lv = Layer_Version.objects.get(pk=int(layer_version_id))
1021 ProjectLayer.objects.get_or_create(project=prj,
1022 layercommit=lv)
1023 except Layer_Version.DoesNotExist:
1024 return error_response("Layer version %s asked to add "
1025 "doesn't exist" % layer_version_id)
1026
1027 # Remove layers
1028 if 'layerDel' in request.POST and len(request.POST['layerDel']) > 0:
1029 layer_version_ids = request.POST['layerDel'].split(',')
1030 ProjectLayer.objects.filter(
1031 project=prj,
1032 layercommit_id__in=layer_version_ids).delete()
1033
1034 # Project name change
1035 if 'projectName' in request.POST:
1036 prj.name = request.POST['projectName']
1037 prj.save()
1038
1039 # Machine name change
1040 if 'machineName' in request.POST:
1041 machinevar = prj.projectvariable_set.get(name="MACHINE")
1042 machinevar.value = request.POST['machineName']
1043 machinevar.save()
1044
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001045 # Distro name change
1046 if 'distroName' in request.POST:
1047 distrovar = prj.projectvariable_set.get(name="DISTRO")
1048 distrovar.value = request.POST['distroName']
1049 distrovar.save()
1050
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001051 return JsonResponse({"error": "ok"})
1052
1053 def get(self, request, *args, **kwargs):
1054 """
1055 Returns:
1056 json object representing the current project
1057 or:
1058 {"error": <error message>}
1059 """
1060
1061 try:
1062 project = Project.objects.get(pk=kwargs['project_id'])
1063 except Project.DoesNotExist:
1064 return error_response("Project %s does not exist" %
1065 kwargs['project_id'])
1066
1067 # Create the frequently built targets list
1068
1069 freqtargets = Counter(Target.objects.filter(
1070 Q(build__project=project),
1071 ~Q(build__outcome=Build.IN_PROGRESS)
1072 ).order_by("target").values_list("target", flat=True))
1073
1074 freqtargets = freqtargets.most_common(5)
1075
1076 # We now have the targets in order of frequency but if there are two
1077 # with the same frequency then we need to make sure those are in
1078 # alphabetical order without losing the frequency ordering
1079
1080 tmp = []
1081 switch = None
1082 for i, freqtartget in enumerate(freqtargets):
1083 target, count = freqtartget
1084 try:
1085 target_next, count_next = freqtargets[i+1]
1086 if count == count_next and target > target_next:
1087 switch = target
1088 continue
1089 except IndexError:
1090 pass
1091
1092 tmp.append(target)
1093
1094 if switch:
1095 tmp.append(switch)
1096 switch = None
1097
1098 freqtargets = tmp
1099
1100 layers = []
1101 for layer in project.projectlayer_set.all():
1102 layers.append({
1103 "id": layer.layercommit.pk,
1104 "name": layer.layercommit.layer.name,
1105 "vcs_url": layer.layercommit.layer.vcs_url,
1106 "local_source_dir": layer.layercommit.layer.local_source_dir,
1107 "vcs_reference": layer.layercommit.get_vcs_reference(),
1108 "url": layer.layercommit.layer.layer_index_url,
1109 "layerdetailurl": layer.layercommit.get_detailspage_url(
1110 project.pk),
Brad Bishop6e60e8b2018-02-01 10:27:11 -05001111 "xhrLayerUrl": reverse("xhr_layer",
1112 args=(project.pk,
1113 layer.layercommit.pk)),
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001114 "layersource": layer.layercommit.layer_source
1115 })
1116
1117 data = {
1118 "name": project.name,
1119 "layers": layers,
1120 "freqtargets": freqtargets,
1121 }
1122
1123 if project.release is not None:
1124 data['release'] = {
1125 "id": project.release.pk,
1126 "name": project.release.name,
1127 "description": project.release.description
1128 }
1129
1130 try:
1131 data["machine"] = {"name":
1132 project.projectvariable_set.get(
1133 name="MACHINE").value}
1134 except ProjectVariable.DoesNotExist:
1135 data["machine"] = None
1136 try:
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001137 data["distro"] = {"name":
1138 project.projectvariable_set.get(
1139 name="DISTRO").value}
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001140 except ProjectVariable.DoesNotExist:
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001141 data["distro"] = None
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001142
1143 data['error'] = "ok"
1144
1145 return JsonResponse(data)
1146
1147 def put(self, request, *args, **kwargs):
1148 # TODO create new project api
1149 return HttpResponse()
1150
1151 def delete(self, request, *args, **kwargs):
1152 """Delete a project. Cancels any builds in progress"""
1153 try:
1154 project = Project.objects.get(pk=kwargs['project_id'])
1155 # Cancel any builds in progress
1156 for br in BuildRequest.objects.filter(
1157 project=project,
1158 state=BuildRequest.REQ_INPROGRESS):
1159 XhrBuildRequest.cancel_build(br)
1160
Brad Bishop1a4b7ee2018-12-16 17:11:34 -08001161 # gather potential orphaned local layers attached to this project
1162 project_local_layer_list = []
1163 for pl in ProjectLayer.objects.filter(project=project):
1164 if pl.layercommit.layer_source == LayerSource.TYPE_IMPORTED:
1165 project_local_layer_list.append(pl.layercommit.layer)
1166
1167 # deep delete the project and its dependencies
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001168 project.delete()
1169
Brad Bishop1a4b7ee2018-12-16 17:11:34 -08001170 # delete any local layers now orphaned
1171 _log("LAYER_ORPHAN_CHECK:Check for orphaned layers")
1172 for layer in project_local_layer_list:
1173 layer_refs = Layer_Version.objects.filter(layer=layer)
1174 _log("LAYER_ORPHAN_CHECK:Ref Count for '%s' = %d" % (layer.name,len(layer_refs)))
1175 if 0 == len(layer_refs):
1176 _log("LAYER_ORPHAN_CHECK:DELETE orpahned '%s'" % (layer.name))
1177 Layer.objects.filter(pk=layer.id).delete()
1178
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001179 except Project.DoesNotExist:
1180 return error_response("Project %s does not exist" %
1181 kwargs['project_id'])
1182
1183 return JsonResponse({
1184 "error": "ok",
1185 "gotoUrl": reverse("all-projects", args=[])
1186 })
1187
1188
1189class XhrBuild(View):
1190 """ Delete a build object
1191
1192 Entry point: /xhr_build/<build_id>
1193 """
1194 def delete(self, request, *args, **kwargs):
1195 """
1196 Delete build data
1197
1198 Args:
1199 build_id = build_id
1200
1201 Returns:
1202 {"error": "ok"}
1203 or
1204 {"error": <error message>}
1205 """
1206 try:
1207 build = Build.objects.get(pk=kwargs['build_id'])
1208 project = build.project
1209 build.delete()
1210 except Build.DoesNotExist:
1211 return error_response("Build %s does not exist" %
1212 kwargs['build_id'])
1213 return JsonResponse({
1214 "error": "ok",
1215 "gotoUrl": reverse("projectbuilds", args=(project.pk,))
1216 })