blob: a91279544daa207870a21e2f1ea430f8ed366747 [file] [log] [blame]
# Copyright 2017 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Module to upload goma logs to Google cloud storage bucket."""
import collections
import datetime
import getpass
import glob
import json
import logging
import os
import shlex
from chromite.lib import cros_build_lib
from chromite.lib import gs
from chromite.lib import osutils
_GOMA_COMPILER_PROXY_LOG_URL_TEMPLATE = (
"https://chromium-build-stats.appspot.com/compiler_proxy_log/%s/%s"
)
_GOMA_NINJA_LOG_URL_TEMPLATE = (
"https://chromium-build-stats.appspot.com/ninja_log/%s/%s"
)
# Note: Public for testing purpose. In real use, please think about using
# Goma.UploadLogs() instead.
class GomaLogUploader(object):
"""Manages to upload goma log files."""
# The Google Cloud Storage bucket to store logs related to goma.
_BUCKET = "chrome-goma-log"
def __init__(
self, goma_log_dir, today=None, dry_run=False, cbb_config_name=""
):
"""Initializes the uploader.
Args:
goma_log_dir: path to the directory containing goma's INFO log files.
today: datetime.date instance representing today. This is for testing
purpose, because datetime.date is unpatchable. In real use case,
this must be None.
dry_run: If True, no actual upload. This is for testing purpose.
cbb_config_name: Name of cbb_config.
"""
self._goma_log_dir = goma_log_dir
logging.info("Goma log directory is: %s", self._goma_log_dir)
# Set log upload destination.
if today is None:
today = datetime.date.today()
self.dest_path = "%s/%s" % (
today.strftime("%Y/%m/%d"),
cros_build_lib.GetHostName(),
)
self._remote_dir = "gs://%s/%s" % (
GomaLogUploader._BUCKET,
self.dest_path,
)
logging.info("Goma log upload destination: %s", self._remote_dir)
# HACK(yyanagisawa): I suppose LUCI do not set BUILDBOT_BUILDERNAME.
is_luci = not bool(os.environ.get("BUILDBOT_BUILDERNAME"))
# Build metadata to be annotated to log files.
# Use OrderedDict for json output stabilization.
builder_info = collections.OrderedDict(
[
("builder", os.environ.get("BUILDBOT_BUILDERNAME", "")),
("master", os.environ.get("BUILDBOT_MASTERNAME", "")),
("slave", os.environ.get("BUILDBOT_SLAVENAME", "")),
("clobber", bool(os.environ.get("BUILDBOT_CLOBBER"))),
("os", "chromeos"),
("is_luci", is_luci),
("cbb_config_name", cbb_config_name),
]
)
if is_luci:
# TODO(yyanagisawa): will adjust to valid value if needed.
builder_info["builder_id"] = collections.OrderedDict(
[
("project", "chromeos"),
("builder", "Prod"),
("bucket", "general"),
]
)
builder_info_json = json.dumps(builder_info)
logging.info("BuilderInfo: %s", builder_info_json)
self._headers = ["x-goog-meta-builderinfo:" + builder_info_json]
self._gs_context = gs.GSContext(dry_run=dry_run)
def Upload(self):
"""Uploads all necessary log files to Google Storage.
Returns:
A list of pairs of label and URL of goma log visualizers to be linked
from the build status page.
"""
compiler_proxy_subproc_paths = self._UploadInfoFiles(
"compiler_proxy-subproc"
)
# compiler_proxy-subproc.INFO file should be exact one.
if len(compiler_proxy_subproc_paths) != 1:
logging.warning(
"Unexpected compiler_proxy-subproc INFO files: %r",
compiler_proxy_subproc_paths,
)
compiler_proxy_paths = self._UploadInfoFiles("compiler_proxy")
# compiler_proxy.INFO file should be exact one.
if len(compiler_proxy_paths) != 1:
logging.warning(
"Unexpected compiler_proxy INFO files: %r", compiler_proxy_paths
)
compiler_proxy_path, uploaded_compiler_proxy_filename = (
compiler_proxy_paths[0] if compiler_proxy_paths else (None, None)
)
self._UploadGomaccInfoFiles()
uploaded_ninja_log_filename = self._UploadNinjaLog(compiler_proxy_path)
# Build URL to be linked.
result = []
if uploaded_compiler_proxy_filename:
result.append(
(
"Goma compiler_proxy log",
_GOMA_COMPILER_PROXY_LOG_URL_TEMPLATE
% (self.dest_path, uploaded_compiler_proxy_filename),
)
)
if uploaded_ninja_log_filename:
result.append(
(
"Goma ninja_log",
_GOMA_NINJA_LOG_URL_TEMPLATE
% (self.dest_path, uploaded_ninja_log_filename),
)
)
return result
def _UploadInfoFiles(self, pattern):
"""Uploads INFO files matched with pattern, with gzip'ing.
Args:
pattern: matching path pattern.
Returns:
A list of uploaded file paths.
"""
# Find files matched with the pattern in |goma_log_dir|. Sort for
# stabilization.
paths = sorted(
glob.glob(os.path.join(self._goma_log_dir, "%s.*.INFO.*" % pattern))
)
if not paths:
logging.warning("No glog files matched with: %s", pattern)
result = []
for path in paths:
logging.info("Uploading %s", path)
uploaded_filename = os.path.basename(path) + ".gz"
self._gs_context.CopyInto(
path,
self._remote_dir,
filename=uploaded_filename,
auto_compress=True,
headers=self._headers,
)
result.append((path, uploaded_filename))
return result
def _UploadGomaccInfoFiles(self):
"""Uploads gomacc INFO files, with gzip'ing.
Returns:
Uploaded file path. If failed, None.
"""
# Since the number of gomacc logs can be large, we'd like to compress them.
# Otherwise, upload will take long (> 10 mins).
# Each gomacc logs file size must be small (around 4KB).
# Find files matched with the pattern in |goma_log_dir|.
# The paths were themselves used as the inputs for the create
# tarball, but there can be too many of them. As long as we have
# files we'll just tar up the entire directory.
gomacc_paths = glob.glob(
os.path.join(self._goma_log_dir, "gomacc.*.INFO.*")
)
if not gomacc_paths:
# gomacc logs won't be made every time.
# Only when goma compiler_proxy has
# crashed. So it's usual gomacc logs are not found.
logging.info("No gomacc logs found")
return None
# Taking the alphabetically first name as uploaded_filename.
tarball_name = os.path.basename(min(gomacc_paths)) + ".tar.gz"
# When using the pigz compressor (what we use for gzip) to create an
# archive in a folder that is also a source for contents, there is a race
# condition involving the created archive itself that can cause it to fail
# creating the archive. To avoid this, make the archive in a tempdir.
with osutils.TempDir() as tempdir:
tarball_path = os.path.join(tempdir, tarball_name)
cros_build_lib.CreateTarball(
tarball_path,
cwd=self._goma_log_dir,
compression=cros_build_lib.CompressionType.GZIP,
)
self._gs_context.CopyInto(
tarball_path,
self._remote_dir,
filename=tarball_name,
headers=self._headers,
)
return tarball_name
def _UploadNinjaLog(self, compiler_proxy_path):
"""Uploads .ninja_log file and its related metadata.
This uploads the .ninja_log file generated by ninja to build Chrome.
Also, it appends some related metadata at the end of the file following
'# end of ninja log' marker.
Args:
compiler_proxy_path: Path to the compiler proxy, which will be contained
in the metadata.
Returns:
The name of the uploaded file.
"""
ninja_log_path = os.path.join(self._goma_log_dir, "ninja_log")
if not os.path.exists(ninja_log_path):
logging.warning("ninja_log is not found: %s", ninja_log_path)
return None
ninja_log_content = osutils.ReadFile(ninja_log_path)
try:
st = os.stat(ninja_log_path)
ninja_log_mtime = datetime.datetime.fromtimestamp(st.st_mtime)
except OSError:
logging.exception("Failed to get timestamp: %s", ninja_log_path)
return None
ninja_log_info = self._BuildNinjaInfo(compiler_proxy_path)
# Append metadata at the end of the log content.
ninja_log_content += "# end of ninja log\n" + json.dumps(ninja_log_info)
# Aligned with goma_utils in chromium bot.
pid = os.getpid()
upload_ninja_log_path = os.path.join(
self._goma_log_dir,
"ninja_log.%s.%s.%s.%d"
% (
getpass.getuser(),
cros_build_lib.GetHostName(),
ninja_log_mtime.strftime("%Y%m%d-%H%M%S"),
pid,
),
)
osutils.WriteFile(upload_ninja_log_path, ninja_log_content)
uploaded_filename = os.path.basename(upload_ninja_log_path) + ".gz"
self._gs_context.CopyInto(
upload_ninja_log_path,
self._remote_dir,
filename=uploaded_filename,
auto_compress=True,
headers=self._headers,
)
return uploaded_filename
def _BuildNinjaInfo(self, compiler_proxy_path):
"""Reads metadata for the ninja run.
Each metadata should be written into a dedicated file in the log directory.
Read the info, and build the dict containing metadata.
Args:
compiler_proxy_path: Path to the compiler_proxy log file.
Returns:
A dict of the metadata.
"""
info = {"platform": "chromeos"}
command_path = os.path.join(self._goma_log_dir, "ninja_command")
if os.path.exists(command_path):
info["cmdline"] = shlex.split(
osutils.ReadFile(command_path).strip()
)
cwd_path = os.path.join(self._goma_log_dir, "ninja_cwd")
if os.path.exists(cwd_path):
info["cwd"] = osutils.ReadFile(cwd_path).strip()
exit_path = os.path.join(self._goma_log_dir, "ninja_exit")
if os.path.exists(exit_path):
info["exit"] = int(osutils.ReadFile(exit_path).strip())
env_path = os.path.join(self._goma_log_dir, "ninja_env")
if os.path.exists(env_path):
# env is null-byte separated, and has a trailing null byte.
content = osutils.ReadFile(env_path).rstrip("\0")
info["env"] = dict(
line.split("=", 1) for line in content.split("\0")
)
if compiler_proxy_path:
info["compiler_proxy_info"] = os.path.basename(compiler_proxy_path)
return info