diff --git a/src/pylorax/api/queue.py b/src/pylorax/api/queue.py index 1a7e2958..db0de2f7 100644 --- a/src/pylorax/api/queue.py +++ b/src/pylorax/api/queue.py @@ -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() diff --git a/src/pylorax/api/v0.py b/src/pylorax/api/v0.py index 8382f8d3..16e7da33 100644 --- a/src/pylorax/api/v0.py +++ b/src/pylorax/api/v0.py @@ -833,6 +833,31 @@ DELETE `/api/v0/compose/delete/` 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/[?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/") + @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))