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