0
0
mirror of https://github.com/mongodb/mongo.git synced 2024-12-01 09:32:32 +01:00
mongodb/buildscripts/package_test.py
2022-10-15 00:07:30 +00:00

403 lines
16 KiB
Python

import argparse
import dataclasses
import json
import logging
import os
import platform
import sys
import time
import traceback
import uuid
from concurrent import futures
from pathlib import Path
from typing import Any, Dict, Generator, List, Optional, Tuple, Set
import docker
import docker.errors
import requests
from simple_report import Result, Report
root = logging.getLogger()
root.setLevel(logging.DEBUG)
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter('[%(asctime)s]%(levelname)s:%(message)s')
handler.setFormatter(formatter)
root.addHandler(handler)
PACKAGE_MANAGER_COMMANDS = {
"apt": {
"update": "export DEBIAN_FRONTEND=noninteractive; apt-get update -y",
"install": "export DEBIAN_FRONTEND=noninteractive; apt-get install -y {}",
},
"yum": {
"update": "yum update -y",
"install": "yum install -y {}",
},
"zypper": {
"update": "zypper -n update",
"install": "zypper -n install {}",
},
}
DOCKER_SYSTEMCTL_REPO = "https://raw.githubusercontent.com/gdraheim/docker-systemctl-replacement"
SYSTEMCTL_URL = f"{DOCKER_SYSTEMCTL_REPO}/master/files/docker/systemctl3.py"
JOURNALCTL_URL = f"{DOCKER_SYSTEMCTL_REPO}/master/files/docker/journalctl3.py"
# Lookup table used when building and running containers
# os_name, Optional[(base_image, package_manager, frozenset(base_packages), python_command)]
OS_DOCKER_LOOKUP = {
'amazon': None,
'amzn64': None,
# TODO(SERVER-69982) This can be reenabled when the ticket is fixed
# 'amazon': ('amazonlinux:1', "yum", frozenset(["python38", "wget", "pkgconfig", "systemd"]), "python3"),
# 'amzn64': ('amazonlinux:1', "yum", frozenset(["python38", "wget", "pkgconfig", "systemd"]), "python3"),
'amazon2': ('amazonlinux:2', "yum", frozenset(["python3", "wget", "pkgconfig", "systemd"]),
"python3"),
'amazon2022': ('amazonlinux:2022', "yum", frozenset(["python3", "wget", "pkgconfig",
"systemd"]), "python3"),
'debian10': ('debian:10-slim', "apt", frozenset(["python3", "wget", "pkg-config", "systemd"]),
"python3"),
'debian11': ('debian:11-slim', "apt", frozenset(["python3", "wget", "pkg-config", "systemd"]),
"python3"),
'debian71': ('debian:7-slim', "apt", frozenset(["python3", "wget", "pkg-config", "systemd"]),
"python3"),
'debian81': ('debian:8-slim', "apt", frozenset(["python3", "wget", "pkg-config", "systemd"]),
"python3"),
'debian92': ('debian:9-slim', "apt", frozenset(["python3", "wget", "pkg-config", "systemd"]),
"python3"),
'linux_i686': None,
'linux_x86_64': None,
'macos': None,
'osx': None,
'osx-ssl': None,
'rhel55': None,
'rhel57': None,
'rhel62': None,
'rhel70': ('registry.access.redhat.com/ubi7/ubi:7.9', "yum",
frozenset(["rh-python38.x86_64", "wget", "pkgconfig", "systemd"]),
"/opt/rh/rh-python38/root/usr/bin/python3"),
'rhel71': ('registry.access.redhat.com/ubi7/ubi:7.9', "yum",
frozenset(["rh-python38.x86_64", "wget", "pkgconfig", "systemd"]),
"/opt/rh/rh-python38/root/usr/bin/python3"),
'rhel72': ('registry.access.redhat.com/ubi7/ubi:7.9', "yum",
frozenset(["rh-python38.x86_64", "wget", "pkgconfig", "systemd"]),
"/opt/rh/rh-python38/root/usr/bin/python3"),
'rhel80': ('redhat/ubi8', "yum", frozenset(["python3", "wget", "pkgconfig", "systemd"]),
"python3"),
'rhel81': ('redhat/ubi8', "yum", frozenset(["python3", "wget", "pkgconfig", "systemd"]),
"python3"),
'rhel82': ('redhat/ubi8', "yum", frozenset(["python3", "wget", "pkgconfig", "systemd"]),
"python3"),
'rhel83': ('redhat/ubi8', "yum", frozenset(["python3", "wget", "pkgconfig", "systemd"]),
"python3"),
'rhel90': ('redhat/ubi9', "yum", frozenset(["python3", "wget", "pkgconfig", "systemd"]),
"python3"),
'sunos5': None,
'suse11': None,
'suse12': None,
# ('registry.suse.com/suse/sles12sp5:latest', "zypper", frozenset(["python3", "wget", "pkg-config", "systemd"]), "python3"),
# The offical repo fails with the following error
# Problem retrieving the repository index file for service 'container-suseconnect-zypp':
# [container-suseconnect-zypp|file:/usr/lib/zypp/plugins/services/container-suseconnect-zypp]
'suse15': ('opensuse/leap:15', "zypper", frozenset(["python3", "wget", "pkg-config",
"systemd"]), "python3"),
# 'suse15': ('registry.suse.com/suse/sle15:latest', "zypper", frozenset(["python3", "wget", "pkg-config", "systemd"]), "python3"),
# Has the same error as above
'ubuntu1204': None,
'ubuntu1404': None,
'ubuntu1604': ('ubuntu:16.04', "apt", frozenset(["python3", "wget", "pkg-config", "systemd"]),
"python3"),
'ubuntu1804': ('ubuntu:18.04', "apt", frozenset(["python3", "wget", "pkg-config", "systemd"]),
"python3"),
'ubuntu2004': ('ubuntu:20.04', "apt", frozenset(["python3", "wget", "pkg-config", "systemd"]),
"python3"),
'ubuntu2204': ('ubuntu:22.04', "apt", frozenset(["python3", "wget", "pkg-config", "systemd"]),
"python3"),
'windows': None,
'windows_i686': None,
'windows_x86_64': None,
'windows_x86_64-2008plus': None,
'windows_x86_64-2008plus-ssl': None,
'windows_x86_64-2012plus': None,
}
VERSIONS_TO_SKIP = set(['3.0.15', '3.2.22', '3.4.24', '3.6.23', '4.0.28'])
# TODO(SERVER-70016) These can be deleted once these versions are no longer is current.json
DISABLED_TESTS = [("amazon2", "4.4.16"), ("amazon2", "4.4.17-rc2"), ("amazon2", "4.2.23-rc0"),
("amazon2", "4.2.23-rc1"), ("amazon2", "4.2.22")]
@dataclasses.dataclass
class Test:
"""Class to track a single test."""
os_name: str
version: str
base_image: str = dataclasses.field(default="", repr=False)
package_manager: str = dataclasses.field(default="", repr=False)
update_command: str = dataclasses.field(default="", repr=False)
install_command: str = dataclasses.field(default="", repr=False)
base_packages: str = dataclasses.field(default="", repr=False)
python_command: str = dataclasses.field(default="", repr=False)
packages_urls: List[str] = dataclasses.field(default_factory=list)
packages_paths: List[Path] = dataclasses.field(default_factory=list)
def __post_init__(self) -> None:
assert OS_DOCKER_LOOKUP[self.os_name] is not None
self.base_image = OS_DOCKER_LOOKUP[self.os_name][0]
self.package_manager = OS_DOCKER_LOOKUP[self.os_name][1]
self.base_packages = OS_DOCKER_LOOKUP[self.os_name][2]
self.python_command = OS_DOCKER_LOOKUP[self.os_name][3]
self.update_command = PACKAGE_MANAGER_COMMANDS[self.package_manager]["update"]
self.install_command = PACKAGE_MANAGER_COMMANDS[self.package_manager]["install"]
def __repr__(self) -> str:
ret = f"\nTest:\n\tos: {self.os_name}\n"
ret += f"image: {self.base_image}\n"
ret += f"\tversion: {self.version}\n"
ret += f"\tpackages_urls: {self.packages_urls}\n"
ret += f"\tpackages_paths: {self.packages_paths}\n"
return ret
def name(self) -> str:
return self.os_name + "-" + self.version
def build_image(test: Test) -> str:
commands: List[str] = [
test.update_command,
test.install_command.format(" ".join(test.base_packages)),
"mkdir -p /run/systemd/system",
"mkdir -p $(pkg-config systemd --variable=systemdsystempresetdir)",
"echo 'disable *' > $(pkg-config systemd --variable=systemdsystempresetdir)/00-test.preset",
f"wget -P /usr/bin {SYSTEMCTL_URL}",
f"wget -P /usr/bin {JOURNALCTL_URL}",
"chmod +x /usr/bin/systemctl3.py /usr/bin/journalctl3.py",
"ln -sf /usr/bin/systemctl3.py /bin/systemd",
"ln -sf /usr/bin/systemctl3.py /usr/bin/systemd",
]
if test.python_command != 'python3':
commands.append(f"ln -s {test.python_command} /usr/bin/python3")
logging.info("Building base image for %s: %s", test.os_name, test.base_image)
client = docker.from_env()
container = client.containers.run(
test.base_image, ["/bin/bash", "-x", "-c", " && ".join(commands)], detach=True, tty=True)
# Wait for the container to finish and exit (timeout is in seconds)
container.wait(timeout=120)
logging.debug(container.logs().decode('utf-8'))
return container.commit(repository=f"localhost/{test.os_name}", changes="CMD /bin/systemd")
def run_test(test: Test) -> Result:
result = Result(status="pass", test_file=test.name(), start=time.time(), log_raw="")
client = docker.from_env()
log_name = f"logs/{test.os_name}_{test.version}_{uuid.uuid4().hex}.log"
test_docker_root = Path("/mnt/package_test").resolve()
log_docker_path = Path.joinpath(test_docker_root, log_name)
test_external_root = Path(__file__).parent.resolve()
logging.debug(test_external_root)
log_external_path = Path.joinpath(test_external_root, log_name)
os.makedirs(log_external_path.parent, exist_ok=True)
command = f"bash -c \"{test.python_command} /mnt/package_test/package_test_internal.py {log_docker_path} {' '.join(test.packages_urls)}\""
logging.debug("Attemtping to run the following docker command: %s", command)
try:
image = build_image(test)
container: docker.Container = client.containers.run(
image, command=command, auto_remove=True, detach=True,
volumes=[f'{test_external_root}:{test_docker_root}'])
for log in container.logs(stream=True):
result["log_raw"] += log.decode('UTF-8')
# This is pretty verbose, lets run this way for a while and we can delete this if it ends up being too much
logging.debug(log.decode('UTF-8').strip())
exit_code = container.wait()
result["exit_code"] = exit_code['StatusCode']
except docker.errors.APIError as exc:
traceback.print_exception(type(exc), exc, exc.__traceback__)
logging.error("Failed to start docker container")
result["end"] = time.time()
result['status'] = 'fail'
result["exit_code"] = 1
return result
try:
with open(log_external_path, 'r') as log_raw:
result["log_raw"] += log_raw.read()
except OSError as oserror:
logging.error("Failed to open %s with error %s", log_external_path, oserror)
if exit_code['StatusCode'] != 0:
logging.error("Failed test %s with exit code %s", test, exit_code)
result['status'] = 'fail'
result["end"] = time.time()
return result
logging.info("Attemping to download current mongo releases json")
r = requests.get('https://downloads.mongodb.org/current.json')
current_releases = r.json()
logging.info("Attemping to download current mongo tools releases json")
r = requests.get('https://downloads.mongodb.org/tools/db/release.json')
current_tools_releases = r.json()
logging.info("Attemping to download current mongosh releases json")
r = requests.get('https://s3.amazonaws.com/info-mongodb-com/com-download-center/mongosh.json')
mongosh_releases = r.json()
def iterate_over_downloads() -> Generator[Dict[str, Any], None, None]:
for version in current_releases["versions"]:
for download in version["downloads"]:
if download["edition"] == "source":
continue
if version["version"] in VERSIONS_TO_SKIP:
continue
download["version"] = version["version"]
yield download
def get_tools_package(arch_name: str, os_name: str) -> Optional[str]:
for download in current_tools_releases["versions"][0]["downloads"]:
if download["name"] == os_name and download["arch"] == arch_name:
return download["package"]["url"]
return None
def get_mongosh_package(arch_name: str, os_name: str) -> Optional[str]:
if arch_name == "x86_64":
arch_name = "x64"
pkg_ext = "rpm"
if "debian" in os_name or "ubuntu" in os_name:
pkg_ext = "deb"
for platform_type in mongosh_releases["versions"][0]["platform"]:
if platform_type["os"] == pkg_ext and platform_type["arch"] == arch_name:
return platform_type["download_link"]
return None
arches: Set[str] = set()
oses: Set[str] = set()
editions: Set[str] = set()
versions: Set[str] = set()
for dl in iterate_over_downloads():
editions.add(dl["edition"])
arches.add(dl["arch"])
oses.add(dl["target"])
versions.add(dl["version"])
parser = argparse.ArgumentParser(
description=
'Test packages on various hosts. This will spin up docker containers and test the installs.')
parser.add_argument("--arch", type=str, help="Arch of host machine to use",
choices=["auto"] + list(arches), default="auto")
parser.add_argument(
"--os", type=str, help=
"OS of docker image to run test(s) on. All means run all os tests on this arch. None means run no os test on this arch (except for one specified in extra-packages.",
choices=["all", "none"] + list(oses), default="all")
parser.add_argument(
"-e", "--extra-test", type=str, help=
"Space-separated tuple of (test_os, package_archive_path). For example ubuntu2004,https://s3.amazonaws.com/mciuploads/${project}/${build_variant}/${revision}/artifacts/${build_id}-packages.tgz.",
action='append', nargs=2, default=[])
args = parser.parse_args()
mongo_os: str = args.os
extra_tests: List[Tuple[str, str]] = args.extra_test
arch: str = args.arch
if arch == "auto":
arch = platform.machine()
tests: List[Test] = []
for extra_test in extra_tests:
test_os = extra_test[0]
logging.info(extra_test[1])
urls: List[str] = [extra_test[1]]
if test_os not in OS_DOCKER_LOOKUP:
logging.error("We have not seen this OS %s before, please add it to OS_DOCKER_LOOKUP",
test_os)
sys.exit(1)
if not OS_DOCKER_LOOKUP[test_os]:
logging.info("Skipping test on target because the OS has no associated container %s->???",
test_os)
continue
tools_package = get_tools_package(arch, test_os)
mongosh_package = get_mongosh_package(arch, test_os)
if tools_package:
urls.append(tools_package)
else:
logging.warning("Could not find tools package for %s and %s", arch, test_os)
if mongosh_package:
urls.append(mongosh_package)
else:
logging.warning("Could not find mongosh package for %s and %s", arch, test_os)
tests.append(Test(os_name=test_os, version="custom", packages_urls=urls))
# If os is None we only want to do the tests specified in the arguments
if mongo_os != "none":
for dl in iterate_over_downloads():
if mongo_os not in ["all", dl["target"]]:
continue
if dl["arch"] != arch:
continue
if not OS_DOCKER_LOOKUP[dl["target"]]:
logging.info(
"Skipping test on target because the OS has no associated container %s->??? on mongo version %s",
dl['target'], dl['version'])
continue
if not "packages" in dl:
logging.info(
"Skipping test on target because there are no packages %s->??? on mongo version %s",
dl['target'], dl['version'])
continue
if (dl["target"], dl["version"]) in DISABLED_TESTS:
continue
tests.append(
Test(os_name=dl["target"], packages_urls=dl["packages"], version=dl["version"]))
report = Report(results=[], failures=0)
with futures.ThreadPoolExecutor() as tpe:
test_futures = [tpe.submit(run_test, test) for test in tests]
completed_tests = 0 # pylint: disable=invalid-name
for f in futures.as_completed(test_futures):
completed_tests += 1
test_result = f.result()
if test_result["exit_code"] != 0:
report["failures"] += 1
report["results"].append(test_result)
logging.info("Completed %s/%s tests", completed_tests, len(test_futures))
with open("report.json", "w") as fh:
json.dump(report, fh)
if report["failures"] == 0:
logging.info("All %s tests passed :)", len(report['results']))
sys.exit(0)
else:
success_count = sum([1 for test_result in report["results"] if test_result["exit_code"] == 0])
logging.info("%s/%s tests passed", success_count, len(report['results']))
sys.exit(1)