Skip to content
Snippets Groups Projects
docker_dev_start.py 28.7 KiB
Newer Older
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Launch a docker image but bind the local directory inside the container
and define the module path automatically.
try:
    from configparser import ConfigParser
except ImportError:
    from ConfigParser import SafeConfigParser as ConfigParser
import docker  # apt python-docker (1.9) or pip install docker
from psycopg2 import connect, OperationalError  # apt python-psycopg2
if docker.__version__ > '2.5.0':
    from docker import APIClient as docker_api
else:
    from docker import Client as docker_api

# TODO auto create list of module

_logger = logging.getLogger(__name__)

Florent Aide's avatar
Florent Aide committed

def which(program):
    """Return path of program if it exists
https://stackoverflow.com/questions/377017/test-if-executable-exists-in-python/377028#377028
    """
    def is_exe(fpath):
        return os.path.isfile(fpath) and os.access(fpath, os.X_OK)

    fpath, fname = os.path.split(program)
    if fpath:
        if is_exe(program):
            return program
    else:
        for path in os.environ["PATH"].split(os.pathsep):
            exe_file = os.path.join(path, program)
            if is_exe(exe_file):
                return exe_file

    return None


def flake8(odoo_type):
    """Run flake8
    _logger.info('Running flake8')
    if odoo_type in ('odoo7', 'odoo8', 'odoo9', 'odoo10'):
        image = 'xcgd/flake8:2'
        image = 'xcgd/flake8:3'
    call([
        'docker', 'run', '--rm', '--volume',
        '{}:/mnt'.format(os.environ['PWD']), image,
        '/mnt'])
def isort(docker_client):
    """Run isort
    _logger.info('Pulling isort')
    docker_client.pull(repository='xcgd/isort', tag='odoo')
    _logger.info('Running isort')
    call([
        'docker', 'run', '--rm', '--volume',
        '{}:/mnt'.format(os.environ['PWD']), '-w', '/mnt', 'xcgd/isort:odoo',
        'isort', '-c'])
def main(argv=None):  # IGNORE:C0111
    """Parse arguments and launch conversion
    """
    program_version = __version__
    program_build_date = str(__updated__)
    program_version_message = '%%(prog)s %s (%s)' % (
        program_version, program_build_date)
    program_shortdesc = __doc__.split(".")[0]
    program_license = '''%s

  Created by Vincent Hatakeyama on %s.
  Copyright 2017, 2018, 2019 XCG Consulting. All rights reserved.

  Licensed under the MIT License

  Distributed on an "AS IS" basis without warranties
  or conditions of any kind, either express or implied.

USAGE
Florent Aide's avatar
Florent Aide committed
''' % (program_shortdesc, str(__date__))

    project_path = os.path.realpath('.')
    project_name = os.path.basename(project_path)
    if project_name == 'odoo_scripts':
        logging.fatal(
            "You must run this script from the super project"
            " (./odoo_scripts/docker_dev_start.py)")
