mirror of
https://github.com/odoo/runbot.git
synced 2025-03-19 09:25:46 +07:00

When a build is running, the stmp is the localhost. Since Docker builds, the localhost is the container which does not catch port 25 smtp. Mails are lost in the limbo. With this commit, the default gateway of the Docker network is used as smtp host for the builds. It's the responsability of the runbot host to catch smtp traffic from the container. This bridge interface exists by default on a system where Docker is running. However, Docker is affected by this issue: https://github.com/moby/moby/issues/26799 The first time the Docker daemon is installed, the Gateway is not defined on the bridge interface. When the Docker daemon is restarted, the gateway is correctly defined. Pay attention that restarting the Docker daemon will kill all the running/testing builds.
196 lines
8.9 KiB
Python
196 lines
8.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Containerize builds
|
|
|
|
The docker image used for the build is always tagged like this:
|
|
odoo:runbot_tests
|
|
This file contains helpers to containerize builds with Docker.
|
|
When testing this file:
|
|
the first parameter should be a directory containing Odoo.
|
|
The second parameter is the exposed port
|
|
"""
|
|
import argparse
|
|
import datetime
|
|
import json
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import time
|
|
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
DOCKERUSER = """
|
|
RUN groupadd -g %(group_id)s odoo \\
|
|
&& useradd -u %(user_id)s -g odoo -G audio,video odoo \\
|
|
&& mkdir /home/odoo \\
|
|
&& chown -R odoo:odoo /home/odoo \\
|
|
&& echo "odoo ALL= NOPASSWD: /usr/bin/pip" > /etc/sudoers.d/pip \\
|
|
&& echo "odoo ALL= NOPASSWD: /usr/bin/pip3" >> /etc/sudoers.d/pip
|
|
USER odoo
|
|
ENV COVERAGE_FILE /data/build/.coverage
|
|
""" % {'group_id': os.getgid(), 'user_id': os.getuid()}
|
|
|
|
def docker_build(log_path, build_dir):
|
|
"""Build the docker image
|
|
:param log_path: path to the logfile that will contain odoo stdout and stderr
|
|
:param build_dir: the build directory that contains the Odoo sources to build.
|
|
"""
|
|
# Prepare docker image
|
|
docker_dir = os.path.join(build_dir, 'docker')
|
|
os.makedirs(docker_dir, exist_ok=True)
|
|
shutil.copy(os.path.join(os.path.dirname(__file__), 'data', 'Dockerfile'), docker_dir)
|
|
# synchronise the current user with the odoo user inside the Dockerfile
|
|
with open(os.path.join(docker_dir, 'Dockerfile'), 'a') as df:
|
|
df.write(DOCKERUSER)
|
|
logs = open(log_path, 'w')
|
|
dbuild = subprocess.Popen(['docker', 'build', '--tag', 'odoo:runbot_tests', '.'], stdout=logs, stderr=logs, cwd=docker_dir)
|
|
dbuild.wait()
|
|
|
|
def docker_run(odoo_cmd, log_path, build_dir, container_name, exposed_ports=None, cpu_limit=None, preexec_fn=None):
|
|
"""Run tests in a docker container
|
|
:param odoo_cmd: command that starts odoo
|
|
:param log_path: path to the logfile that will contain odoo stdout and stderr
|
|
:param build_dir: the build directory that contains the Odoo sources to build.
|
|
This directory is shared as a volume with the container
|
|
:param container_name: used to give a name to the container for later reference
|
|
:param exposed_ports: if not None, starting at 8069, ports will be exposed as exposed_ports numbers
|
|
"""
|
|
# build cmd
|
|
cmd_chain = []
|
|
cmd_chain.append('cd /data/build')
|
|
cmd_chain.append('head -1 odoo-bin | grep -q python3 && sudo pip3 install -r requirements.txt || sudo pip install -r requirements.txt')
|
|
cmd_chain.append(' '.join(odoo_cmd))
|
|
run_cmd = ' && '.join(cmd_chain)
|
|
_logger.debug('Docker run command: %s', run_cmd)
|
|
logs = open(log_path, 'w')
|
|
|
|
# create start script
|
|
docker_command = [
|
|
'docker', 'run', '--rm',
|
|
'--name', container_name,
|
|
'--volume=/var/run/postgresql:/var/run/postgresql',
|
|
'--volume=%s:/data/build' % build_dir,
|
|
'--shm-size=128m',
|
|
'--init',
|
|
]
|
|
serverrc_path = os.path.expanduser('~/.openerp_serverrc')
|
|
odoorc_path = os.path.expanduser('~/.odoorc')
|
|
final_rc = odoorc_path if os.path.exists(odoorc_path) else serverrc_path if os.path.exists(serverrc_path) else None
|
|
if final_rc:
|
|
docker_command.extend(['--volume=%s:/home/odoo/.odoorc:ro' % final_rc])
|
|
if exposed_ports:
|
|
for dp,hp in enumerate(exposed_ports, start=8069):
|
|
docker_command.extend(['-p', '127.0.0.1:%s:%s' % (hp, dp)])
|
|
if cpu_limit:
|
|
docker_command.extend(['--ulimit', 'cpu=%s' % int(cpu_limit)])
|
|
docker_command.extend(['odoo:runbot_tests', '/bin/bash', '-c', "%s" % run_cmd])
|
|
docker_run = subprocess.Popen(docker_command, stdout=logs, stderr=logs, preexec_fn=preexec_fn, close_fds=False, cwd=build_dir)
|
|
_logger.info('Started Docker container %s', container_name)
|
|
return docker_run.pid
|
|
|
|
def docker_stop(container_name):
|
|
"""Stops the container named container_name"""
|
|
_logger.info('Stopping container %s', container_name)
|
|
dstop = subprocess.run(['docker', 'stop', container_name])
|
|
|
|
def docker_is_running(container_name):
|
|
"""Return True if container is still running"""
|
|
dinspect = subprocess.run(['docker', 'container', 'inspect', container_name], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
|
|
return True if dinspect.returncode == 0 else False
|
|
|
|
def docker_get_gateway_ip():
|
|
"""Return the host ip of the docker default bridge gateway"""
|
|
docker_net_inspect = subprocess.run(['docker', 'network', 'inspect', 'bridge'], stdout=subprocess.PIPE)
|
|
if docker_net_inspect.returncode != 0:
|
|
return None
|
|
if docker_net_inspect.stdout:
|
|
try:
|
|
return json.loads(docker_net_inspect.stdout)[0]['IPAM']['Config'][0]['Gateway']
|
|
except KeyError:
|
|
return None
|
|
|
|
def build(args):
|
|
"""Build container from CLI"""
|
|
_logger.info('Building the base image container')
|
|
logdir = os.path.join(args.build_dir, 'logs')
|
|
os.makedirs(logdir, exist_ok=True)
|
|
logfile = os.path.join(logdir, 'logs-build.txt')
|
|
docker_build(logfile, args.build_dir)
|
|
_logger.info('Finished building the base image container')
|
|
|
|
def tests(args):
|
|
_logger.info('Start container tests')
|
|
os.makedirs(os.path.join(args.build_dir, 'logs'), exist_ok=True)
|
|
os.makedirs(os.path.join(args.build_dir, 'datadir'), exist_ok=True)
|
|
|
|
if args.kill:
|
|
# Test stopping a non running container
|
|
_logger.info('Test killing an non existing container')
|
|
docker_stop('xy' * 5)
|
|
# Test building
|
|
_logger.info('Test building the base image container')
|
|
logfile = os.path.join(args.build_dir, 'logs', 'logs-build.txt')
|
|
docker_build(logfile, args.build_dir)
|
|
|
|
# Test testing
|
|
odoo_cmd = ['/data/build/odoo-bin', '-d %s' % args.db_name, '--addons-path=/data/build/addons', '--data-dir', '/data/build/datadir', '-r %s' % os.getlogin(), '-i', args.odoo_modules, '--test-enable', '--stop-after-init', '--max-cron-threads=0']
|
|
|
|
if args.kill:
|
|
logfile = os.path.join(args.build_dir, 'logs', 'logs-partial.txt')
|
|
container_name = 'odoo-container-test-%s' % datetime.datetime.now().microsecond
|
|
docker_run(odoo_cmd, logfile, args.build_dir, container_name)
|
|
# Test stopping the container
|
|
_logger.info('Waiting 30 sec before killing the build')
|
|
time.sleep(30)
|
|
docker_stop(container_name)
|
|
time.sleep(3)
|
|
|
|
# Test full testing
|
|
logfile = os.path.join(args.build_dir, 'logs', 'logs-full-test.txt')
|
|
container_name = 'odoo-container-test-%s' % datetime.datetime.now().microsecond
|
|
if args.coverage:
|
|
omit = ['--omit', '*__manifest__.py']
|
|
with open(os.path.join(args.build_dir, 'odoo-bin'), 'r') as exfile:
|
|
pyversion = 'python3' if 'python3' in exfile.readline() else 'python'
|
|
odoo_cmd = [ pyversion, '-m', 'coverage', 'run', '--branch', '--source', '/data/build'] + omit + odoo_cmd
|
|
docker_run(odoo_cmd, logfile, args.build_dir, container_name)
|
|
time.sleep(1) # give time for the container to start
|
|
|
|
while docker_is_running(container_name):
|
|
time.sleep(10)
|
|
_logger.info("Waiting for %s to stop", container_name)
|
|
|
|
if args.run:
|
|
# Test running
|
|
logfile = os.path.join(args.build_dir, 'logs', 'logs-running.txt')
|
|
odoo_cmd = [
|
|
'/data/build/odoo-bin', '-d %s' % args.db_name,
|
|
'--db-filter', '%s.*$' % args.db_name, '--addons-path=/data/build/addons',
|
|
'-r %s' % os.getlogin(), '-i', 'web', '--max-cron-threads=1',
|
|
'--data-dir', '/data/build/datadir', '--workers', '2',
|
|
'--longpolling-port', '8070']
|
|
smtp_host = docker_get_gateway_ip()
|
|
if smtp_host:
|
|
odoo_cmd.extend(['--smtp', smtp_host])
|
|
container_name = 'odoo-container-test-%s' % datetime.datetime.now().microsecond
|
|
docker_run(odoo_cmd, logfile, args.build_dir, container_name, exposed_ports=[args.odoo_port, args.odoo_port + 1], cpu_limit=300)
|
|
|
|
if __name__ == '__main__':
|
|
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(name)s: %(message)s')
|
|
parser = argparse.ArgumentParser()
|
|
subparser = parser.add_subparsers(help='commands')
|
|
p_build = subparser.add_parser('build', help='Build docker image')
|
|
p_build.add_argument('build_dir')
|
|
p_build.set_defaults(func=build)
|
|
p_test = subparser.add_parser('tests', help='Test docker functions')
|
|
p_test.set_defaults(func=tests)
|
|
p_test.add_argument('build_dir')
|
|
p_test.add_argument('odoo_port', type=int)
|
|
p_test.add_argument('db_name')
|
|
p_test.add_argument('--coverage', action='store_true', help= 'test a build with coverage')
|
|
p_test.add_argument('-i', dest='odoo_modules', default='web', help='Comma separated list of modules')
|
|
p_test.add_argument('--kill', action='store_true', default=False, help='Also test container kill')
|
|
p_test.add_argument('--run', action='store_true', default=False, help='Also test running (Warning: the container survives exit)')
|
|
args = parser.parse_args()
|
|
args.func(args)
|