summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJasur Nurboyev <bluestacks6523@gmail.com>2022-01-12 17:34:48 +0000
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2022-01-12 18:27:21 +0000
commitdf2f772316ab5cb43d22e00b85c45ba7e52a903d (patch)
treed4869140c04fabca0da7c2fc59e781198ebb840c
parent1ccc8af43a3698b4178c8d2bd810e395bc81f0ac (diff)
downloadmongo-df2f772316ab5cb43d22e00b85c45ba7e52a903d.tar.gz
SERVER-61242 Add logic to the CI to add BUILD ID info to the symbolizer service
-rw-r--r--buildscripts/debugsymb_mapper.py363
-rwxr-xr-xbuildscripts/mongosymb.py528
-rw-r--r--buildscripts/util/oauth.py303
-rw-r--r--etc/evergreen.yml51
-rw-r--r--etc/pip/components/core.req3
-rwxr-xr-xevergreen/generate_buildid_debug_symbols_mapping.sh15
6 files changed, 1190 insertions, 73 deletions
diff --git a/buildscripts/debugsymb_mapper.py b/buildscripts/debugsymb_mapper.py
new file mode 100644
index 00000000000..9946f4719aa
--- /dev/null
+++ b/buildscripts/debugsymb_mapper.py
@@ -0,0 +1,363 @@
+"""Script to generate & upload 'buildId -> debug symbols URL' mappings to symbolizer service."""
+import argparse
+import json
+import logging
+import os
+import pathlib
+import shutil
+import subprocess
+import sys
+import time
+import typing
+
+import requests
+from db_contrib_tool.setup_repro_env.setup_repro_env import SetupReproEnv, download
+
+# register parent directory in sys.path, so 'buildscripts' is detected no matter where the script is called from
+sys.path.append(str(pathlib.Path(os.path.join(os.getcwd(), __file__)).parent.parent))
+
+# pylint: disable=wrong-import-position
+from buildscripts.util.oauth import get_client_cred_oauth_credentials, Configs
+
+
+class LinuxBuildIDExtractor:
+ """Parse readlef command output & extract Build ID."""
+
+ default_executable_path = "readelf"
+
+ def __init__(self, executable_path: str = None):
+ """Initialize instance."""
+
+ self.executable_path = executable_path or self.default_executable_path
+
+ def callreadelf(self, binary_path: str) -> str:
+ """Call readelf command for given binary & return string output."""
+
+ args = [self.executable_path, "-n", binary_path]
+ process = subprocess.Popen(args=args, close_fds=True, stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE)
+ process.wait()
+ return process.stdout.read().decode()
+
+ @staticmethod
+ def extractbuildid(out: str) -> typing.Optional[str]:
+ """Parse readelf output and extract Build ID from it."""
+
+ build_id = None
+ for line in out.splitlines():
+ line = line.strip()
+ if line.startswith('Build ID'):
+ if build_id is not None:
+ raise ValueError("Found multiple Build ID values.")
+ build_id = line.split(': ')[1]
+ return build_id
+
+ def run(self, binary_path: str) -> typing.Tuple[str, str]:
+ """Perform all necessary actions to get Build ID."""
+
+ readelfout = self.callreadelf(binary_path)
+ buildid = self.extractbuildid(readelfout)
+
+ return buildid, readelfout
+
+
+class DownloadOptions(object):
+ """A class to collect download option configurations."""
+
+ def __init__(self, download_binaries=False, download_symbols=False, download_artifacts=False,
+ download_python_venv=False):
+ """Initialize instance."""
+
+ self.download_binaries = download_binaries
+ self.download_symbols = download_symbols
+ self.download_artifacts = download_artifacts
+ self.download_python_venv = download_python_venv
+
+
+class Mapper:
+ """A class to to basically all of the work."""
+
+ # pylint: disable=too-many-instance-attributes
+ # pylint: disable=too-many-arguments
+ # This amount of attributes are necessary.
+
+ default_web_service_base_url: str = "https://symbolizer-service.server-tig.prod.corp.mongodb.com"
+ default_cache_dir = os.path.join(os.getcwd(), 'build', 'symbols_cache')
+ selected_binaries = ('mongos.debug', 'mongod.debug', 'mongo.debug')
+ default_client_credentials_scope = "servertig-symbolizer-fullaccess"
+ default_client_credentials_user_name = "client-user"
+ default_creds_file_path = os.path.join(os.getcwd(), '.symbolizer_credentials.json')
+
+ def __init__(self, version: str, client_id: str, client_secret: str, variant: str,
+ cache_dir: str = None, web_service_base_url: str = None,
+ logger: logging.Logger = None):
+ """
+ Initialize instance.
+
+ :param version: version string
+ :param variant: build variant string
+ :param cache_dir: full path to cache directory as a string
+ :param web_service_base_url: URL of symbolizer web service
+ """
+ self.version = version
+ self.variant = variant
+ self.cache_dir = cache_dir or self.default_cache_dir
+ self.web_service_base_url = web_service_base_url or self.default_web_service_base_url
+
+ if not logger:
+ logging.basicConfig()
+ logger = logging.getLogger('symbolizer')
+ logger.setLevel(logging.INFO)
+ self.logger = logger
+
+ self.http_client = requests.Session()
+
+ self.multiversion_setup = SetupReproEnv(
+ DownloadOptions(download_symbols=True, download_binaries=True), variant=self.variant,
+ ignore_failed_push=True)
+ self.debug_symbols_url = None
+ self.url = None
+ self.configs = Configs(
+ client_credentials_scope=self.default_client_credentials_scope,
+ client_credentials_user_name=self.default_client_credentials_user_name)
+ self.client_id = client_id
+ self.client_secret = client_secret
+
+ if not os.path.exists(self.cache_dir):
+ os.makedirs(self.cache_dir)
+
+ self.authenticate()
+ self.setup_urls()
+
+ def authenticate(self):
+ """Login & get credentials for further requests to web service."""
+
+ # try to read from file
+ if os.path.exists(self.default_creds_file_path):
+ with open(self.default_creds_file_path) as cfile:
+ data = json.loads(cfile.read())
+ access_token, expire_time = data.get("access_token"), data.get("expire_time")
+ if time.time() < expire_time:
+ # credentials hasn't expired yet
+ self.http_client.headers.update({"Authorization": f"Bearer {access_token}"})
+ return
+
+ credentials = get_client_cred_oauth_credentials(self.client_id, self.client_secret,
+ configs=self.configs)
+ self.http_client.headers.update({"Authorization": f"Bearer {credentials.access_token}"})
+
+ # write credentials to local file for further useage
+ with open(self.default_creds_file_path, "w") as cfile:
+ cfile.write(
+ json.dumps({
+ "access_token": credentials.access_token,
+ "expire_time": time.time() + credentials.expires_in
+ }))
+
+ def __enter__(self):
+ """Return instance when used as a context manager."""
+
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ """Do cleaning process when used as a context manager."""
+
+ self.cleanup()
+
+ def cleanup(self):
+ """Remove temporary files & folders."""
+
+ if os.path.exists(self.cache_dir):
+ shutil.rmtree(self.cache_dir)
+
+ @staticmethod
+ def url_to_filename(url: str) -> str:
+ """
+ Convert URL to local filename.
+
+ :param url: download URL
+ :return: full name for local file
+ """
+ return url.split('/')[-1]
+
+ def setup_urls(self):
+ """Set up URLs using multiversion."""
+
+ urls = self.multiversion_setup.get_urls(self.version, self.variant).urls
+
+ download_symbols_url = urls.get("mongo-debugsymbols.tgz", None)
+ binaries_url = urls.get("Binaries", "")
+
+ if not download_symbols_url:
+ download_symbols_url = urls.get("mongo-debugsymbols.zip", None)
+
+ if not download_symbols_url:
+ self.logger.error("Couldn't find URL for debug symbols. Version: %s, URLs dict: %s",
+ self.version, urls)
+ raise ValueError(f"Debug symbols URL not found. URLs dict: {urls}")
+
+ self.debug_symbols_url = download_symbols_url
+ self.url = binaries_url
+
+ def unpack(self, path: str) -> str:
+ """
+ Use to untar/unzip files.
+
+ :param path: full path of file
+ :return: full path of directory of unpacked file
+ """
+ foldername = path.replace('.tgz', '', 1).split('/')[-1]
+ out_dir = os.path.join(self.cache_dir, foldername)
+
+ if not os.path.exists(out_dir):
+ os.makedirs(out_dir)
+
+ download.extract_archive(path, out_dir)
+
+ # extracted everything, we don't need the original tar file anymore and it should be deleted
+ if os.path.exists(path):
+ os.remove(path)
+
+ return out_dir
+
+ @staticmethod
+ def download(url: str) -> str:
+ """
+ Use to download file from URL.
+
+ :param url: URL of file to download
+ :return: full path of downloaded file in local filesystem
+ """
+
+ tarball_full_path = download.download_from_s3(url)
+ return tarball_full_path
+
+ def generate_build_id_mapping(self) -> typing.Generator[typing.Dict[str, str], None, None]:
+ """
+ Extract build id from binaries and creates new dict using them.
+
+ :return: mapped data as dict
+ """
+
+ readelf_extractor = LinuxBuildIDExtractor()
+
+ debug_symbols_path = self.download(self.debug_symbols_url)
+ debug_symbols_unpacked_path = self.unpack(debug_symbols_path)
+
+ binaries_path = self.download(self.url)
+ binaries_unpacked_path = self.unpack(binaries_path)
+
+ # we need to analyze two directories: bin inside debug-symbols and lib inside binaries.
+ # bin holds main binaries, like mongos, mongod, mongo ...
+ # lib holds shared libraries, tons of them. some build variants do not contain shared libraries.
+
+ debug_symbols_unpacked_path = os.path.join(debug_symbols_unpacked_path, 'dist-test')
+ binaries_unpacked_path = os.path.join(binaries_unpacked_path, 'dist-test')
+
+ self.logger.info("INSIDE unpacked debug-symbols/dist-test: %s",
+ os.listdir(debug_symbols_unpacked_path))
+ self.logger.info("INSIDE unpacked binaries/dist-test: %s",
+ os.listdir(binaries_unpacked_path))
+
+ # start with 'bin' folder
+ for binary in self.selected_binaries:
+ full_bin_path = os.path.join(debug_symbols_unpacked_path, 'bin', binary)
+
+ if not os.path.exists(full_bin_path):
+ self.logger.error("Could not find binary at %s", full_bin_path)
+ return
+
+ build_id, readelf_out = readelf_extractor.run(full_bin_path)
+
+ if not build_id:
+ self.logger.error("Build ID couldn't be extracted. \nReadELF output %s",
+ readelf_out)
+ return
+
+ yield {
+ 'url': self.url, 'debug_symbols_url': self.debug_symbols_url, 'build_id': build_id,
+ 'file_name': binary, 'version': self.version
+ }
+
+ # move to 'lib' folder.
+ # it contains all shared library binary files,
+ # we run readelf on each of them.
+ lib_folder_path = os.path.join(binaries_unpacked_path, 'lib')
+
+ if not os.path.exists(lib_folder_path):
+ # sometimes we don't get lib folder, which means there is no shared libraries for current build variant.
+ self.logger.info("'lib' folder does not exist.")
+ sofiles = []
+ else:
+ sofiles = os.listdir(lib_folder_path)
+ self.logger.info("'lib' folder: %s", sofiles)
+
+ for sofile in sofiles:
+ sofile_path = os.path.join(lib_folder_path, sofile)
+
+ if not os.path.exists(sofile_path):
+ self.logger.error("Could not find binary at %s", sofile_path)
+ return
+
+ build_id, readelf_out = readelf_extractor.run(sofile_path)
+
+ if not build_id:
+ self.logger.error("Build ID couldn't be extracted. \nReadELF out %s", readelf_out)
+ return
+
+ yield {
+ 'url': self.url,
+ 'debug_symbols_url': self.debug_symbols_url,
+ 'build_id': build_id,
+ 'file_name': sofile,
+ 'version': self.version,
+ }
+
+ def run(self):
+ """Run all necessary processes."""
+
+ mappings = self.generate_build_id_mapping()
+ if not mappings:
+ self.logger.error("Could not generate mapping")
+ return
+
+ # mappings is a generator, we iterate over to generate mappings on the go
+ for mapping in mappings:
+ response = self.http_client.post('/'.join((self.web_service_base_url, 'add')),
+ json=mapping)
+ if response.status_code != 200:
+ self.logger.error(
+ "Could not store mapping, web service returned status code %s from URL %s. "
+ "Response: %s", response.status_code, response.url, response.text)
+
+
+def make_argument_parser(parser=None, **kwargs):
+ """Make and return an argparse."""
+
+ if parser is None:
+ parser = argparse.ArgumentParser(**kwargs)
+
+ parser.add_argument('--version')
+ parser.add_argument('--client-id')
+ parser.add_argument('--client-secret')
+ parser.add_argument('--variant')
+ parser.add_argument('--web-service-base-url', default="")
+ return parser
+
+
+def main(options):
+ """Execute mapper here. Main entry point."""
+
+ mapper = Mapper(version=options.version, variant=options.variant, client_id=options.client_id,
+ client_secret=options.client_secret,
+ web_service_base_url=options.web_service_base_url)
+
+ # when used as a context manager, mapper instance automatically cleans files/folders after finishing its job.
+ # in other cases, mapper.cleanup() method should be called manually.
+ with mapper:
+ mapper.run()
+
+
+if __name__ == '__main__':
+ mapper_options = make_argument_parser(description=__doc__).parse_args()
+ main(mapper_options)
diff --git a/buildscripts/mongosymb.py b/buildscripts/mongosymb.py
index 44cfd56008d..b9511b590cd 100755
--- a/buildscripts/mongosymb.py
+++ b/buildscripts/mongosymb.py
@@ -17,11 +17,343 @@ You can also pass --output-format=json, to get rich json output. It shows some e
but emits json instead of plain text.
"""
-import json
import argparse
+import json
import os
+import signal
import subprocess
import sys
+import time
+from collections import OrderedDict
+from pathlib import Path
+from typing import Dict
+
+import requests
+
+# pylint: disable=wrong-import-position
+sys.path.append(str(Path(os.getcwd(), __file__).parent.parent))
+from buildscripts.util.oauth import Configs, get_oauth_credentials
+
+
+class PathDbgFileResolver(object):
+ """PathDbgFileResolver class."""
+
+ def __init__(self, bin_path_guess):
+ """Initialize PathDbgFileResolver."""
+ self._bin_path_guess = os.path.realpath(bin_path_guess)
+ self.mci_build_dir = None
+
+ def get_dbg_file(self, soinfo):
+ """Return dbg file name."""
+ path = soinfo.get("path", "")
+ # TODO: make identifying mongo shared library directory more robust
+ if self.mci_build_dir is None and path.startswith("/data/mci/"):
+ self.mci_build_dir = path.split("/src/", maxsplit=1)[0]
+ return path if path else self._bin_path_guess
+
+
+class S3BuildidDbgFileResolver(object):
+ """S3BuildidDbgFileResolver class."""
+
+ def __init__(self, cache_dir, s3_bucket):
+ """Initialize S3BuildidDbgFileResolver."""
+ self._cache_dir = cache_dir
+ self._s3_bucket = s3_bucket
+ self.mci_build_dir = None
+
+ def get_dbg_file(self, soinfo):
+ """Return dbg file name."""
+ build_id = soinfo.get("buildId", None)
+ if build_id is None:
+ return None
+ build_id = build_id.lower()
+ build_id_path = os.path.join(self._cache_dir, build_id + ".debug")
+ if not os.path.exists(build_id_path):
+ try:
+ self._get_from_s3(build_id)
+ except Exception: # pylint: disable=broad-except
+ ex = sys.exc_info()[0]
+ sys.stderr.write("Failed to find debug symbols for {} in s3: {}\n".format(
+ build_id, ex))
+ return None
+ if not os.path.exists(build_id_path):
+ return None
+ return build_id_path
+
+ def _get_from_s3(self, build_id):
+ """Download debug symbols from S3."""
+ subprocess.check_call(
+ ['wget', 'https://s3.amazonaws.com/{}/{}.debug.gz'.format(self._s3_bucket, build_id)],
+ cwd=self._cache_dir)
+ subprocess.check_call(['gunzip', build_id + ".debug.gz"], cwd=self._cache_dir)
+
+
+class CachedResults(object):
+ """
+ Used to manage / store results in a cache form (using dict as an underlying data structure).
+
+ Idea is to allow only N items to be present in cache at a time and eliminate extra items on the go.
+ """
+
+ def __init__(self, max_cache_size: int, initial_cache: Dict[str, str] = None):
+ """
+ Initialize instance.
+
+ :param max_cache_size: max number of items that can be added to cache
+ :param initial_cache: initial items as dict
+ """
+ self._max_cache_size = max_cache_size
+ self._cached_results = OrderedDict(initial_cache or {})
+
+ def insert(self, key: str, value: str) -> Dict[str, str] or None:
+ """
+ Insert new data into cache.
+
+ :param key: key string
+ :param value: value string
+ :return: inserted data as dict or None (if not possible to insert)
+ """
+ if self._max_cache_size <= 0:
+ # we can't insert into 0-length dict
+ return None
+
+ if len(self._cached_results) >= self._max_cache_size:
+ # remove items causing the size overflow of cache
+ # we use FIFO order when removing objects from cache,
+ # so that we delete olds and keep track of only the recent ones
+ keys_iterator = iter(self._cached_results.keys())
+ while len(self._cached_results) >= self._max_cache_size:
+ # pop the first (the oldest) item in dict
+ self._cached_results.pop(next(keys_iterator))
+
+ if key not in self._cached_results:
+ # actual insert operation
+ self._cached_results[key] = value
+
+ return dict(build_id=value)
+
+ def get(self, key: str) -> str or None:
+ """
+ Try to get object by key.
+
+ :param key: key string
+ :return: value for key
+ """
+ if self._max_cache_size <= 0:
+ return None
+
+ return self._cached_results.get(key)
+
+
+class PathResolver(object):
+ """
+ Class to find path for given buildId.
+
+ We'll be sending request each time to another server to get path.
+ This process is fairly small, but can be heavy in case of increased amount of requests per second.
+ Thus, I'm implementing a caching mechanism (as a suggestion).
+ It keeps track of the last N results from server, we always try to search from that cache, if not found then send
+ request to server and cache the response for further usage.
+ Cache size differs according to the situation, system resources and overall decision of development team.
+ """
+
+ # pylint: disable=too-many-instance-attributes
+ # pylint: disable=too-many-arguments
+ # This amount of attributes are necessary.
+
+ # the main (API) sever that we'll be sending requests to
+ default_host = 'https://symbolizer-service.server-tig.prod.corp.mongodb.com'
+ default_cache_dir = os.path.join(os.getcwd(), 'build', 'symbolizer_downloads_cache')
+ default_creds_file_path = os.path.join(os.getcwd(), '.symbolizer_credentials.json')
+ default_client_credentials_scope = "servertig-symbolizer-fullaccess"
+ default_client_credentials_user_name = "client-user"
+
+ def __init__(self, host: str = None, cache_size: int = 0, cache_dir: str = None,
+ client_credentials_scope: str = None, client_credentials_user_name: str = None,
+ client_id: str = None, redirect_port: int = None, scope: str = None,
+ auth_domain: str = None):
+ """
+ Initialize instance.
+
+ :param host: URL of host - web service
+ :param cache_size: size of cache. We try to cache recent results and use them instead of asking from server.
+ Use 0 (by default) to disable caching
+ """
+ self.host = host or self.default_host
+ self._cached_results = CachedResults(max_cache_size=cache_size)
+ self.cache_dir = cache_dir or self.default_cache_dir
+ self.mci_build_dir = None
+ self.client_credentials_scope = client_credentials_scope or self.default_client_credentials_scope
+ self.client_credentials_user_name = client_credentials_user_name or self.default_client_credentials_user_name
+ self.client_id = client_id
+ self.redirect_port = redirect_port
+ self.scope = scope
+ self.auth_domain = auth_domain
+ self.configs = Configs(client_credentials_scope=self.client_credentials_scope,
+ client_credentials_user_name=self.client_credentials_user_name,
+ client_id=self.client_id, auth_domain=self.auth_domain,
+ redirect_port=self.redirect_port, scope=self.scope)
+ self.http_client = requests.Session()
+
+ # create cache dir if it doesn't exist
+ if not os.path.exists(self.cache_dir):
+ os.makedirs(self.cache_dir)
+
+ self.authenticate()
+
+ def authenticate(self):
+ """Login & get credentials for further requests to web service."""
+
+ # try to read from file
+ if os.path.exists(self.default_creds_file_path):
+ with open(self.default_creds_file_path) as cfile:
+ data = json.loads(cfile.read())
+ access_token, expire_time = data.get("access_token"), data.get("expire_time")
+ if time.time() < expire_time:
+ # credentials hasn't expired yet
+ self.http_client.headers.update({"Authorization": f"Bearer {access_token}"})
+ return
+
+ credentials = get_oauth_credentials(configs=self.configs, print_auth_url=True)
+ self.http_client.headers.update({"Authorization": f"Bearer {credentials.access_token}"})
+
+ # write credentials to local file for further useage
+ with open(self.default_creds_file_path, "w") as cfile:
+ cfile.write(
+ json.dumps({
+ "access_token": credentials.access_token,
+ "expire_time": time.time() + credentials.expires_in
+ }))
+
+ @staticmethod
+ def is_valid_path(path: str) -> bool:
+ """
+ Sometimes the given path may not be valid: e.g: path for a non-existing file.
+
+ If we need to do extra checks on path, we'll do all of them here.
+ :param path: path string
+ :return: bool indicating the validation status
+ """
+ return os.path.exists(path)
+
+ def get_from_cache(self, key: str) -> str or None:
+ """
+ Try to get value from cache.
+
+ :param key: key string
+ :return: value or None (if doesn't exist)
+ """
+ return self._cached_results.get(key)
+
+ def add_to_cache(self, key: str, value: str) -> Dict[str, str]:
+ """
+ Add new value to cache.
+
+ :param key: key string
+ :param value: value string
+ :return: added data as dict
+ """
+ return self._cached_results.insert(key, value)
+
+ @staticmethod
+ def url_to_filename(url: str) -> str:
+ """
+ Convert URL to local filename.
+
+ :param url: download URL
+ :return: full name for local file
+ """
+ return url.split('/')[-1]
+
+ @staticmethod
+ def unpack(path: str) -> str:
+ """
+ Use to utar/unzip files.
+
+ :param path: full path of file
+ :return: full path of 'bin' directory of unpacked file
+ """
+ out_dir = path.replace('.tgz', '', 1)
+ if not os.path.exists(out_dir):
+ os.mkdir(out_dir)
+
+ args = ["tar", "xopf", path, "-C", out_dir, "--strip-components 1"]
+ cmd = " ".join(args)
+ subprocess.check_call(cmd, shell=True)
+
+ return out_dir
+
+ def download(self, url: str) -> (str, bool):
+ """
+ Use to download file from URL.
+
+ :param url: URL string
+ :return: full path of downloaded file in local filesystem, bool indicating if file is already downloaded or not
+ """
+ exists_locally = False
+ filename = self.url_to_filename(url)
+ path = os.path.join(self.cache_dir, filename)
+ if not os.path.exists(path):
+ subprocess.check_call(['wget', url], cwd=self.cache_dir)
+ else:
+ print('File aready exists in cache')
+ exists_locally = True
+ return path, exists_locally
+
+ def get_dbg_file(self, soinfo: dict) -> str or None:
+ """
+ To get path for given buildId.
+
+ :param soinfo: soinfo as dict
+ :return: path as string or None (if path not found)
+ """
+ build_id = soinfo.get("buildId", "").lower()
+ binary_name = 'mongo'
+ # search from cached results
+ path = self.get_from_cache(build_id)
+ if not path:
+ # path does not exist in cache, so we send request to server
+ try:
+ response = self.http_client.get(f'{self.host}/find_by_id',
+ params={'build_id': build_id})
+ if response.status_code != 200:
+ sys.stderr.write(
+ f"Server returned unsuccessful status: {response.status_code}, "
+ f"response body: {response.text}\n")
+ return None
+ else:
+ data = response.json().get('data', {})
+ path, binary_name = data.get('debug_symbols_url'), data.get('file_name')
+ except Exception as err: # noqa pylint: disable=broad-except
+ sys.stderr.write(f"Error occurred while trying to get response from server "
+ f"for buildId({build_id}): {err}\n")
+ return None
+
+ # update cached results
+ if path:
+ self.add_to_cache(build_id, path)
+
+ if not path:
+ return None
+
+ # download & unpack debug symbols file and assign `path` to unpacked file's local path
+ try:
+ dl_path, exists_locally = self.download(path)
+ if exists_locally:
+ path = dl_path.replace('.tgz', '', 1)
+ else:
+ print("Downloaded, now unpacking...")
+ path = self.unpack(dl_path)
+ except Exception as err: # noqa pylint: disable=broad-except
+ sys.stderr.write(f"Failed to download & unpack file: {err}\n")
+ # we may have '<name>.debug', '<name>.so' or just executable binary file which may not have file 'extension'.
+ # if file has extension, it is good. if not, we should append .debug, because those without extension are
+ # from release builds, and their debug symbol files contain .debug extension.
+ # we need to map those 2 different file names ('<name>' becomes '<name>.debug').
+ if not binary_name.endswith('.debug') and not binary_name.endswith('.so'):
+ binary_name = f'{binary_name}.debug'
+
+ return os.path.join(path, binary_name)
def parse_input(trace_doc, dbg_path_resolver):
@@ -38,6 +370,11 @@ def parse_input(trace_doc, dbg_path_resolver):
frames = []
for frame in trace_doc["backtrace"]:
+ if "b" not in frame:
+ print(
+ f"Ignoring frame {frame} as it's missing the `b` field; See SERVER-58863 for discussions"
+ )
+ continue
soinfo = base_addr_map.get(frame["b"], {})
elf_type = soinfo.get("elfType", 0)
if elf_type == 3:
@@ -61,20 +398,23 @@ def parse_input(trace_doc, dbg_path_resolver):
def symbolize_frames(trace_doc, dbg_path_resolver, symbolizer_path, dsym_hint, input_format,
- **_kwargs):
+ **kwargs):
"""Return a list of symbolized stack frames from a trace_doc in MongoDB stack dump format."""
- if not symbolizer_path:
- symbolizer_path = os.environ.get("MONGOSYMB_SYMBOLIZER_PATH", "llvm-symbolizer")
+ # Keep frames in kwargs to avoid changing the function signature.
+ frames = kwargs.get("frames")
+ if frames is None:
+ frames = preprocess_frames(dbg_path_resolver, trace_doc, input_format)
- if input_format == "classic":
- frames = parse_input(trace_doc, dbg_path_resolver)
- elif input_format == "thin":
- frames = trace_doc["backtrace"]
- for frame in frames:
- frame["path"] = dbg_path_resolver.get_dbg_file(frame)
- else:
- raise ValueError('Unknown input format "{}"'.format(input_format))
+ if not symbolizer_path:
+ symbolizer_path_env = "MONGOSYMB_SYMBOLIZER_PATH"
+ default_symbolizer_path = "llvm-symbolizer"
+ symbolizer_path = os.environ.get(symbolizer_path_env)
+ if not symbolizer_path:
+ print(
+ f"Env value for '{symbolizer_path_env}' not found, using '{default_symbolizer_path}' "
+ f"as a defualt executable path.")
+ symbolizer_path = default_symbolizer_path
symbolizer_args = [symbolizer_path]
for dh in dsym_hint:
@@ -113,6 +453,7 @@ def symbolize_frames(trace_doc, dbg_path_resolver, symbolizer_path, dsym_hint, i
for frame in frames:
if frame["path"] is None:
+ print("Path not found in frame:", frame)
continue
symbol_line = "CODE {path:} {addr:}\n".format(**frame)
symbolizer_process.stdin.write(symbol_line.encode())
@@ -123,91 +464,125 @@ def symbolize_frames(trace_doc, dbg_path_resolver, symbolizer_path, dsym_hint, i
return frames
-class PathDbgFileResolver(object):
- """PathDbgFileResolver class."""
-
- def __init__(self, bin_path_guess):
- """Initialize PathDbgFileResolver."""
- self._bin_path_guess = os.path.realpath(bin_path_guess)
-
- def get_dbg_file(self, soinfo):
- """Return dbg file name."""
- path = soinfo.get("path", "")
- return path if path else self._bin_path_guess
-
-
-class S3BuildidDbgFileResolver(object):
- """S3BuildidDbgFileResolver class."""
-
- def __init__(self, cache_dir, s3_bucket):
- """Initialize S3BuildidDbgFileResolver."""
- self._cache_dir = cache_dir
- self._s3_bucket = s3_bucket
-
- def get_dbg_file(self, soinfo):
- """Return dbg file name."""
- build_id = soinfo.get("buildId", None)
- if build_id is None:
- return None
- build_id = build_id.lower()
- build_id_path = os.path.join(self._cache_dir, build_id + ".debug")
- if not os.path.exists(build_id_path):
- try:
- self._get_from_s3(build_id)
- except Exception: # pylint: disable=broad-except
- ex = sys.exc_info()[0]
- sys.stderr.write("Failed to find debug symbols for {} in s3: {}\n".format(
- build_id, ex))
- return None
- if not os.path.exists(build_id_path):
- return None
- return build_id_path
-
- def _get_from_s3(self, build_id):
- """Download debug symbols from S3."""
- subprocess.check_call(
- ['wget', 'https://s3.amazonaws.com/{}/{}.debug.gz'.format(self._s3_bucket, build_id)],
- cwd=self._cache_dir)
- subprocess.check_call(['gunzip', build_id + ".debug.gz"], cwd=self._cache_dir)
+def preprocess_frames(dbg_path_resolver, trace_doc, input_format):
+ """Process the paths in frame objects."""
+ if input_format == "classic":
+ frames = parse_input(trace_doc, dbg_path_resolver)
+ elif input_format == "thin":
+ frames = trace_doc["backtrace"]
+ for frame in frames:
+ frame["path"] = dbg_path_resolver.get_dbg_file(frame)
+ else:
+ raise ValueError('Unknown input format "{}"'.format(input_format))
+ return frames
def classic_output(frames, outfile, **kwargs): # pylint: disable=unused-argument
"""Provide classic output."""
for frame in frames:
- symbinfo = frame["symbinfo"]
+ symbinfo = frame.get("symbinfo")
if symbinfo:
for sframe in symbinfo:
outfile.write(" {file:s}:{line:d}:{column:d}: {fn:s}\n".format(**sframe))
else:
- outfile.write(" {path:s}!!!\n".format(**symbinfo))
+ outfile.write(" Couldn't extract symbols: {path:s}!!!\n".format(**frame))
-def make_argument_parser(**kwargs):
+def make_argument_parser(parser=None, **kwargs):
"""Make and return an argparse."""
- parser = argparse.ArgumentParser(**kwargs)
+ if parser is None:
+ parser = argparse.ArgumentParser(**kwargs)
+
parser.add_argument('--dsym-hint', default=[], action='append')
parser.add_argument('--symbolizer-path', default='')
parser.add_argument('--input-format', choices=['classic', 'thin'], default='classic')
parser.add_argument('--output-format', choices=['classic', 'json'], default='classic',
help='"json" shows some extra information')
- parser.add_argument('--debug-file-resolver', choices=['path', 's3'], default='path')
+ parser.add_argument('--debug-file-resolver', choices=['path', 's3', 'pr'], default='pr')
+ parser.add_argument('--src-dir-to-move', action="store", type=str, default=None,
+ help="Specify a src dir to move to /data/mci/{original_buildid}/src")
+
+ parser.add_argument('--live', action='store_true')
s3_group = parser.add_argument_group(
"s3 options", description='Options used with \'--debug-file-resolver s3\'')
s3_group.add_argument('--s3-cache-dir')
s3_group.add_argument('--s3-bucket')
- parser.add_argument('path_to_executable')
+
+ pr_group = parser.add_argument_group(
+ 'Path Resolver options (Path Resolver uses a special web service to retrieve URL of debug symbols file for '
+ 'a given BuildID), we use "pr" as a shorter/easier name for this',
+ description='Options used with \'--debug-file-resolver pr\'')
+ pr_group.add_argument('--pr-host', default='',
+ help='URL of web service running the API to get debug symbol URL')
+ pr_group.add_argument('--pr-cache-dir', default='',
+ help='Full path to a directory to store cache/files')
+ # caching mechanism is currently not fully developed and needs more advanced cleaning techniques, we add an option
+ # to enable it after completing the implementation
+
+ # Look for symbols in the cwd by default.
+ parser.add_argument('path_to_executable', nargs="?")
return parser
-def main():
+def substitute_stdin(options, resolver):
+ """Accept stdin stream as source of logs and symbolize it."""
+
+ # Ignore Ctrl-C. When the process feeding the pipe exits, `stdin` will be closed.
+ signal.signal(signal.SIGINT, signal.SIG_IGN)
+
+ print("Live mode activated, waiting for input...")
+ while True:
+ backtrace_indicator = '{"backtrace":'
+ line = sys.stdin.readline()
+ if not line:
+ return
+
+ line = line.strip()
+
+ if 'Frame: 0x' in line:
+ continue
+
+ if backtrace_indicator in line:
+ backtrace_index = line.index(backtrace_indicator)
+ prefix = line[:backtrace_index]
+ backtrace = line[backtrace_index:]
+ trace_doc = json.loads(backtrace)
+ if not trace_doc["backtrace"]:
+ print("Trace is empty, skipping...")
+ continue
+ frames = symbolize_frames(trace_doc, resolver, options.symbolizer_path, [],
+ options.output_format)
+ print(prefix)
+ print("Symbolizing...")
+ classic_output(frames, sys.stdout, indent=2)
+ else:
+ print(line)
+
+
+def main(options):
"""Execute Main program."""
- options = make_argument_parser(description=__doc__).parse_args()
+ resolver = None
+ if options.debug_file_resolver == 'path':
+ resolver = PathDbgFileResolver(options.path_to_executable)
+ elif options.debug_file_resolver == 's3':
+ resolver = S3BuildidDbgFileResolver(options.s3_cache_dir, options.s3_bucket)
+ elif options.debug_file_resolver == 'pr':
+ resolver = PathResolver(host=options.pr_host, cache_dir=options.pr_cache_dir)
+
+ if options.live:
+ print("Entering live mode")
+ substitute_stdin(options, resolver)
+ sys.exit(0)
# Skip over everything before the first '{' since it is likely to be log line prefixes.
# Additionally, using raw_decode() to ignore extra data after the closing '}' to allow maximal
# sloppiness in copy-pasting input.
trace_doc = sys.stdin.read()
+
+ if not trace_doc or not trace_doc.strip():
+ print("Please provide the backtrace through stdin for symbolization;"
+ "e.g. `your/symbolization/command < /file/with/stacktrace`")
trace_doc = trace_doc[trace_doc.find('{'):]
trace_doc = json.JSONDecoder().raw_decode(trace_doc)[0]
@@ -236,16 +611,23 @@ def main():
if options.output_format == 'classic':
output_fn = classic_output
- resolver = None
- if options.debug_file_resolver == 'path':
- resolver = PathDbgFileResolver(options.path_to_executable)
- elif options.debug_file_resolver == 's3':
- resolver = S3BuildidDbgFileResolver(options.s3_cache_dir, options.s3_bucket)
+ frames = preprocess_frames(resolver, trace_doc, options.input_format)
+
+ if options.src_dir_to_move and resolver.mci_build_dir is not None:
+ try:
+ os.makedirs(resolver.mci_build_dir)
+ os.symlink(
+ os.path.join(os.getcwd(), options.src_dir_to_move),
+ os.path.join(resolver.mci_build_dir, 'src'))
+ except FileExistsError:
+ pass
- frames = symbolize_frames(trace_doc, resolver, **vars(options))
+ frames = symbolize_frames(frames=frames, trace_doc=trace_doc, dbg_path_resolver=resolver,
+ **vars(options))
output_fn(frames, sys.stdout, indent=2)
if __name__ == '__main__':
- main()
+ symbolizer_options = make_argument_parser(description=__doc__).parse_args()
+ main(symbolizer_options)
sys.exit(0)
diff --git a/buildscripts/util/oauth.py b/buildscripts/util/oauth.py
new file mode 100644
index 00000000000..12726d06a85
--- /dev/null
+++ b/buildscripts/util/oauth.py
@@ -0,0 +1,303 @@
+"""Helper tools to get OAuth credentials using the PKCE flow."""
+from __future__ import annotations
+
+from datetime import datetime, timedelta
+from http.server import BaseHTTPRequestHandler, HTTPServer
+from random import choice
+from string import ascii_lowercase
+from typing import Any, Callable, Optional, Tuple
+from urllib.parse import parse_qs, urlsplit
+from webbrowser import open as web_open
+
+import requests
+from oauthlib.oauth2 import BackendApplicationClient
+from pkce import generate_pkce_pair
+from pydantic import ValidationError
+from pydantic.main import BaseModel
+from requests_oauthlib import OAuth2Session
+from buildscripts.util.fileops import read_yaml_file
+
+AUTH_HANDLER_RESPONSE = """\
+<html>
+ <head>
+ <title>Authentication Status</title>
+ <script>
+ window.onload = function() {
+ window.close();
+ }
+ </script>
+ </head>
+ <body>
+ <p>The authentication flow has completed.</p>
+ </body>
+</html>
+""".encode("utf-8")
+
+
+class Configs:
+ """Collect configurations necessary for authentication process."""
+
+ # pylint: disable=invalid-name
+ # pylint: disable=too-many-arguments
+
+ AUTH_DOMAIN = "corp.mongodb.com/oauth2/aus4k4jv00hWjNnps297"
+ CLIENT_ID = "0oa5zf9ps4N3JKWIJ297"
+ REDIRECT_PORT = 8989
+ SCOPE = "kanopy+openid+profile"
+
+ def __init__(self, client_credentials_scope: str = None,
+ client_credentials_user_name: str = None, auth_domain: str = None,
+ client_id: str = None, redirect_port: int = None, scope: str = None):
+ """Initialize configs instance."""
+
+ self.AUTH_DOMAIN = auth_domain or self.AUTH_DOMAIN
+ self.CLIENT_ID = client_id or self.CLIENT_ID
+ self.REDIRECT_PORT = redirect_port or self.REDIRECT_PORT
+ self.SCOPE = scope or self.SCOPE
+ self.CLIENT_CREDENTIALS_SCOPE = client_credentials_scope
+ self.CLIENT_CREDENTIALS_USER_NAME = client_credentials_user_name
+
+
+class OAuthCredentials(BaseModel):
+ """OAuth access token and its associated metadata."""
+
+ expires_in: int
+ access_token: str
+ created_time: datetime
+ user_name: str
+
+ def are_expired(self) -> bool:
+ """
+ Check whether the current OAuth credentials are expired or not.
+
+ :return: Whether the credentials are expired or not.
+ """
+ return self.created_time + timedelta(seconds=self.expires_in) < datetime.now()
+
+ @classmethod
+ def get_existing_credentials_from_file(cls, file_path: str) -> Optional[OAuthCredentials]:
+ """
+ Try to get OAuth credentials from a file location.
+
+ Will return None if credentials either don't exist or are expired.
+ :param file_path: Location to check for OAuth credentials.
+ :return: Valid OAuth credentials or None if valid credentials don't exist
+ """
+ try:
+ creds = OAuthCredentials(**read_yaml_file(file_path))
+ if (creds.access_token and creds.created_time and creds.expires_in and creds.user_name
+ and not creds.are_expired()):
+ return creds
+ else:
+ return None
+ except ValidationError:
+ return None
+ except OSError:
+ return None
+
+
+class _RedirectServer(HTTPServer):
+ """HTTP server to use when fetching OAuth credentials using the PKCE flow."""
+
+ # pylint: disable=too-many-arguments
+
+ pkce_credentials: Optional[OAuthCredentials] = None
+ auth_domain: str
+ client_id: str
+ redirect_uri: str
+ code_verifier: str
+
+ def __init__(
+ self,
+ server_address: Tuple[str, int],
+ handler: Callable[..., BaseHTTPRequestHandler],
+ redirect_uri: str,
+ auth_domain: str,
+ client_id: str,
+ code_verifier: str,
+ ):
+ self.redirect_uri = redirect_uri
+ self.auth_domain = auth_domain
+ self.client_id = client_id
+ self.code_verifier = code_verifier
+ super().__init__(server_address, handler)
+
+
+class _Handler(BaseHTTPRequestHandler):
+ """Request handler class to use when trying to get OAuth credentials."""
+
+ # pylint: disable=invalid-name
+
+ server: _RedirectServer
+
+ def _set_response(self) -> None:
+ """Set the response to the server making a request."""
+ self.send_response(200)
+ self.send_header("Content-type", "text/html")
+ self.end_headers()
+
+ def log_message(self, log_format: Any, *args: Any) -> None: # pylint: disable=unused-argument,arguments-differ
+ """
+ Log HTTP Server internal messages.
+
+ :param log_format: The format to use when logging messages.
+ :param args: Key word args.
+ """
+ return None
+
+ def do_GET(self) -> None:
+ """Handle the callback response from the auth server."""
+ params = parse_qs(urlsplit(self.path).query)
+ code = params.get("code")
+
+ if not code:
+ raise ValueError("Could not get authorization code when signing in to Okta")
+
+ url = f"https://{self.server.auth_domain}/v1/token"
+ body = {
+ "grant_type": "authorization_code",
+ "client_id": self.server.client_id,
+ "code_verifier": self.server.code_verifier,
+ "code": code,
+ "redirect_uri": self.server.redirect_uri,
+ }
+
+ resp = requests.post(url, data=body).json()
+
+ access_token = resp.get("access_token")
+ expires_in = resp.get("expires_in")
+
+ if not access_token or not expires_in:
+ raise ValueError("Could not get access token or expires_in data about access token")
+
+ headers = {"Authorization": f"Bearer {access_token}"}
+ resp = requests.get(f"https://{self.server.auth_domain}/v1/userinfo",
+ headers=headers).json()
+
+ split_username = resp["preferred_username"].split("@")
+
+ if len(split_username) != 2:
+ raise ValueError("Could not get user_name of current user")
+
+ self.server.pkce_credentials = OAuthCredentials(
+ access_token=access_token,
+ expires_in=expires_in,
+ created_time=datetime.now(),
+ user_name=split_username[0],
+ )
+ self._set_response()
+ self.wfile.write(AUTH_HANDLER_RESPONSE)
+
+
+class PKCEOauthTools:
+ """Basic toolset to get OAuth credentials using the PKCE flow."""
+
+ auth_domain: str
+ client_id: str
+ redirect_port: int
+ redirect_uri: str
+ scope: str
+
+ def __init__(self, auth_domain: str, client_id: str, redirect_port: int, scope: str):
+ """
+ Create a new PKCEOauth tools instance.
+
+ :param auth_domain: The uri of the auth server to get the credentials from.
+ :param client_id: The id of the client that you are using to authenticate.
+ :param redirect_port: Port to use when setting up the local server for the auth redirect.
+ :param scope: The OAuth scopes to request access for.
+ """
+ self.auth_domain = auth_domain
+ self.client_id = client_id
+ self.redirect_port = redirect_port
+ self.redirect_uri = f"http://localhost:{redirect_port}/"
+ self.scope = scope
+
+ def get_pkce_credentials(self, print_auth_url: bool = False) -> OAuthCredentials:
+ """
+ Try to get an OAuth access token and its associated metadata.
+
+ :param print_auth_url: Whether to print the auth url to the console instead of opening it.
+ :return: OAuth credentials and some associated metadata to check if they have expired.
+ """
+ code_verifier, code_challenge = generate_pkce_pair()
+
+ state = "".join(choice(ascii_lowercase) for i in range(10))
+
+ authorization_url = (f"https://{self.auth_domain}/v1/authorize?"
+ f"scope={self.scope}&"
+ f"response_type=code&"
+ f"response_mode=query&"
+ f"client_id={self.client_id}&"
+ f"code_challenge={code_challenge}&"
+ f"state={state}&"
+ f"code_challenge_method=S256&"
+ f"redirect_uri={self.redirect_uri}")
+
+ httpd = _RedirectServer(
+ ("", self.redirect_port),
+ _Handler,
+ self.redirect_uri,
+ self.auth_domain,
+ self.client_id,
+ code_verifier,
+ )
+ if print_auth_url:
+ print("Please open the below url in a browser and sign in if necessary")
+ print(authorization_url)
+ else:
+ web_open(authorization_url)
+ httpd.handle_request()
+
+ if not httpd.pkce_credentials:
+ raise ValueError(
+ "Could not retrieve Okta credentials to talk to Kanopy with. "
+ "Please sign out of Okta in your browser and try runnning this script again")
+
+ return httpd.pkce_credentials
+
+
+def get_oauth_credentials(configs: Configs, print_auth_url: bool = False) -> OAuthCredentials:
+ """
+ Run the OAuth workflow to get credentials for a human user.
+
+ :param configs: Configs instance.
+ :param print_auth_url: Whether to print the auth url to the console instead of opening it.
+ :return: OAuth credentials for the given user.
+ """
+ oauth_tools = PKCEOauthTools(auth_domain=configs.AUTH_DOMAIN, client_id=configs.CLIENT_ID,
+ redirect_port=configs.REDIRECT_PORT, scope=configs.SCOPE)
+ credentials = oauth_tools.get_pkce_credentials(print_auth_url)
+ return credentials
+
+
+def get_client_cred_oauth_credentials(client_id: str, client_secret: str,
+ configs: Configs) -> OAuthCredentials:
+ """
+ Run the OAuth workflow to get credentials for a machine user.
+
+ :param client_id: The client_id of the machine user to authenticate as.
+ :param client_secret: The client_secret of the machine user to authenticate as.
+ :param configs: Configs instance.
+ :return: OAuth credentials for the given machine user.
+ """
+ client = BackendApplicationClient(client_id=client_id)
+ oauth = OAuth2Session(client=client)
+ token = oauth.fetch_token(
+ token_url=f"https://{configs.AUTH_DOMAIN}/v1/token",
+ client_id=client_id,
+ client_secret=client_secret,
+ scope=configs.CLIENT_CREDENTIALS_SCOPE,
+ )
+ access_token = token.get("access_token")
+ expires_in = token.get("expires_in")
+
+ if not access_token or not expires_in:
+ raise ValueError("Could not get access token or expires_in data about access token")
+
+ return OAuthCredentials(
+ access_token=access_token,
+ expires_in=expires_in,
+ created_time=datetime.now(),
+ user_name=configs.CLIENT_CREDENTIALS_USER_NAME,
+ )
diff --git a/etc/evergreen.yml b/etc/evergreen.yml
index cc065caba80..25ce3658d27 100644
--- a/etc/evergreen.yml
+++ b/etc/evergreen.yml
@@ -6883,6 +6883,22 @@ tasks:
vars:
resmoke_args: --suites=mqlrun
+- name: generate_buildid_to_debug_symbols_mapping
+ tags: ["symbolizer"]
+ stepback: false
+ patchable: true
+ depends_on:
+ - archive_dist_test_debug
+ commands:
+ - *f_expansions_write
+ - func: "do setup"
+ - func: "configure evergreen api credentials"
+ - command: subprocess.exec
+ params:
+ binary: bash
+ args:
+ - "./src/evergreen/generate_buildid_debug_symbols_mapping.sh"
+
#######################################
# Task Groups #
#######################################
@@ -7205,6 +7221,7 @@ buildvariants:
- name: server_selection_json_test_TG
distros:
- rhel80-large
+ - name: generate_buildid_to_debug_symbols_mapping
- name: linux-64-duroff
@@ -7238,6 +7255,7 @@ buildvariants:
- name: failpoints_auth
- name: .jscore .common !.sharding !.decimal !.txns
- name: .jstestfuzz .common !.sharding !.repl
+ - name: generate_buildid_to_debug_symbols_mapping
- name: ubuntu1804
display_name: Ubuntu 18.04
@@ -7295,6 +7313,7 @@ buildvariants:
- name: .publish
distros:
- ubuntu1804-small
+ - name: generate_buildid_to_debug_symbols_mapping
- name: enterprise-ubuntu1804-64
display_name: Enterprise Ubuntu 18.04
@@ -7367,6 +7386,7 @@ buildvariants:
- name: .publish
distros:
- ubuntu1804-small
+ - name: generate_buildid_to_debug_symbols_mapping
- name: tla-plus
display_name: TLA+
@@ -7431,6 +7451,7 @@ buildvariants:
- name: .publish
distros:
- ubuntu1804-test
+ - name: generate_buildid_to_debug_symbols_mapping
- name: ubuntu1804-arm64
display_name: Ubuntu 18.04 arm64
@@ -7466,6 +7487,7 @@ buildvariants:
- name: .publish
distros:
- ubuntu1804-test
+ - name: generate_buildid_to_debug_symbols_mapping
- name: ubuntu2004
display_name: Ubuntu 20.04
@@ -7517,6 +7539,7 @@ buildvariants:
- name: .publish
distros:
- ubuntu2004-small
+ - name: generate_buildid_to_debug_symbols_mapping
- name: enterprise-ubuntu2004-64
display_name: Enterprise Ubuntu 20.04
@@ -7569,6 +7592,7 @@ buildvariants:
- name: .publish
distros:
- ubuntu2004-small
+ - name: generate_buildid_to_debug_symbols_mapping
- name: enterprise-ubuntu2004-arm64
display_name: Enterprise Ubuntu 20.04 arm64
@@ -7621,6 +7645,7 @@ buildvariants:
- name: .publish
distros:
- ubuntu2004-test
+ - name: generate_buildid_to_debug_symbols_mapping
- name: ubuntu2004-arm64
display_name: Ubuntu 20.04 arm64
@@ -7655,6 +7680,7 @@ buildvariants:
- name: .publish
distros:
- ubuntu2004-test
+ - name: generate_buildid_to_debug_symbols_mapping
- name: enterprise-linux-64-amazon-ami
display_name: "Enterprise Amazon Linux"
@@ -7712,6 +7738,7 @@ buildvariants:
- name: .publish
distros:
- amazon1-2018-small
+ - name: generate_buildid_to_debug_symbols_mapping
- name: amazon
display_name: Amazon Linux
@@ -7766,6 +7793,7 @@ buildvariants:
- name: .publish
distros:
- amazon1-2018-small
+ - name: generate_buildid_to_debug_symbols_mapping
- name: enterprise-amazon2
display_name: "Enterprise Amazon Linux 2"
@@ -7822,6 +7850,7 @@ buildvariants:
- name: .publish
distros:
- amazon2-small
+ - name: generate_buildid_to_debug_symbols_mapping
- name: amazon2
display_name: Amazon Linux 2
@@ -7876,6 +7905,7 @@ buildvariants:
- name: .publish
distros:
- amazon2-small
+ - name: generate_buildid_to_debug_symbols_mapping
- name: enterprise-amazon2-arm64
display_name: "Enterprise Amazon Linux 2 arm64"
@@ -7931,6 +7961,7 @@ buildvariants:
- name: .publish
distros:
- rhel80-small
+ - name: generate_buildid_to_debug_symbols_mapping
- name: amazon2-arm64
display_name: Amazon Linux 2 arm64
@@ -7980,6 +8011,7 @@ buildvariants:
- name: .publish
distros:
- rhel80-small
+ - name: generate_buildid_to_debug_symbols_mapping
- name: stm-daily-cron
modules:
@@ -8719,6 +8751,7 @@ buildvariants:
distros:
- ubuntu2004-package
- name: .publish
+ - name: generate_buildid_to_debug_symbols_mapping
- &enterprise-rhel-80-64-bit-dynamic-required-template
name: enterprise-rhel-80-64-bit-dynamic-required
@@ -9007,6 +9040,7 @@ buildvariants:
- name: sharded_multi_stmt_txn_jscore_passthrough
distros:
- rhel80-medium
+ - name: generate_buildid_to_debug_symbols_mapping
# This build variant is used to run multiversion tests as part of burn_in_tags as these tests are
# currently only run on our daily builders.
@@ -9337,6 +9371,7 @@ buildvariants:
- name: .publish
distros:
- rhel70-small
+ - name: generate_buildid_to_debug_symbols_mapping
- name: ubi8
display_name: "UBI 8"
@@ -9418,6 +9453,7 @@ buildvariants:
- name: .publish
distros:
- rhel80-small
+ - name: generate_buildid_to_debug_symbols_mapping
- name: enterprise-rhel-82-arm64
display_name: "Enterprise RHEL 8.2 arm64"
@@ -9472,6 +9508,7 @@ buildvariants:
- name: .publish
distros:
- rhel80-small
+ - name: generate_buildid_to_debug_symbols_mapping
# This variant is to intentionally test uncommon features nightly
- <<: *enterprise-rhel-70-64-bit-template
@@ -9652,6 +9689,7 @@ buildvariants:
- name: .publish
distros:
- rhel70-small
+ - name: generate_buildid_to_debug_symbols_mapping
- name: rhel80
display_name: RHEL 8.0
@@ -9703,6 +9741,7 @@ buildvariants:
- name: .publish
distros:
- rhel80-small
+ - name: generate_buildid_to_debug_symbols_mapping
- name: rhel-82-arm64
display_name: RHEL 8.2 arm64
@@ -9751,6 +9790,7 @@ buildvariants:
- name: .publish
distros:
- rhel80-small
+ - name: generate_buildid_to_debug_symbols_mapping
# This variant compiles on RHEL 7.0 and runs tests on RHEL 7.6
- name: rhel76_compile_rhel70
@@ -9819,6 +9859,7 @@ buildvariants:
- name: .publish
distros:
- rhel70-small
+ - name: generate_buildid_to_debug_symbols_mapping
- name: enterprise-rhel-72-s390x-compile
display_name: Enterprise RHEL 7.2 s390x Compile
@@ -9849,6 +9890,7 @@ buildvariants:
- name: compile_test_and_package_serial_TG
distros:
- rhel72-zseries-build
+ - name: generate_buildid_to_debug_symbols_mapping
- name: enterprise-rhel-72-s390x
display_name: Enterprise RHEL 7.2 s390x
@@ -9901,6 +9943,7 @@ buildvariants:
- name: .publish
distros:
- rhel70-small
+ - name: generate_buildid_to_debug_symbols_mapping
###########################################
# Ubuntu buildvariants #
@@ -9977,6 +10020,7 @@ buildvariants:
- name: .publish
distros:
- suse12-small
+ - name: generate_buildid_to_debug_symbols_mapping
- name: suse12
display_name: SUSE 12
@@ -10027,6 +10071,7 @@ buildvariants:
- name: .publish
distros:
- suse12-small
+ - name: generate_buildid_to_debug_symbols_mapping
- name: enterprise-suse15-64
display_name: Enterprise SLES 15
@@ -10068,6 +10113,7 @@ buildvariants:
- name: .publish
distros:
- suse15-small
+ - name: generate_buildid_to_debug_symbols_mapping
- name: suse15
display_name: SUSE 15
@@ -10115,6 +10161,7 @@ buildvariants:
- name: .publish
distros:
- suse15-small
+ - name: generate_buildid_to_debug_symbols_mapping
###########################################
# Debian buildvariants #
@@ -10167,6 +10214,7 @@ buildvariants:
- name: .publish
distros:
- debian92-small
+ - name: generate_buildid_to_debug_symbols_mapping
- name: debian92
display_name: Debian 9.2
@@ -10219,6 +10267,7 @@ buildvariants:
- name: .publish
distros:
- debian92-small
+ - name: generate_buildid_to_debug_symbols_mapping
- name: enterprise-debian10-64
display_name: Enterprise Debian 10
@@ -10266,6 +10315,7 @@ buildvariants:
- name: .publish
distros:
- debian10-small
+ - name: generate_buildid_to_debug_symbols_mapping
- name: debian10
display_name: Debian 10
@@ -10317,6 +10367,7 @@ buildvariants:
- name: .publish
distros:
- debian10-small
+ - name: generate_buildid_to_debug_symbols_mapping
################################
# storage engine buildvariants #
diff --git a/etc/pip/components/core.req b/etc/pip/components/core.req
index 2d6490469e4..dc559640af4 100644
--- a/etc/pip/components/core.req
+++ b/etc/pip/components/core.req
@@ -3,3 +3,6 @@ psutil <= 5.8.0
pymongo >= 3.9, < 4.0
PyYAML >= 3.0.0, <= 6.0.0
requests >= 2.0.0, <= 2.26.0
+pkce == 1.0.3
+oauthlib == 3.1.1
+requests-oauthlib == 1.3.0 \ No newline at end of file
diff --git a/evergreen/generate_buildid_debug_symbols_mapping.sh b/evergreen/generate_buildid_debug_symbols_mapping.sh
new file mode 100755
index 00000000000..104cc21a762
--- /dev/null
+++ b/evergreen/generate_buildid_debug_symbols_mapping.sh
@@ -0,0 +1,15 @@
+DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null 2>&1 && pwd)"
+. "$DIR/prelude.sh"
+
+cd src
+
+set -o errexit
+set -o verbose
+
+activate_venv
+python -m pip --disable-pip-version-check install "db-contrib-tool==0.1.5"
+$python buildscripts/debugsymb_mapper.py \
+ --version "${version_id}" \
+ --client-id "${symbolizer_client_id}" \
+ --client-secret "${symbolizer_client_secret}" \
+ --variant "${build_variant}"