Florent Aide's avatar
Florent Aide committed
    # TODO add a way to store configuration options in a project file
    # Argument parsing
    parser = argparse.ArgumentParser(
        description=program_license,
        formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument(
        '-V', '--version', action='version',
        version=program_version_message)
    parser.add_argument(
        '-v', '--verbose', dest='verbose', action='count',
        help="set verbosity level [default: %(default)s]")
    parser.add_argument(
        '--db_user',
        help="Database user [default: %(default)s]",
    )
    parser.add_argument(
        '--db_password',
        help="Database user password [default: %(default)s]",
        '--max-cron-threads',
        help="max cron threads [default: %(default)s]",
        default=None,
    )
    parser.add_argument(
        '-d',
        '--database',
        help="Database [default: %(default)s]",
        default=None,
    )
    parser.add_argument(
        '-p',
        '--dbport',
        help="Change database port",
        default=None,
    group = parser.add_mutually_exclusive_group()
    group.add_argument(
        '-u',
        '--update',
        help="Module to update (will also set --i18n-overwrite)"
             " [default: %(default)s]\n"
             "Options --update, --test and --test-default"
             " cannot be used at the same time",
    group.add_argument(
        '-t',
        '--test',
        help="Modules to test (will also set --log-level=test)"
             " [default: %(default)s]\n"
             "Options --update, --test and --test-default"
             " cannot be used at the same time",
    )
    group.add_argument(
        '--test-default',
        help="Test all modules in module_list_test"
             " [default: %(default)s]\n"
             "Options --update, --test and --test-default"
             " cannot be used at the same time",
        action='store_true',
    )
    parser.add_argument(
        '-i',
        '--install',
        help="Module to install [default: %(default)s]",
        default=None,
    )
    parser.add_argument(
        '--without-demo',
        help="Module to avoid including demo data [default: %(default)s]",
        default=None,
    )
        '--create-user',
Florent Aide's avatar
Florent Aide committed
        help="Create user in database (using $user) [default: %(default)s]",
Florent Aide's avatar
Florent Aide committed
        help=("Append the module_list from setup.cfg to the modules to install"
              " [default: %(default)s]"),
    parser.add_argument(
        '--start-postgresql',
        help="Start a Postgresql docker",
        action='store_true',
    )
    parser.add_argument(
        '--no-flake8',
        help="Do not run flake8",
        action='store_false',
        dest='flake8',
        '--no-isort',
        help="Do not run isort",
        action='store_false',
        dest='isort',
    )
    parser.add_argument(
        '--odoo-help',
        help="Pass --help to odoo binary [default: %(default)s]",
        action='store_true',
    )
    parser.add_argument(
        '--stop-after-init',
        help="Pass --stop-after-init to odoo binary [default: %(default)s]",
        action='store_true',
    )
    build_group = parser.add_mutually_exclusive_group()
    build_group.add_argument(
        '--no-build',
        help="Do not build locally [default: %(default)s]",
        action='store_true',
    )
    build_group.add_argument(
        '--force-build',
        help="Force new local build [default: %(default)s]",
    network_group = parser.add_mutually_exclusive_group()
    network_group.add_argument(
        '--host-network',
        help="Use host network [default: %(default)s]",
        action='store_true',
        default=True,
Florent Aide's avatar
Florent Aide committed
        help=("Use docker network (works better with"
              " python-netifaces installed) [default: %(default)s]"),
        action='store_false',
        dest='host_network',
        default=False,
    parser.add_argument(
        '--no-dev',
        help="No dev options",
        action='store_false',
        dest='dev',
    )
    parser.add_argument(
        '--load-language',
        help="specifies the languages for the translations you want to be"
             " loaded",
        default=None,
        dest='LOAD_LANGUAGE',
    )
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
    # TODO there are more
    odoo_log_levels = ['info', 'warn', 'debug', 'debug_sql']
    parser.add_argument(
        '--log-level',
        help="Override odoo log level (for tests and install)",
        default=None,
        choices=odoo_log_levels,
    )
    parser.add_argument(
        '--log-handler',
        help="log handler [default: %(default)s]",
        default=None,
    )
    parser.add_argument(
        '--restore',
        help="Database to restore [default: %(default)s]",
        default=None,
        dest='DUMP',
    )
    parser.add_argument(
        '--config',
        help="Configuration file to use, if any [default: %(default)s]",
        default=os.path.join('conf', 'dev', 'odoo.conf'),
    )
    py3o_group = parser.add_mutually_exclusive_group()
    py3o_group.add_argument(
        '--start-py3o',
        help="start py3o docker [default: %(default)s]",
        action='store_const',
        default=None,
        const=1,
        dest='start_py3o',
    )
    py3o_group.add_argument(
        '--no-start-py3o',
        help="do not start py3o docker [default: %(default)s]",
        action='store_const',
        default=None,
        const=-1,
        dest='start_py3o',
    )

    # TODO detect that user is member of docker group

    # TODO add a way to add options to docker
    # TODO add a way to add arg to odoo

    verbose = nmspc.verbose
    if not verbose:
        logging.basicConfig(level=logging.WARN)
    elif verbose == 1:
        logging.basicConfig(level=logging.INFO)
        logging.basicConfig(level=logging.DEBUG)
    db_user = nmspc.db_user
    db_password = nmspc.db_password
    _logger.debug('Use host network: %s', str(use_host_network))
    run_flake8 = nmspc.flake8
    restore_filename = nmspc.DUMP
    database = nmspc.database

    if restore_filename:
        if not database:
            _logger.fatal('No database name given for restore')
    if not os.path.exists(setup_path):
    modules = []
    if c.has_option('odoo_scripts', 'modules'):
        for entry in c.get('odoo_scripts', 'modules').split():
            modules.extend(glob.glob(entry))
    _logger.debug("addon modules: %s", ','.join(modules))
    if not db_user and c.has_option('odoo_scripts', 'db_user'):
        db_user = c.get('odoo_scripts', 'db_user')
    if not db_password and c.has_option('odoo_scripts', 'db_password'):
        db_password = c.get('odoo_scripts', 'db_password')

    registry = (
        c.has_section('odoo_scripts') and
        c.has_option('odoo_scripts', 'registry') and
        c.get('odoo_scripts', 'registry')) or 'registry.xcg.io'
    project = (
        c.has_section('odoo_scripts') and
        c.has_option('odoo_scripts', 'image') and
        c.get('odoo_scripts', 'image')) or os.path.basename(project_path)
    odoo_type = (
        c.has_section('odoo_scripts') and
        c.has_option('odoo_scripts', 'odoo_type') and
        c.get('odoo_scripts', 'odoo_type')) or 'odoo7'
    image = "%s/%s:latest" % (registry, project)
    postgresql_version = (
        c.has_section('odoo_scripts') and
        c.has_option('odoo_scripts', 'postgresql_version') and
        c.get('odoo_scripts', 'postgresql_version')) or '9.6'
    module_list = (
        c.has_section('odoo_scripts') and
        c.has_option('odoo_scripts', 'module_list') and
        c.get('odoo_scripts', 'module_list').split()) or []
    if nmspc.start_py3o:
        _logger.debug("Command line start py3o %d", nmspc.start_py3o)
        start_py3o_stack = nmspc.start_py3o == 1
    else:
        start_py3o_stack = (
            c.has_section('odoo_scripts') and
            c.has_option('odoo_scripts', 'start_py3o') and
            c.get('odoo_scripts', 'start_py3o') in ('yes', 'true'))
        _logger.debug("No command line start py3o %d", start_py3o_stack)

    # detect if docker image already exists
    docker_client = docker_api(base_url='unix://var/run/docker.sock')
    image_list = docker_client.images(name=image, quiet=True)
    if not image_list:
        _logger.info("Image %s does not exist", image)
    if (not image_list and not nmspc.no_build) or nmspc.force_build:
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
        import docker_build
        arguments = [
            '--dev'
        ]
        if verbose == 1:
            arguments.append('-v')
        elif verbose and verbose > 1:
            arguments.append('-vv')
        failed = docker_build.main(arguments)
        if failed:
            return failed
    # options is only used with subprocess call
    options = [
        '--name',
        project_name,
        '--rm',
        '--tty',
        '--interactive',
    ]
    if not use_host_network:
        options.append('--publish')
        options.append('8069:8069')
        arg.append('--db_password %s' % db_password)
    if nmspc.update:
        arg.append('-u %s' % nmspc.update)
        arg.append('--i18n-overwrite')
    if nmspc.test:
        arg.append('-u %s' % nmspc.test)
        arg.append('--test-enable')
        if odoo_type == 'odoo7':
            arg.append('--log-level=test')
    if nmspc.test or nmspc.stop_after_init:
        arg.append('--stop-after-init')
    if nmspc.test_default:
        test_modules = c.get('odoo_scripts', 'module_list_tests').split() or []
        str_modules = ','.join(test_modules)
        arg.append('-u %s' % str_modules)
        arg.append('--test-enable')
        if odoo_type == 'odoo7':
            arg.append('--log-level=test')
        arg.append('--stop-after-init')
    if database:
        arg.append('-d %s' % database)
    if nmspc.install or nmspc.install_default:
        modules_to_install = []
        if nmspc.install:
            modules_to_install.extend(nmspc.install.split(','))
            modules_to_install.extend(module_list)
        if modules_to_install:
            arg.append('-i')
            arg.append(','.join(modules_to_install))
        arg.append('--without-demo')
        arg.append(nmspc.without_demo)
    if nmspc.max_cron_threads:
        arg.append('--max-cron-threads=%s' % nmspc.max_cron_threads)
    if nmspc.log_level:
        arg.append('--log-level')
        arg.append(nmspc.log_level)
    if nmspc.log_handler:
        arg.append('--log-handler')
        arg.append(nmspc.log_handler)
    if nmspc.LOAD_LANGUAGE:
        arg.append('--load-language')
        arg.append(nmspc.LOAD_LANGUAGE)
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
            from netifaces import ifaddresses, AF_INET  # apt python-netifaces
Florent Aide's avatar
Florent Aide committed
            iface_name = 'docker0'
            addresses = [
                i['addr']
                for i in ifaddresses(
                    iface_name
                ).setdefault(AF_INET, [{'addr': 'No IP addr'}])
            ]
            if addresses:
                local_ip = addresses[0]
        except ImportError:
            _logger.warn("Consider installing python netifaces"
Florent Aide's avatar
Florent Aide committed
                         " to ease local IP detection")
            _logger.info('Contacting Google Public DNS to find our IP')
Florent Aide's avatar
Florent Aide committed
            local_ip = [
                (
                    s.connect(('8.8.8.8', 53)),
                    s.getsockname()[0],
                    s.close()
                ) for s in [
                    socket.socket(socket.AF_INET, socket.SOCK_DGRAM)]
            ][0][1]

Florent Aide's avatar
Florent Aide committed

    arg.append('--db_host')
    arg.append(local_ip)
    if nmspc.dbport:
        arg.append("--db_port")
oury.balde's avatar
oury.balde committed
        arg.append('%s' % nmspc.dbport)
    local_conf_path = nmspc.config
    local_conf_dir = os.path.dirname(os.path.realpath(local_conf_path))
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
    user = db_user
        _logger.info('Local configuration file found: %s', local_conf_path)
        odoo_conf_path = (
            '/etc/odoo' if odoo_type in ('odoo11', 'odoo12', )
            else '/opt/odoo/etc'
        )
        binds.append('{}:{}'.format(os.path.join(
            project_path, local_conf_dir), odoo_conf_path))
            user = cp_local.get('options', 'db_user')
Florent Aide's avatar
Florent Aide committed
            password = cp_local.get(
                'options',
                'db_password',
            )
        # doing this avoid having to change the value of redis_host
        # used by smile_redis_session_store
        redis_host = (
             cp_local.has_section('options') and
             cp_local.has_option('options', 'redis_host') and
             cp_local.get('options', 'redis_host')) or None
        if redis_host:
            options.append('--add-host')
            options.append('{}:{}'.format(redis_host, local_ip))
    else:
        _logger.info('No configuration file at: %s', local_conf_path)
    # default values if nothing else
    if not user:
        user = 'odoo'
    if not password:
        password = 'odoo'
    if odoo_type == 'odoo7':
        _logger.debug('Only sessions volume for this odoo version')
        # this path is in odoo code, the last part is the system user
        data_volume_path = '/tmp/oe-sessions-{}'.format('odoo')
    else:
        data_volume_path = '/mnt/data'
        arg.append('--data-dir {}'.format(data_volume_path))
    data_volume_name = '{}_data'.format(project_name)
    _logger.debug('Using data volume %s', data_volume_name)
    createVolume(docker_client, data_volume_name)
    # make sure the permission in the volume are correct
    mount_opts = '{}:/mnt'.format(data_volume_name)
    # TODO replace by something cleaner if possible
    call([
        'docker', 'run', '--rm', '-v', mount_opts, '--entrypoint',
        '/bin/chown', image, 'odoo', '/mnt',
    ])
Florent Aide's avatar
Florent Aide committed

    binds.append('{}:{}'.format(data_volume_name, data_volume_path))
    # avoid the duplication of unbind volumes with all addons
    if odoo_type == 'odoo7':
        extra_volumes = [
            '/opt/odoo/additional_addons', '/opt/odoo/var',
            # not used but declared as a mount point in Dockerfile :-(
            '/opt/odoo/data',
        ]
    elif odoo_type == 'odoo8':
        extra_volumes = [
            '/opt/odoo/additional_addons', '/opt/odoo/var', '/opt/odoo/data',
        ]
    elif odoo_type == 'odoo10':
        extra_volumes = [
            '/opt/odoo/additional_addons', '/opt/odoo/var', '/opt/odoo/data',
        ]
    else:
        extra_volumes = [
            '/var/lib/odoo',
        ]
    for extra_volume in extra_volumes:
        volume_name = '{}_{}'.format(
            project_name, extra_volume.replace('/', '_'))
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
        # volume = createVolume(docker_client, volume_name)
        binds.append('{}:{}'.format(volume_name, extra_volume))
    if start_py3o_stack:
        start_py3o(docker_client)
        options.append('--add-host')
        options.append('py3o-fusion-server:{}'.format(local_ip))

        host_pg_port = nmspc.dbport if nmspc.dbport else 5433
        name, stop_postgresql = docker_run_postgresql(
            docker_client, project_name, postgresql_version, host_pg_port)
Florent Aide's avatar
Florent Aide committed

Florent Aide's avatar
Florent Aide committed
        # Check that connection can be done, try to create user if asked to
        # TODO: handle the case where the database is still starting up
        # TODO: (and remove the sleep)
        try:
            if local_ip or start_postgresql:
                port = host_pg_port if start_postgresql else None
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
                connect(
Florent Aide's avatar
Florent Aide committed
                    user=user,
                    password=password,
                    database='postgres',
                    host=local_ip,
                    port=port,
                )
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
                connect(
Florent Aide's avatar
Florent Aide committed
                    user=user,
                    password=password,
                    database='postgres',
                )

        except OperationalError as exception:
            if nmspc.create_user:
                _logger.debug('Cannot connect to database with user %s', user)
                _logger.debug(exception)
                _logger.info('Creating user %s', user)
Florent Aide's avatar
Florent Aide committed
                if start_postgresql:
                    connection = connect(
Florent Aide's avatar
Florent Aide committed
                        database='postgres',
                        host=local_ip,
                        port=host_pg_port,
                    )
                else:
                    loginname = pwd.getpwuid(os.getuid())[0]
                    connection = connect(user=loginname, database='postgres')
                with connection.cursor() as cursor:
                    # not injection safe but you are on your own machine
                    # with already full access to db
                    cursor.execute(
                        'CREATE ROLE %s LOGIN CREATEDB PASSWORD %%s' % user,
                        (password,))
                connection.commit()
                connection.close()
            else:
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
                    'Cannot connect to database with user %s', user)
                    "You can add the --create-user argument to create it")
                return 16
        # restore
        if restore_filename:
            restore_basename = os.path.basename(restore_filename)
            _logger.info("Copying dump file in docker")
            call([
                'docker', 'cp', restore_filename,
                '{}:/tmp/{}'.format(name, restore_basename)])
            call([
                'docker', 'exec', name, 'createdb', '-U', user, database])
            restore = call([
                'docker', 'exec', name, 'pg_restore', '-U', user, '-O',
                '-d', database, '/tmp/{}'.format(restore_basename)])
            if not restore:
                return 15
            _logger.info("Removing dump file in docker")
            call(['docker', 'exec', name, '/tmp/{}'.format(restore_basename)])

    if not start_postgresql and not odoo_help and restore_filename:
        _logger.info("Creating database %s", database)
        createdb = call([
            'createdb', '-U', user, database])
        if not createdb:
            return 17
        _logger.info("Restoring database %s", database)
        restore = call([
            'pg_restore', '-U', user, '-O',
            '-d', database, restore_filename])
        if not restore:
            return 15
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
        binds.append(
                os.path.realpath(os.path.join(
                    project_path, module)),
                os.path.basename(module))
        )
    all_addons_dir = ['/mnt/addons']
    if odoo_type in ('odoo7', 'odoo8'):
        all_addons_dir.append('/opt/odoo/sources/odoo/addons')
    if all_addons_dir:
        arg.append('--addons-path')
        arg.append(','.join(all_addons_dir))
        flake8(odoo_type)
    if dev:
        if odoo_type in ('odoo11', 'odoo10'):
            # ipdb should not be run if not interactive
            arg.append('--dev=reload,ipdb')
        if odoo_type in ('odoo8',):
            arg.append('--auto-reload')
    # use call to allow usage of pdb
    for bind in binds:
        options.append('--volume')
        options.append(bind)
    cmd = ['docker', 'run']
    cmd.extend(options)
    cmd.append(image)
    cmd.extend(arg)
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed

def createVolume(docker_client, data_volume_name):
    """Return the volume passed in parameter, creating it if it does not exists
    """
    volumes = docker_client.volumes(filters={'name': data_volume_name})
    if volumes['Volumes']:
        _logger.debug('Volume %s already exist', data_volume_name)
        _logger.debug('Creating volume %s', data_volume_name)
        return docker_client.create_volume(name=data_volume_name)
def isRunning(docker_client, container_id):
    """Return true if the container is still running
    """
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
    return len(docker_client.containers(
        filters={'id': container_id}, quiet=True))

def docker_run_postgresql(
    docker_client, project_name, postgresql_version, host_pg_port=None,
    stop_at_exit=True,
):
    """

    :param docker_client:
    :param project_name:
    :param postgresql_version:
    :param host_pg_port: if None, put the socket in /tmp, else publish the port
    :param stop_at_exit:
    :return:
    """
    pg_repository = 'postgres'
    version = '{}-alpine'.format(postgresql_version)
    pg_image = '{}:{}'.format(pg_repository, version)
    name = 'postgresql-{}-{}'.format(postgresql_version, project_name)
    docker_client.pull(repository=pg_repository, tag=version)
    pg_data_volume_name = 'postgresql_{}-{}'.format(
        postgresql_version, project_name)

    # volume = createVolume(docker_client, pg_data_volume_name)
    binds = [
        '{}:/var/lib/postgresql/data'.format(pg_data_volume_name),
    ]
    port_bindings = {}
    if host_pg_port:
        port_bindings[5432] = host_pg_port
    else:
        binds.append('/tmp:/var/run/postgresql')
    host_config = docker_client.create_host_config(
        binds=binds,
        port_bindings=port_bindings,
    )
    if any(
        '/{}'.format(name) in container['Names']
        for container in docker_client.containers()
    ):
        _logger.debug('Postgresql Container already running')
        return name, False
    _logger.debug('Creating postgresql container')
    pg = docker_client.create_container(
        image=pg_image,
        host_config=host_config,
        name=name,
    )

    _logger.debug('Starting postgresql container')
    docker_client.start(pg.get('Id'))

    def stop_postgresql():
        # TODO test if still exists
        try:
            _logger.info('Stopping postgresql')
            docker_client.stop(pg.get('Id'))
            _logger.info('Removing container postgresql')
            docker_client.remove_container(pg.get('Id'))
        except docker.errors.NotFound:
            _logger.info('Postgresql already stopped')

    if stop_at_exit:
        atexit.register(stop_postgresql)
    _logger.debug("Waiting for postgres to start")
    # give pg the time to start up
    import time
    time.sleep(5)
    return name, stop_postgresql


def find_container(docker_client, name):
    """Return container object from its name
    """
    for container in docker_client.containers():
        if '/{}'.format(name) in container['Names']:
            return container


def remove_and_stop(docker_client, name):
    # TODO handle the case when the container has been created, not running
    tostop = find_container(docker_client, name)
    docker_client.stop(tostop.get('Id'))
    docker_client.remove_container(tostop.get('Id'))


def start_py3o(
    docker_client, host_fusion_port=8765, stop_at_exit=True,
):
    fusion_repository = 'xcgd/py3o'
    fusion_version = '1.0.0'
    fusion_name = 'py3o_fusion'
    fusion_image = '{}:{}'.format(fusion_repository, fusion_version)
    try:
        docker_client.pull(repository=fusion_repository, tag=fusion_version)
    except Exception as e:
        _logger.warning('Exception when trying to pull: %s', e)
    if any(
            '/{}'.format(fusion_name) in container['Names']
            for container in docker_client.containers()
    ):
        _logger.debug('%s Container already running', fusion_name)
        remove_and_stop(docker_client, fusion_name)
    # TODO handle --host-network option
    port_bindings = {8765: host_fusion_port}
    host_config = docker_client.create_host_config(
        binds=[],
        port_bindings=port_bindings,
    )
    _logger.debug('Starting %s container', fusion_name)
    fusion = docker_client.create_container(
        image=fusion_image,
        host_config=host_config,
        name=fusion_name,
    )
    _logger.debug('Starting %s container', fusion_name)
    docker_client.start(container=fusion.get('Id'))

    def stop_py3o():
        # TODO test if still exists
        _logger.info('Stopping fusion')
        docker_client.stop(fusion.get('Id'))
        _logger.info('Removing containers')
        docker_client.remove_container(fusion.get('Id'))

    if stop_at_exit:
        atexit.register(stop_py3o)
    return fusion_name, stop_py3o