Add /compose/info route to retrieve details about a compose

The results is a JSON string with the following information:

* id - The uuid of the comoposition
* config - containing the configuration settings used to run Anaconda
* recipe - The depsolved recipe used to generate the kickstart
* commit - The (local) git commit hash for the recipe used
* deps - The NEVRA of all of the dependencies used in the composition
* compose_type - The type of output generated (tar, iso, etc.)
* queue_status - The final status of the composition (FINISHED or FAILED)
This commit is contained in:
Brian C. Lane 2018-02-05 14:27:24 -08:00
parent d3a9ec3002
commit ed03ac7524
3 changed files with 147 additions and 23 deletions

View File

@ -41,6 +41,7 @@ from uuid import uuid4
from pylorax.api.projects import projects_depsolve, dep_nevra
from pylorax.api.projects import ProjectsError
from pylorax.api.recipes import read_recipe_and_id
from pylorax.imgutils import default_image_name
from pylorax.sysutils import joinpaths
@ -75,7 +76,7 @@ def repo_to_ks(r, url="url"):
return cmd
def start_build(cfg, yumlock, recipe, compose_type):
def start_build(cfg, yumlock, gitlock, branch, recipe_name, compose_type):
""" Start the build
:param cfg: Configuration object
@ -96,6 +97,9 @@ def start_build(cfg, yumlock, recipe, compose_type):
if compose_type not in compose_types(share_dir):
raise RuntimeError("Invalid compose type (%s), must be one of %s" % (compose_type, compose_types(share_dir)))
with gitlock.lock:
(commit_id, recipe) = read_recipe_and_id(gitlock.repo, branch, recipe_name)
# 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 [])
@ -114,6 +118,11 @@ def start_build(cfg, yumlock, recipe, compose_type):
results_dir = joinpaths(lib_dir, "results", build_id)
os.makedirs(results_dir)
# Write the recipe commit hash
commit_path = joinpaths(results_dir, "COMMIT")
with open(commit_path, "w") as f:
f.write(commit_id)
# Write the original recipe
recipe_path = joinpaths(results_dir, "recipe.toml")
with open(recipe_path, "w") as f:
@ -131,10 +140,9 @@ def start_build(cfg, yumlock, recipe, compose_type):
ks_template = open(ks_template_path, "r").read()
# Write out the dependencies to the results dir
deps_path = joinpaths(results_dir, "deps.txt")
deps_path = joinpaths(results_dir, "deps.toml")
with open(deps_path, "w") as f:
for d in deps:
f.write(dep_nevra(d)+"\n")
f.write(toml.dumps({"packages":deps}).encode("UTF-8"))
# Create the final kickstart with repos and package list
ks_path = joinpaths(results_dir, "final-kickstart.ks")

View File

@ -122,6 +122,17 @@ def make_compose(cfg, results_dir):
log.debug("Install finished, chowning results to %s:%s", user, group)
subprocess.call(["chown", "-R", "%s:%s" % (user, group), results_dir])
def get_compose_type(results_dir):
""" Return the type of composition.
"""
# Should only be 2 kickstarts, the final-kickstart.ks and the template
t = [os.path.basename(ks)[:-3] for ks in glob(joinpaths(results_dir, "*.ks"))
if "final-kickstart" not in ks]
if len(t) != 1:
raise RuntimeError("Cannot find ks template for build %s" % os.path.basename(results_dir))
return t[0]
def compose_detail(results_dir):
""" Return details about the build."""
@ -134,15 +145,12 @@ def compose_detail(results_dir):
mtime = os.stat(joinpaths(results_dir, "STATUS")).st_mtime
recipe = recipe_from_file(joinpaths(results_dir, "recipe.toml"))
# Should only be 2 kickstarts, the final-kickstart.ks and the template
types = [os.path.basename(ks)[:-3] for ks in glob(joinpaths(results_dir, "*.ks"))
if "final-kickstart" not in ks]
if len(types) != 1:
raise RuntimeError("Cannot find ks template for build %s" % build_id)
compose_type = get_compose_type(results_dir)
return {"id": build_id,
"status": status,
"timestamp":mtime,
"queue_status": status,
"timestamp": mtime,
"compose_type": compose_type,
"recipe": recipe["name"],
"version": recipe["version"]
}
@ -218,3 +226,58 @@ def uuid_delete(cfg, uuid):
raise RuntimeError("Directory length is too short: %s" % uuid_dir)
shutil.rmtree(uuid_dir)
return True
def uuid_info(cfg, uuid):
"""Return information about the composition
:param cfg: Configuration settings
:type cfg: ComposerConfig
:param uuid: The UUID of the build
:type uuid: str
:returns: dictionary of information about the composition
:rtype: dict
This will return a dict with the following fields populated:
* id - The uuid of the comoposition
* config - containing the configuration settings used to run Anaconda
* recipe - The depsolved recipe used to generate the kickstart
* commit - The (local) git commit hash for the recipe used
* deps - The NEVRA of all of the dependencies used in the composition
* compose_type - The type of output generated (tar, iso, etc.)
* queue_status - The final status of the composition (FINISHED or FAILED)
"""
uuid_dir = joinpaths(cfg.get("composer", "lib_dir"), "results", uuid)
if not os.path.exists(uuid_dir):
raise RuntimeError("%s is not a valid build_id" % uuid)
# Load the compose configuration
cfg_path = joinpaths(uuid_dir, "config.toml")
if not os.path.exists(cfg_path):
raise RuntimeError("Missing config.toml for %s" % uuid)
cfg_dict = toml.loads(open(cfg_path, "r").read())
frozen_path = joinpaths(uuid_dir, "frozen.toml")
if not os.path.exists(frozen_path):
raise RuntimeError("Missing frozen.toml for %s" % uuid)
frozen_dict = toml.loads(open(frozen_path, "r").read())
deps_path = joinpaths(uuid_dir, "deps.toml")
if not os.path.exists(deps_path):
raise RuntimeError("Missing deps.toml for %s" % uuid)
deps_dict = toml.loads(open(deps_path, "r").read())
compose_type = get_compose_type(uuid_dir)
status = open(joinpaths(uuid_dir, "STATUS")).read().strip()
commit_path = joinpaths(uuid_dir, "COMMIT")
commit_id = open(commit_path, "r").read().strip()
return {"id": uuid,
"config": cfg_dict,
"recipe": frozen_dict,
"commit": commit_id,
"deps": deps_dict,
"compose_type": compose_type,
"queue_status": status
}

View File

@ -736,6 +736,54 @@ DELETE `/api/v0/compose/delete/<uuids>`
]
}
`/api/v0/compose/info/<uuid>`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Get detailed information about the compose. The returned JSON string will
contain the following information:
* id - The uuid of the comoposition
* config - containing the configuration settings used to run Anaconda
* recipe - The depsolved recipe used to generate the kickstart
* commit - The (local) git commit hash for the recipe used
* deps - The NEVRA of all of the dependencies used in the composition
* compose_type - The type of output generated (tar, iso, etc.)
* queue_status - The final status of the composition (FINISHED or FAILED)
Example::
{
"commit": "7078e521a54b12eae31c3fd028680da7a0815a4d",
"compose_type": "tar",
"config": {
"anaconda_args": "",
"armplatform": "",
"compress_args": [],
"compression": "xz",
"image_name": "root.tar.xz",
...
},
"deps": {
"packages": [
{
"arch": "x86_64",
"epoch": "0",
"name": "acl",
"release": "14.el7",
"version": "2.2.51"
}
]
},
"id": "c30b7d80-523b-4a23-ad52-61b799739ce8",
"queue_status": "FINISHED",
"recipe": {
"description": "An example kubernetes master",
...
}
}
"""
import logging
@ -747,7 +795,7 @@ from pylorax.api.compose import start_build, compose_types
from pylorax.api.crossdomain import crossdomain
from pylorax.api.projects import projects_list, projects_info, projects_depsolve
from pylorax.api.projects import modules_list, modules_info, ProjectsError
from pylorax.api.queue import queue_status, build_status, uuid_delete, uuid_status
from pylorax.api.queue import queue_status, build_status, uuid_delete, uuid_status, uuid_info
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
@ -1231,15 +1279,9 @@ def v0_api(api):
if errors:
return jsonify(status=False, error={"msg":"\n".join(errors)}), 400
# 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:
log.error("Problem reading recipe %s: %s", recipe_name, str(e))
return jsonify(status=False, error={"msg":str(e)}), 400
try:
build_id = start_build(api.config["COMPOSER_CFG"], api.config["YUMLOCK"], recipe, compose_type)
build_id = start_build(api.config["COMPOSER_CFG"], api.config["YUMLOCK"], api.config["GITLOCK"],
branch, recipe_name, compose_type)
except Exception as e:
return jsonify(status=False, error={"msg":str(e)}), 400
@ -1303,3 +1345,14 @@ def v0_api(api):
else:
results.append({"uuid":uuid, "status":True})
return jsonify(uuids=results, errors=errors)
@api.route("/api/v0/compose/info/<uuid>")
@crossdomain(origin="*")
def v0_compose_info(uuid):
"""Return detailed info about a compose"""
try:
info = uuid_info(api.config["COMPOSER_CFG"], uuid)
except Exception as e:
return jsonify(status=False, msg=str(e))
return jsonify(**info)