311 lines
12 KiB
Python
311 lines
12 KiB
Python
import json
|
|
import sys
|
|
import os
|
|
from concurrent import futures
|
|
import dataclasses
|
|
from pathlib import Path
|
|
import platform
|
|
from typing import Any, Dict, Generator, List, Optional, Set
|
|
import argparse
|
|
import uuid
|
|
import time
|
|
import traceback
|
|
import logging
|
|
import requests
|
|
import docker
|
|
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)
|
|
|
|
APT_PYTHON_INSTALL = "apt update && apt install -y python3 && python3"
|
|
YUM_PYTHON_INSTALL = "yum update && yum install -y python3 && python3"
|
|
ZYPPER_PYTHON_INSTALL = "zypper -n update && zypper -n install python3 && python3"
|
|
UBI7_PYTHON_INSTALL = "yum update -y && yum install -y rh-python38.x86_64 && /opt/rh/rh-python38/root/usr/bin/python3"
|
|
AMAZON1_PYTHON_INSTALL = "yum update -y && yum install -y python38 && python3"
|
|
|
|
OS_DOCKER_LOOKUP = {
|
|
'amazon': None,
|
|
'amzn64': None,
|
|
# TODO(SERVER-69982) This can be reenabled when the ticket is fixed
|
|
# 'amazon': ('amazonlinux:1', AMAZON1_PYTHON_INSTALL),
|
|
# 'amzn64': ('amazonlinux:1', AMAZON1_PYTHON_INSTALL),
|
|
'amazon2': ('amazonlinux:2', YUM_PYTHON_INSTALL),
|
|
'debian10': ('debian:10-slim', APT_PYTHON_INSTALL),
|
|
'debian11': ('debian:11-slim', APT_PYTHON_INSTALL),
|
|
'debian71': ('debian:7-slim', APT_PYTHON_INSTALL),
|
|
'debian81': ('debian:8-slim', APT_PYTHON_INSTALL),
|
|
'debian92': ('debian:9-slim', APT_PYTHON_INSTALL),
|
|
'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', UBI7_PYTHON_INSTALL),
|
|
'rhel71': ('registry.access.redhat.com/ubi7/ubi:7.9', UBI7_PYTHON_INSTALL),
|
|
'rhel72': ('registry.access.redhat.com/ubi7/ubi:7.9', UBI7_PYTHON_INSTALL),
|
|
'rhel80': ('redhat/ubi8', YUM_PYTHON_INSTALL),
|
|
'rhel81': ('redhat/ubi8', YUM_PYTHON_INSTALL),
|
|
'rhel82': ('redhat/ubi8', YUM_PYTHON_INSTALL),
|
|
'rhel83': ('redhat/ubi8', YUM_PYTHON_INSTALL),
|
|
'sunos5': None,
|
|
'suse11': None,
|
|
'suse12': None,
|
|
# ('registry.suse.com/suse/sles12sp5:latest', ZYPPER_PYTHON_INSTALL),
|
|
# 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_PYTHON_INSTALL),
|
|
# 'suse15': ('registry.suse.com/suse/sle15:latest', ZYPPER_PYTHON_INSTALL),
|
|
# Has the same error as above
|
|
'ubuntu1204': None,
|
|
'ubuntu1404': None,
|
|
'ubuntu1604': ('ubuntu:16.04', APT_PYTHON_INSTALL),
|
|
'ubuntu1804': ('ubuntu:18.04', APT_PYTHON_INSTALL),
|
|
'ubuntu2004': ('ubuntu:20.04', APT_PYTHON_INSTALL),
|
|
'ubuntu2204': ('ubuntu:22.04', APT_PYTHON_INSTALL),
|
|
'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 = [("amazonlinux:2", "4.4.16"), ("amazonlinux:2", "4.4.17-rc2"),
|
|
("amazonlinux:2", "4.2.23-rc0"), ("amazonlinux:2", "4.2.23-rc1"),
|
|
("amazonlinux:2", "4.2.22")]
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class Test:
|
|
"""Class to track a single test."""
|
|
|
|
container: str
|
|
start_command: str
|
|
version: str
|
|
packages_urls: List[str] = dataclasses.field(default_factory=list)
|
|
packages_paths: List[Path] = dataclasses.field(default_factory=list)
|
|
|
|
def __repr__(self) -> str:
|
|
ret = f"\nTest:\n\tcontainer: {self.container}\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.container + "-" + self.version
|
|
|
|
|
|
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.container.replace(':','-').replace('/', '_')}_{test.version}_{uuid.uuid4().hex}.log"
|
|
test_docker_root = Path("/mnt/package_test")
|
|
log_docker_path = Path.joinpath(test_docker_root, log_name)
|
|
test_external_root = Path(__file__).parent
|
|
log_external_path = Path.joinpath(test_external_root, log_name)
|
|
|
|
os.makedirs(log_external_path.parent, exist_ok=True)
|
|
command = f"bash -c \"{test.start_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:
|
|
container: docker.Container = client.containers.run(
|
|
test.container, 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: # pylint: disable=bare-except
|
|
traceback.print_exception() # pylint: disable=no-value-for-parameter
|
|
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='Package tester')
|
|
|
|
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=
|
|
"Comma seperated tuple of (OS to run test on, Path to packages to use to do the install test). For example ubuntu2004,https://s3.amazonaws.com/mciuploads/${project}/${build_variant}/${revision}/artifacts/${build_id}-packages.tgz.",
|
|
action='append', nargs='+', default=[])
|
|
args = parser.parse_args()
|
|
|
|
mongo_os: str = args.os
|
|
extra_tests: List[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]
|
|
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)
|
|
start_command = OS_DOCKER_LOOKUP[test_os][1]
|
|
|
|
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.warn("Could not find tools package for %s and %s", arch, test_os)
|
|
|
|
if mongosh_package:
|
|
urls.append(mongosh_package)
|
|
else:
|
|
logging.warn("Could not find mongosh package for %s and %s", arch, test_os)
|
|
|
|
tests.append(
|
|
Test(container=OS_DOCKER_LOOKUP[test_os][0], start_command=start_command, 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
|
|
|
|
container_name = OS_DOCKER_LOOKUP[dl["target"]][0]
|
|
start_command = OS_DOCKER_LOOKUP[dl["target"]][1]
|
|
|
|
if (container_name, dl["version"]) in DISABLED_TESTS:
|
|
continue
|
|
|
|
tests.append(
|
|
Test(container=container_name, start_command=start_command,
|
|
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)
|