lorax/src/pylorax/api/v0.py
Brian C. Lane 6a5ff22e61 Add documentation for the API routes.
Use Sphinx on a CentOS7/RHEL7 system to generate the docs with 'make docs'
2018-01-15 14:14:35 -08:00

1102 lines
39 KiB
Python

#
# 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 <http://www.gnu.org/licenses/>.
#
""" 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=<branch-name> 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/<recipe_names>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
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/<recipe_names>[?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/<recipe_name>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
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/<recipe_name>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
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/<recipe_name>/<commit>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
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/<recipe_name>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Tag a recipe as a new release. This uses git tags with a special format.
`refs/tags/<branch>/<filename>/r<revision>`. 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/<recipe_name>/<from_commit>/<to_commit>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
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/<recipe_names>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
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/<recipe_names>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
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/<project_names>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
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/<project_names>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
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/<module_names>[?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/<module_names>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
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/<recipe_names>")
@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/<recipe_names>")
@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/<recipe_name>", 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/<recipe_name>", 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/<recipe_name>/<commit>", 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/<recipe_name>", 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/<recipe_name>/<from_commit>/<to_commit>")
@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/<recipe_names>")
@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/<recipe_names>")
@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/<project_names>")
@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/<project_names>")
@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/<module_names>")
@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/<module_names>")
@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)