Return most relevant log file from /compose/log

Return anaconda.log if anaconda is running, composer otherwise

Return packaging.log if it's at least 15s newer than anaconda.log
This commit is contained in:
Evan Goode 2019-06-21 16:22:29 -04:00 committed by Brian C. Lane
parent 9bf8d8a2fc
commit 90626f97b6
3 changed files with 79 additions and 33 deletions

View File

@ -16,6 +16,8 @@
""" Functions to monitor compose queue and run anaconda"""
import logging
log = logging.getLogger("pylorax")
program_log = logging.getLogger("program")
dnf_log = logging.getLogger("dnf")
import os
import grp
@ -34,7 +36,7 @@ from pylorax.api.timestamp import TS_CREATED, TS_STARTED, TS_FINISHED, write_tim
import pylorax.api.toml as toml
from pylorax.base import DataHolder
from pylorax.creator import run_creator
from pylorax.sysutils import joinpaths
from pylorax.sysutils import joinpaths, read_tail
def check_queues(cfg):
"""Check to make sure the new and run queue symlinks are correct
@ -136,6 +138,23 @@ def monitor(cfg):
# The symlink may vanish if uuid_cancel() has been called
continue
# The anaconda logs are also copied into ./anaconda/ in this directory
os.makedirs(joinpaths(dst, "logs"), exist_ok=True)
def open_handler(loggers, file_name):
handler = logging.FileHandler(joinpaths(dst, "logs", file_name))
handler.setLevel(logging.DEBUG)
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s: %(message)s"))
for logger in loggers:
logger.addHandler(handler)
return (handler, loggers)
loggers = (((log, program_log, dnf_log), "combined.log"),
((log,), "composer.log"),
((program_log,), "program.log"),
((dnf_log,), "dnf.log"))
handlers = [open_handler(loggers, file_name) for loggers, file_name in loggers]
log.info("Starting new compose: %s", dst)
open(joinpaths(dst, "STATUS"), "w").write("RUNNING\n")
@ -152,6 +171,11 @@ def monitor(cfg):
# log.error("Error running compose: %s", e)
open(joinpaths(dst, "STATUS"), "w").write("FAILED\n")
write_timestamp(dst, TS_FINISHED)
finally:
for handler, loggers in handlers:
for logger in loggers:
logger.removeHandler(handler)
handler.close()
os.unlink(dst)
@ -179,11 +203,6 @@ def make_compose(cfg, results_dir):
if not os.path.exists(ks_path):
raise RuntimeError("Missing kickstart file at %s" % ks_path)
# The anaconda logs are copied into ./anaconda/ in this directory
log_dir = joinpaths(results_dir, "logs/")
if not os.path.exists(log_dir):
os.makedirs(log_dir)
# Load the compose configuration
cfg_path = joinpaths(results_dir, "config.toml")
if not os.path.exists(cfg_path):
@ -631,20 +650,23 @@ def get_image_name(uuid_dir):
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
"""Return `size` KiB from the end of the most currently relevant log for a
given compose
: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
:param size: Number of KiB to read. Default is 1024
:type size: int
:returns: Up to `size` kbytes from the end of the log
:returns: Up to `size` KiB from the end of the log
:rtype: str
:raises: RuntimeError if there was a problem (eg. no log file available)
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.
This function will return the end of either the anaconda log, the packaging
log, or the combined composer logs, depending on the progress of the
compose. It tries to return lines from the end of the log, it will attempt
to start on a line boundary, and it may return less than `size` kbytes.
"""
uuid_dir = joinpaths(cfg.get("composer", "lib_dir"), "results", uuid)
if not os.path.exists(uuid_dir):
@ -656,20 +678,29 @@ def uuid_log(cfg, uuid, size=1024):
if status is None:
raise RuntimeError("Status is missing for %s" % 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()
def get_log_path():
# Try to return the most relevant log at any given time during the
# compose. If the compose is not running, return the composer log.
anaconda_log = "/tmp/anaconda.log"
packaging_log = "/tmp/packaging.log"
combined_log = joinpaths(uuid_dir, "logs", "combined.log")
if status["queue_status"] != "RUNNING" or not os.path.isfile(anaconda_log):
return combined_log
if not os.path.isfile(packaging_log):
return anaconda_log
try:
anaconda_mtime = os.stat(anaconda_log).st_mtime
packaging_mtime = os.stat(packaging_log).st_mtime
# If the packaging log exists and its last message is at least 15
# seconds newer than the anaconda log, return the packaging log.
if packaging_mtime > anaconda_mtime + 15:
return packaging_log
return anaconda_log
except OSError:
# Return the combined log if anaconda_log or packaging_log disappear
return combined_log
try:
tail = read_tail(get_log_path(), size)
except OSError as e:
raise RuntimeError("No log available.") from e
return tail

View File

@ -2006,13 +2006,15 @@ def v0_compose_image(uuid):
@v0_api.route("/compose/log/<uuid>")
@checkparams([("uuid","", "no UUID given")])
def v0_compose_log_tail(uuid):
"""Return the end of the main anaconda.log, defaults to 1Mbytes
"""Return the tail of the most currently relevant log
**/api/v0/compose/log/<uuid>[?size=kbytes]**
**/api/v0/compose/log/<uuid>[?size=KiB]**
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.
Returns the end of either the anaconda log, the packaging log, or the
composer logs, depending on the progress of the compose. The size
parameter is optional and defaults to 1 MiB if it is not included. The
returned data is raw text from the end of the log file, starting on a
line boundary.
Example::

View File

@ -130,3 +130,16 @@ def flatconfig(filename):
config = UnquotingConfigParser()
config.read_string(conftext)
return config['main']
def read_tail(path, size):
"""Read up to `size` kibibytes from the end of a file"""
with open(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()