diff options
author | Jasur Nurboyev <bluestacks6523@gmail.com> | 2022-01-12 17:34:48 +0000 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2022-01-12 18:27:21 +0000 |
commit | df2f772316ab5cb43d22e00b85c45ba7e52a903d (patch) | |
tree | d4869140c04fabca0da7c2fc59e781198ebb840c | |
parent | 1ccc8af43a3698b4178c8d2bd810e395bc81f0ac (diff) | |
download | mongo-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.py | 363 | ||||
-rwxr-xr-x | buildscripts/mongosymb.py | 528 | ||||
-rw-r--r-- | buildscripts/util/oauth.py | 303 | ||||
-rw-r--r-- | etc/evergreen.yml | 51 | ||||
-rw-r--r-- | etc/pip/components/core.req | 3 | ||||
-rwxr-xr-x | evergreen/generate_buildid_debug_symbols_mapping.sh | 15 |
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}" |