Add /compose/log/ API to retrieve the end of the build log

This allows the client to request the end of the anaconda.log during and
after a build. The amount of data returned can be set by adding
?size=<kbytes>

Output is raw bytes, starting on the next available line boundry.
This commit is contained in:
Brian C. Lane 2018-02-08 10:32:56 -08:00
parent caee8e7cdf
commit ff7d36bcbc
2 changed files with 83 additions and 1 deletions

View File

@ -408,3 +408,43 @@ def uuid_image(cfg, uuid):
image_name = cfg_dict["image_name"]
return (image_name, joinpaths(uuid_dir, image_name))
def uuid_log(cfg, uuid, size=1024):
"""Return `size` kbytes from the end of the anaconda.log
:param cfg: Configuration settings
:type cfg: ComposerConfig
:param uuid: The UUID of the build
:type uuid: str
:param size: Number of kbytes to read. Default is 1024
:type size: int
:returns: Up to `size` kbytes from the end of the log
:rtype: str
This function tries to return lines from the end of the log, it will
attempt to start on a line boundry, and may return less than `size` kbytes.
"""
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)
# While a build is running the logs will be in /tmp/anaconda.log and when it
# has finished they will be in the results directory
status = uuid_status(cfg, uuid)
if status["queue_status"] == "RUNNING":
log_path = "/tmp/anaconda.log"
else:
log_path = joinpaths(uuid_dir, "logs", "anaconda", "anaconda.log")
if not os.path.exists(log_path):
raise RuntimeError("No anaconda.log available.")
with open(log_path, "r") as f:
f.seek(0, 2)
end = f.tell()
if end < 1024 * size:
f.seek(0, 0)
else:
f.seek(end - (1024 * size))
# Find the start of the next line and return the rest
f.readline()
return f.read()

View File

@ -833,6 +833,31 @@ DELETE `/api/v0/compose/delete/<uuids>`
Returns the output image from the build. The filename is set to the filename
from the build. eg. root.tar.xz or boot.iso.
`/api/v0/compose/log/<uuid>[?size=kbytes]`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Returns the end of the anaconda.log. The size parameter is optional and defaults to 1Mbytes
if it is not included. The returned data is raw text from the end of the logfile, starting on
a line boundry.
Example::
12:59:24,222 INFO anaconda: Running Thread: AnaConfigurationThread (140629395244800)
12:59:24,223 INFO anaconda: Configuring installed system
12:59:24,912 INFO anaconda: Configuring installed system
12:59:24,912 INFO anaconda: Creating users
12:59:24,913 INFO anaconda: Clearing libuser.conf at /tmp/libuser.Dyy8Gj
12:59:25,154 INFO anaconda: Creating users
12:59:25,155 INFO anaconda: Configuring addons
12:59:25,155 INFO anaconda: Configuring addons
12:59:25,155 INFO anaconda: Generating initramfs
12:59:49,467 INFO anaconda: Generating initramfs
12:59:49,467 INFO anaconda: Running post-installation scripts
12:59:49,467 INFO anaconda: Running kickstart %%post script(s)
12:59:50,782 INFO anaconda: All kickstart %%post script(s) have been run
12:59:50,782 INFO anaconda: Running post-installation scripts
12:59:50,784 INFO anaconda: Thread Done: AnaConfigurationThread (140629395244800)
"""
import logging
@ -845,7 +870,7 @@ 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, uuid_info
from pylorax.api.queue import uuid_tar, uuid_image, uuid_cancel
from pylorax.api.queue import uuid_tar, uuid_image, uuid_cancel, uuid_log
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
@ -1486,3 +1511,20 @@ def v0_api(api):
# XXX - Will mime type guessing work for all our output?
return send_file(image_path, as_attachment=True, attachment_filename=image_name, add_etags=False)
@api.route("/api/v0/compose/log/<uuid>")
@crossdomain(origin="*")
def v0_compose_log_tail(uuid):
"""Return the end of the main anaconda.log, defaults to 1Mbytes"""
try:
size = int(request.args.get("size", "1024"))
except ValueError as e:
return jsonify(error={"msg":str(e)}), 400
status = uuid_status(api.config["COMPOSER_CFG"], uuid)
if status is None or status["queue_status"] == "WAITING":
return jsonify(status=False, uuid=uuid, msg="Build has not started yet. No logs to view")
try:
return Response(uuid_log(api.config["COMPOSER_CFG"], uuid, size), direct_passthrough=True)
except RuntimeError as e:
return jsonify(status=False, uuid=uuid, msg=str(e))