# # Copyright (C) 2017-2018 Red Hat, Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # """ Setup v0 of the API server v0_api() must be called to setup the API routes for Flask Status Responses ---------------- Some requests only return a status/error response. The response will be a status response with `status` set to true, or an error response with it set to false and an error message included. Example response:: { "status": true } Error response:: { "error": { "msg": "ggit-error: Failed to remove entry. File isn't in the tree - jboss.toml (-1)" }, "status": false } API Routes ---------- All of the recipes routes support the optional `branch` argument. If it is not used then the API will use the `master` branch for recipes. If you want to create a new branch use the `new` or `workspace` routes with ?branch= to store the new recipe on the new branch. `/api/v0/test` ^^^^^^^^^^^^^^ Return a test string. It is not JSON encoded. `/api/v0/status` ^^^^^^^^^^^^^^^^ Return the status of the API Server:: { "api": "0", "build": "devel", "db_supported": false, "db_version": "0", "schema_version": "0" } `/api/v0/recipes/list` ^^^^^^^^^^^^^^^^^^^^^^ List the available recipes:: { "limit": 20, "offset": 0, "recipes": [ "atlas", "development", "glusterfs", "http-server", "jboss", "kubernetes" ], "total": 6 } `/api/v0/recipes/info/` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Return the JSON representation of the recipe. This includes 3 top level objects. `changes` which lists whether or not the workspace is different from the most recent commit. `recipes` which lists the JSON representation of the recipe, and `errors` which will list any errors, like non-existant recipes. Example:: { "changes": [ { "changed": false, "name": "glusterfs" } ], "errors": [], "recipes": [ { "description": "An example GlusterFS server with samba", "modules": [ { "name": "glusterfs", "version": "3.7.*" }, { "name": "glusterfs-cli", "version": "3.7.*" } ], "name": "glusterfs", "packages": [ { "name": "2ping", "version": "3.2.1" }, { "name": "samba", "version": "4.2.*" } ], "version": "0.0.6" } ] } Error example:: { "changes": [], "errors": [ { "msg": "ggit-error: the path 'missing.toml' does not exist in the given tree (-3)", "recipe": "missing" } ], "recipes": [] } `/api/v0/recipes/changes/[?offset=0&limit=20]` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Return the commits to a recipe. By default it returns the first 20 commits, this can be changed by passing `offset` and/or `limit`. The response will include the commit hash, summary, timestamp, and optionally the revision number. The commit hash can be passed to `/api/v0/recipes/diff/` to retrieve the exact changes. Example:: { "errors": [], "limit": 20, "offset": 0, "recipes": [ { "changes": [ { "commit": "e083921a7ed1cf2eec91ad12b9ad1e70ef3470be", "message": "Recipe glusterfs, version 0.0.6 saved.", "revision": null, "timestamp": "2017-11-23T00:18:13Z" }, { "commit": "cee5f4c20fc33ea4d54bfecf56f4ad41ad15f4f3", "message": "Recipe glusterfs, version 0.0.5 saved.", "revision": null, "timestamp": "2017-11-11T01:00:28Z" }, { "commit": "29b492f26ed35d80800b536623bafc51e2f0eff2", "message": "Recipe glusterfs, version 0.0.4 saved.", "revision": null, "timestamp": "2017-11-11T00:28:30Z" }, { "commit": "03374adbf080fe34f5c6c29f2e49cc2b86958bf2", "message": "Recipe glusterfs, version 0.0.3 saved.", "revision": null, "timestamp": "2017-11-10T23:15:52Z" }, { "commit": "0e08ecbb708675bfabc82952599a1712a843779d", "message": "Recipe glusterfs, version 0.0.2 saved.", "revision": null, "timestamp": "2017-11-10T23:14:56Z" }, { "commit": "3e11eb87a63d289662cba4b1804a0947a6843379", "message": "Recipe glusterfs, version 0.0.1 saved.", "revision": null, "timestamp": "2017-11-08T00:02:47Z" } ], "name": "glusterfs", "total": 6 } ] } POST `/api/v0/recipes/new` ^^^^^^^^^^^^^^^^^^^^^^^^^^ Create a new recipe, or update an existing recipe. This supports both JSON and TOML for the recipe format. The recipe should be in the body of the request with the `Content-Type` header set to either `application/json` or `text/x-toml`. The response will be a status response with `status` set to true, or an error response with it set to false and an error message included. DELETE `/api/v0/recipes/delete/` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Delete a recipe. The recipe is deleted from the branch, and will no longer be listed by the `list` route. A recipe can be undeleted using the `undo` route to revert to a previous commit. The response will be a status response with `status` set to true, or an error response with it set to false and an error message included. POST `/api/v0/recipes/workspace` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Write a recipe to the temporary workspace. This works exactly the same as `new` except that it does not create a commit. JSON and TOML bodies are supported. The workspace is meant to be used as a temporary recipe storage for clients. It will be read by the `info` and `diff` routes if it is different from the most recent commit. The response will be a status response with `status` set to true, or an error response with it set to false and an error message included. DELETE `/api/v0/recipes/workspace/` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Remove the temporary workspace copy of a recipe. The `info` route will now return the most recent commit of the recipe. Any changes that were in the workspace will be lost. The response will be a status response with `status` set to true, or an error response with it set to false and an error message included. POST `/api/v0/recipes/undo//` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This will revert the recipe to a previous commit. The commit hash from the `changes` route can be used in this request. The response will be a status response with `status` set to true, or an error response with it set to false and an error message included. POST `/api/v0/recipes/tag/` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Tag a recipe as a new release. This uses git tags with a special format. `refs/tags///r`. Only the most recent recipe commit can be tagged. Revisions start at 1 and increment for each new tag (per-recipe). If the commit has already been tagged it will return false. The response will be a status response with `status` set to true, or an error response with it set to false and an error message included. `/api/v0/recipes/diff///` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Return the differences between two commits, or the workspace. The commit hash from the `changes` response can be used here, or several special strings: - NEWEST will select the newest git commit. This works for `from_commit` or `to_commit` - WORKSPACE will select the workspace copy. This can only be used in `to_commit` eg. `/api/v0/recipes/diff/glusterfs/NEWEST/WORKSPACE` will return the differences between the most recent git commit and the contents of the workspace. Each entry in the response's diff object contains the old recipe value and the new one. If old is null and new is set, then it was added. If new is null and old is set, then it was removed. If both are set, then it was changed. The old/new entries will have the name of the recipe field that was changed. This can be one of: Name, Description, Version, Module, or Package. The contents for these will be the old/new values for them. In the example below the version was changed and the ping package was added. Example:: { "diff": [ { "new": { "Version": "0.0.6" }, "old": { "Version": "0.0.5" } }, { "new": { "Package": { "name": "ping", "version": "3.2.1" } }, "old": null } ] } `/api/v0/recipes/freeze/` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Return a JSON representation of the recipe with the package and module versions set to the exact versions chosen by depsolving the recipe. Example:: { "errors": [], "recipes": [ { "recipe": { "description": "An example GlusterFS server with samba", "modules": [ { "name": "glusterfs", "version": "3.8.4-18.4.el7.x86_64" }, { "name": "glusterfs-cli", "version": "3.8.4-18.4.el7.x86_64" } ], "name": "glusterfs", "packages": [ { "name": "ping", "version": "2:3.2.1-2.el7.noarch" }, { "name": "samba", "version": "4.6.2-8.el7.x86_64" } ], "version": "0.0.6" } } ] } `/api/v0/recipes/depsolve/` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Depsolve the recipe using yum, return the recipe used, and the NEVRAs of the packages chosen to satisfy the recipe's requirements. The response will include a list of results, with the full dependency list in `dependencies`, the NEVRAs for the recipe's direct modules and packages in `modules`, and any error will be in `errors`. Example:: { "errors": [], "recipes": [ { "dependencies": [ { "arch": "noarch", "epoch": "0", "name": "2ping", "release": "2.el7", "version": "3.2.1" }, { "arch": "x86_64", "epoch": "0", "name": "acl", "release": "12.el7", "version": "2.2.51" }, { "arch": "x86_64", "epoch": "0", "name": "audit-libs", "release": "3.el7", "version": "2.7.6" }, { "arch": "x86_64", "epoch": "0", "name": "avahi-libs", "release": "17.el7", "version": "0.6.31" }, ... ], "modules": [ { "arch": "noarch", "epoch": "0", "name": "2ping", "release": "2.el7", "version": "3.2.1" }, { "arch": "x86_64", "epoch": "0", "name": "glusterfs", "release": "18.4.el7", "version": "3.8.4" }, ... ], "recipe": { "description": "An example GlusterFS server with samba", "modules": [ { "name": "glusterfs", "version": "3.7.*" }, ... } } ] } `/api/v0/projects/list[?offset=0&limit=20]` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ List all of the available projects. By default this returns the first 20 items, but this can be changed by setting the `offset` and `limit` arguments. Example:: { "limit": 20, "offset": 0, "projects": [ { "description": "0 A.D. (pronounced \"zero ey-dee\") is a ...", "homepage": "http://play0ad.com", "name": "0ad", "summary": "Cross-Platform RTS Game of Ancient Warfare", "upstream_vcs": "UPSTREAM_VCS" }, ... ], "total": 21770 } `/api/v0/projects/info/` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Return information about the comma-separated list of projects. It includes the description of the package along with the list of available builds. Example:: { "projects": [ { "builds": [ { "arch": "x86_64", "build_config_ref": "BUILD_CONFIG_REF", "build_env_ref": "BUILD_ENV_REF", "build_time": "2017-03-01T08:39:23", "changelog": "- restore incremental backups correctly, files ...", "epoch": "2", "metadata": {}, "release": "32.el7", "source": { "license": "GPLv3+", "metadata": {}, "source_ref": "SOURCE_REF", "version": "1.26" } } ], "description": "The GNU tar program saves many ...", "homepage": "http://www.gnu.org/software/tar/", "name": "tar", "summary": "A GNU file archiving program", "upstream_vcs": "UPSTREAM_VCS" } ] } `/api/v0/projects/depsolve/` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Depsolve the comma-separated list of projects and return the list of NEVRAs needed to satisfy the request. Example:: { "projects": [ { "arch": "noarch", "epoch": "0", "name": "basesystem", "release": "7.el7", "version": "10.0" }, { "arch": "x86_64", "epoch": "0", "name": "bash", "release": "28.el7", "version": "4.2.46" }, { "arch": "x86_64", "epoch": "0", "name": "filesystem", "release": "21.el7", "version": "3.2" }, ... ] } `/api/v0/modules/list[?offset=0&limit=20]` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Return a list of all of the available modules. This includes the name and the group_type, which is always "rpm" for lorax-composer. By default this returns the first 20 items. This can be changed by setting the `offset` and `limit` arguments. Example:: { "limit": 20, "modules": [ { "group_type": "rpm", "name": "0ad" }, { "group_type": "rpm", "name": "0ad-data" }, { "group_type": "rpm", "name": "0install" }, { "group_type": "rpm", "name": "2048-cli" }, ... ] "total": 21770 } `/api/v0/modules/list/[?offset=0&limit=20]` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Return the list of comma-separated modules. Output is the same as `/modules/list` Example:: { "limit": 20, "modules": [ { "group_type": "rpm", "name": "tar" } ], "offset": 0, "total": 1 } `/api/v0/modules/info/` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Return the module's dependencies, and the information about the module. Example:: { "modules": [ { "dependencies": [ { "arch": "noarch", "epoch": "0", "name": "basesystem", "release": "7.el7", "version": "10.0" }, { "arch": "x86_64", "epoch": "0", "name": "bash", "release": "28.el7", "version": "4.2.46" }, ... ], "description": "The GNU tar program saves ...", "homepage": "http://www.gnu.org/software/tar/", "name": "tar", "summary": "A GNU file archiving program", "upstream_vcs": "UPSTREAM_VCS" } ] } """ import logging log = logging.getLogger("lorax-composer") from flask import jsonify, request # Use pykickstart to calculate disk image size from pykickstart.parser import KickstartParser from pykickstart.version import makeVersion, RHEL7 from pylorax.api.crossdomain import crossdomain from pylorax.api.projects import projects_list, projects_info, projects_depsolve, dep_evra from pylorax.api.projects import modules_list, modules_info, ProjectsError from pylorax.api.recipes import list_branch_files, read_recipe_commit, recipe_filename, list_commits from pylorax.api.recipes import recipe_from_dict, recipe_from_toml, commit_recipe, delete_recipe, revert_recipe from pylorax.api.recipes import tag_recipe_commit, recipe_diff, Recipe, RecipePackage, RecipeModule from pylorax.api.workspace import workspace_read, workspace_write, workspace_delete from pylorax.creator import DRACUT_DEFAULT, mount_boot_part_over_root from pylorax.creator import make_appliance, make_image, make_livecd, make_live_images from pylorax.creator import make_runtime, make_squashfs from pylorax.imgutils import copytree from pylorax.imgutils import Mount, PartitionMount, umount from pylorax.installer import InstallError from pylorax.sysutils import joinpaths # The API functions don't actually get called by any code here # pylint: disable=unused-variable # no-virt mode doesn't need libvirt, so make it optional try: import libvirt except ImportError: libvirt = None def take_limits(iterable, offset, limit): """ Apply offset and limit to an iterable object :param iterable: The object to limit :type iterable: iter :param offset: The number of items to skip :type offset: int :param limit: The total number of items to return :type limit: int :returns: A subset of the iterable """ return iterable[offset:][:limit] def v0_api(api): # Note that Sphinx will not generate documentations for any of these. @api.route("/api/v0/test") @crossdomain(origin="*") def v0_test(): return "API v0 test" @api.route("/api/v0/status") @crossdomain(origin="*") def v0_status(): return jsonify(build="devel", api="0", db_version="0", schema_version="0", db_supported=False) @api.route("/api/v0/recipes/list") @crossdomain(origin="*") def v0_recipes_list(): """List the available recipes on a branch.""" branch = request.args.get("branch", "master") try: limit = int(request.args.get("limit", "20")) offset = int(request.args.get("offset", "0")) except ValueError as e: return jsonify(error={"msg":str(e)}), 400 with api.config["GITLOCK"].lock: recipes = take_limits(map(lambda f: f[:-5], list_branch_files(api.config["GITLOCK"].repo, branch)), offset, limit) return jsonify(recipes=recipes, limit=limit, offset=offset, total=len(recipes)) @api.route("/api/v0/recipes/info/") @crossdomain(origin="*") def v0_recipes_info(recipe_names): """Return the contents of the recipe, or a list of recipes""" branch = request.args.get("branch", "master") recipes = [] changes = [] errors = [] for recipe_name in [n.strip() for n in recipe_names.split(",")]: exceptions = [] # Get the workspace version (if it exists) try: with api.config["GITLOCK"].lock: ws_recipe = workspace_read(api.config["GITLOCK"].repo, branch, recipe_name) except Exception as e: ws_recipe = None exceptions.append(str(e)) log.error("(v0_recipes_info) %s", str(e)) # Get the git version (if it exists) try: with api.config["GITLOCK"].lock: git_recipe = read_recipe_commit(api.config["GITLOCK"].repo, branch, recipe_name) except Exception as e: git_recipe = None exceptions.append(str(e)) log.error("(v0_recipes_info) %s", str(e)) if not ws_recipe and not git_recipe: # Neither recipe, return an error errors.append({"recipe":recipe_name, "msg":", ".join(exceptions)}) elif ws_recipe and not git_recipe: # No git recipe, return the workspace recipe changes.append({"name":recipe_name, "changed":True}) recipes.append(ws_recipe) elif not ws_recipe and git_recipe: # No workspace recipe, no change, return the git recipe changes.append({"name":recipe_name, "changed":False}) recipes.append(git_recipe) else: # Both exist, maybe changed, return the workspace recipe changes.append({"name":recipe_name, "changed":ws_recipe != git_recipe}) recipes.append(ws_recipe) # Sort all the results by case-insensitive recipe name changes = sorted(changes, key=lambda c: c["name"].lower()) recipes = sorted(recipes, key=lambda r: r["name"].lower()) errors = sorted(errors, key=lambda e: e["recipe"].lower()) return jsonify(changes=changes, recipes=recipes, errors=errors) @api.route("/api/v0/recipes/changes/") @crossdomain(origin="*") def v0_recipes_changes(recipe_names): """Return the changes to a recipe or list of recipes""" branch = request.args.get("branch", "master") try: limit = int(request.args.get("limit", "20")) offset = int(request.args.get("offset", "0")) except ValueError as e: return jsonify(error={"msg":str(e)}), 400 recipes = [] errors = [] for recipe_name in [n.strip() for n in recipe_names.split(",")]: filename = recipe_filename(recipe_name) try: with api.config["GITLOCK"].lock: commits = take_limits(list_commits(api.config["GITLOCK"].repo, branch, filename), offset, limit) except Exception as e: errors.append({"recipe":recipe_name, "msg":e}) log.error("(v0_recipes_changes) %s", str(e)) else: recipes.append({"name":recipe_name, "changes":commits, "total":len(commits)}) recipes = sorted(recipes, key=lambda r: r["name"].lower()) errors = sorted(errors, key=lambda e: e["recipe"].lower()) return jsonify(recipes=recipes, errors=errors, offset=offset, limit=limit) @api.route("/api/v0/recipes/new", methods=["POST"]) @crossdomain(origin="*") def v0_recipes_new(): """Commit a new recipe""" branch = request.args.get("branch", "master") try: if request.headers['Content-Type'] == "text/x-toml": recipe = recipe_from_toml(request.data) else: recipe = recipe_from_dict(request.get_json(cache=False)) with api.config["GITLOCK"].lock: commit_recipe(api.config["GITLOCK"].repo, branch, recipe) # Read the recipe with new version and write it to the workspace recipe = read_recipe_commit(api.config["GITLOCK"].repo, branch, recipe["name"]) workspace_write(api.config["GITLOCK"].repo, branch, recipe) except Exception as e: log.error("(v0_recipes_new) %s", str(e)) return jsonify(status=False, error={"msg":str(e)}), 400 else: return jsonify(status=True) @api.route("/api/v0/recipes/delete/", methods=["DELETE"]) @crossdomain(origin="*") def v0_recipes_delete(recipe_name): """Delete a recipe from git""" branch = request.args.get("branch", "master") try: with api.config["GITLOCK"].lock: delete_recipe(api.config["GITLOCK"].repo, branch, recipe_name) except Exception as e: log.error("(v0_recipes_delete) %s", str(e)) return jsonify(status=False, error={"msg":str(e)}), 400 else: return jsonify(status=True) @api.route("/api/v0/recipes/workspace", methods=["POST"]) @crossdomain(origin="*") def v0_recipes_workspace(): """Write a recipe to the workspace""" branch = request.args.get("branch", "master") try: if request.headers['Content-Type'] == "text/x-toml": recipe = recipe_from_toml(request.data) else: recipe = recipe_from_dict(request.get_json(cache=False)) with api.config["GITLOCK"].lock: workspace_write(api.config["GITLOCK"].repo, branch, recipe) except Exception as e: log.error("(v0_recipes_workspace) %s", str(e)) return jsonify(status=False, error={"msg":str(e)}), 400 else: return jsonify(status=True) @api.route("/api/v0/recipes/workspace/", methods=["DELETE"]) @crossdomain(origin="*") def v0_recipes_delete_workspace(recipe_name): """Delete a recipe from the workspace""" branch = request.args.get("branch", "master") try: with api.config["GITLOCK"].lock: workspace_delete(api.config["GITLOCK"].repo, branch, recipe_name) except Exception as e: log.error("(v0_recipes_delete_workspace) %s", str(e)) return jsonify(status=False, error={"msg":str(e)}), 400 else: return jsonify(status=True) @api.route("/api/v0/recipes/undo//", methods=["POST"]) @crossdomain(origin="*") def v0_recipes_undo(recipe_name, commit): """Undo changes to a recipe by reverting to a previous commit.""" branch = request.args.get("branch", "master") try: with api.config["GITLOCK"].lock: revert_recipe(api.config["GITLOCK"].repo, branch, recipe_name, commit) # Read the new recipe and write it to the workspace recipe = read_recipe_commit(api.config["GITLOCK"].repo, branch, recipe_name) workspace_write(api.config["GITLOCK"].repo, branch, recipe) except Exception as e: log.error("(v0_recipes_undo) %s", str(e)) return jsonify(status=False, error={"msg":str(e)}), 400 else: return jsonify(status=True) @api.route("/api/v0/recipes/tag/", methods=["POST"]) @crossdomain(origin="*") def v0_recipes_tag(recipe_name): """Tag a recipe's latest recipe commit as a 'revision'""" branch = request.args.get("branch", "master") try: with api.config["GITLOCK"].lock: tag_recipe_commit(api.config["GITLOCK"].repo, branch, recipe_name) except Exception as e: log.error("(v0_recipes_tag) %s", str(e)) return jsonify(status=False, error={"msg":str(e)}), 400 else: return jsonify(status=True) @api.route("/api/v0/recipes/diff///") @crossdomain(origin="*") def v0_recipes_diff(recipe_name, from_commit, to_commit): """Return the differences between two commits of a recipe""" branch = request.args.get("branch", "master") try: if from_commit == "NEWEST": with api.config["GITLOCK"].lock: old_recipe = read_recipe_commit(api.config["GITLOCK"].repo, branch, recipe_name) else: with api.config["GITLOCK"].lock: old_recipe = read_recipe_commit(api.config["GITLOCK"].repo, branch, recipe_name, from_commit) except Exception as e: log.error("(v0_recipes_diff) %s", str(e)) return jsonify(error={"msg":str(e)}), 400 try: if to_commit == "WORKSPACE": with api.config["GITLOCK"].lock: new_recipe = workspace_read(api.config["GITLOCK"].repo, branch, recipe_name) elif to_commit == "NEWEST": with api.config["GITLOCK"].lock: new_recipe = read_recipe_commit(api.config["GITLOCK"].repo, branch, recipe_name) else: with api.config["GITLOCK"].lock: new_recipe = read_recipe_commit(api.config["GITLOCK"].repo, branch, recipe_name, to_commit) except Exception as e: log.error("(v0_recipes_diff) %s", str(e)) return jsonify(error={"msg":str(e)}), 400 diff = recipe_diff(old_recipe, new_recipe) return jsonify(diff=diff) @api.route("/api/v0/recipes/freeze/") @crossdomain(origin="*") def v0_recipes_freeze(recipe_names): """Return the recipe with the exact modules and packages selected by depsolve""" branch = request.args.get("branch", "master") recipes = [] errors = [] for recipe_name in [n.strip() for n in sorted(recipe_names.split(","), key=lambda n: n.lower())]: # get the recipe # Get the workspace version (if it exists) recipe = None try: with api.config["GITLOCK"].lock: recipe = workspace_read(api.config["GITLOCK"].repo, branch, recipe_name) except Exception: pass if not recipe: # No workspace version, get the git version (if it exists) try: with api.config["GITLOCK"].lock: recipe = read_recipe_commit(api.config["GITLOCK"].repo, branch, recipe_name) except Exception as e: errors.append({"recipe":recipe_name, "msg":str(e)}) log.error("(v0_recipes_freeze) %s", str(e)) # No recipe found, skip it. if not recipe: errors.append({"recipe":recipe_name, "msg":"Recipe not found"}) continue # Combine modules and packages and depsolve the list # TODO include the version/glob in the depsolving module_names = map(lambda m: m["name"], recipe["modules"] or []) package_names = map(lambda p: p["name"], recipe["packages"] or []) projects = sorted(set(module_names+package_names), key=lambda n: n.lower()) deps = [] try: with api.config["YUMLOCK"].lock: deps = projects_depsolve(api.config["YUMLOCK"].yb, projects) except ProjectsError as e: errors.append({"recipe":recipe_name, "msg":str(e)}) log.error("(v0_recipes_freeze) %s", str(e)) # Change the recipe's modules and packages to use the depsolved version new_modules = [] new_packages = [] for dep in deps: if dep["name"] in package_names: new_packages.append(RecipePackage(dep["name"], dep_evra(dep))) elif dep["name"] in module_names: new_modules.append(RecipeModule(dep["name"], dep_evra(dep))) recipes.append({"recipe":Recipe(recipe["name"], recipe["description"], recipe["version"], new_modules, new_packages)}) return jsonify(recipes=recipes, errors=errors) @api.route("/api/v0/recipes/depsolve/") @crossdomain(origin="*") def v0_recipes_depsolve(recipe_names): """Return the dependencies for a recipe""" branch = request.args.get("branch", "master") recipes = [] errors = [] for recipe_name in [n.strip() for n in sorted(recipe_names.split(","), key=lambda n: n.lower())]: # get the recipe # Get the workspace version (if it exists) recipe = None try: with api.config["GITLOCK"].lock: recipe = workspace_read(api.config["GITLOCK"].repo, branch, recipe_name) except Exception: pass if not recipe: # No workspace version, get the git version (if it exists) try: with api.config["GITLOCK"].lock: recipe = read_recipe_commit(api.config["GITLOCK"].repo, branch, recipe_name) except Exception as e: errors.append({"recipe":recipe_name, "msg":str(e)}) log.error("(v0_recipes_depsolve) %s", str(e)) # No recipe found, skip it. if not recipe: errors.append({"recipe":recipe_name, "msg":"Recipe not found"}) continue # Combine modules and packages and depsolve the list # TODO include the version/glob in the depsolving module_names = map(lambda m: m["name"], recipe["modules"] or []) package_names = map(lambda p: p["name"], recipe["packages"] or []) projects = sorted(set(module_names+package_names), key=lambda n: n.lower()) deps = [] try: with api.config["YUMLOCK"].lock: deps = projects_depsolve(api.config["YUMLOCK"].yb, projects) except ProjectsError as e: errors.append({"recipe":recipe_name, "msg":str(e)}) log.error("(v0_recipes_depsolve) %s", str(e)) # Get the NEVRA's of the modules and projects, add as "modules" modules = [] for dep in deps: if dep["name"] in projects: modules.append(dep) modules = sorted(modules, key=lambda m: m["name"].lower()) recipes.append({"recipe":recipe, "dependencies":deps, "modules":modules}) return jsonify(recipes=recipes, errors=errors) @api.route("/api/v0/projects/list") @crossdomain(origin="*") def v0_projects_list(): """List all of the available projects/packages""" try: limit = int(request.args.get("limit", "20")) offset = int(request.args.get("offset", "0")) except ValueError as e: return jsonify(error={"msg":str(e)}), 400 try: with api.config["YUMLOCK"].lock: available = projects_list(api.config["YUMLOCK"].yb) except ProjectsError as e: log.error("(v0_projects_list) %s", str(e)) return jsonify(error={"msg":str(e)}), 400 projects = take_limits(available, offset, limit) return jsonify(projects=projects, offset=offset, limit=limit, total=len(available)) @api.route("/api/v0/projects/info/") @crossdomain(origin="*") def v0_projects_info(project_names): """Return detailed information about the listed projects""" try: with api.config["YUMLOCK"].lock: projects = projects_info(api.config["YUMLOCK"].yb, project_names.split(",")) except ProjectsError as e: log.error("(v0_projects_info) %s", str(e)) return jsonify(error={"msg":str(e)}), 400 return jsonify(projects=projects) @api.route("/api/v0/projects/depsolve/") @crossdomain(origin="*") def v0_projects_depsolve(project_names): """Return detailed information about the listed projects""" try: with api.config["YUMLOCK"].lock: deps = projects_depsolve(api.config["YUMLOCK"].yb, project_names.split(",")) except ProjectsError as e: log.error("(v0_projects_depsolve) %s", str(e)) return jsonify(error={"msg":str(e)}), 400 return jsonify(projects=deps) @api.route("/api/v0/modules/list") @api.route("/api/v0/modules/list/") @crossdomain(origin="*") def v0_modules_list(module_names=None): """List available modules, filtering by module_names""" try: limit = int(request.args.get("limit", "20")) offset = int(request.args.get("offset", "0")) except ValueError as e: return jsonify(error={"msg":str(e)}), 400 if module_names: module_names = module_names.split(",") try: with api.config["YUMLOCK"].lock: available = modules_list(api.config["YUMLOCK"].yb, module_names) except ProjectsError as e: log.error("(v0_modules_list) %s", str(e)) return jsonify(error={"msg":str(e)}), 400 modules = take_limits(available, offset, limit) return jsonify(modules=modules, offset=offset, limit=limit, total=len(available)) @api.route("/api/v0/modules/info/") @crossdomain(origin="*") def v0_modules_info(module_names): """Return detailed information about the listed modules""" try: with api.config["YUMLOCK"].lock: modules = modules_info(api.config["YUMLOCK"].yb, module_names.split(",")) except ProjectsError as e: log.error("(v0_modules_info) %s", str(e)) return jsonify(error={"msg":str(e)}), 400 return jsonify(modules=modules)