api.py 45 KB

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