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.
"""
import argparse
import logging

Vincent Hatakeyama
committed
import pwd

Vincent Hatakeyama
committed
import signal
from subprocess import call
import sys
import ConfigParser
import docker # apt python-docker (1.9) or pip install docker

Vincent Hatakeyama
committed
from psycopg2 import connect, OperationalError # apt python-psycopg2
from requests.exceptions import ConnectionError
if docker.__version__ > '3.0.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__)

Vincent Hatakeyama
committed
__version__ = '1.0.0'
__date__ = '2017-08-11'

Vincent Hatakeyama
committed
__updated__ = '2018-04-06'
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 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)

Vincent Hatakeyama
committed
program_shortdesc = __doc__.split(".")[0]
program_license = '''%s
Created by Vincent Hatakeyama on %s.
Copyright 2017 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

Vincent Hatakeyama
committed
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)")
return

Vincent Hatakeyama
committed
setup_path = 'setup.cfg'
# 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]",
default=None,
)
parser.add_argument(
'--db_password',
help="Database user password [default: %(default)s]",
default=None,
)
parser.add_argument(
'-d',
'--database',
help="Database [default: %(default)s]",
default=None,
)
parser.add_argument(
'-p',
'--dbport',
help="Database port [default: %(default)s]",
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",
default=None,
)
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,
)

Vincent Hatakeyama
committed
parser.add_argument(
help="Create user in database (using $user) [default: %(default)s]",

Vincent Hatakeyama
committed
action='store_true',
)

Vincent Hatakeyama
committed
parser.add_argument(
'--install-default',
help=("Append the module_list from setup.cfg to the modules to install"
" [default: %(default)s]"),

Vincent Hatakeyama
committed
action='store_true',
)

Vincent Hatakeyama
committed
parser.add_argument(
'--start-postgresql',
help="Start a Postgresql docker",
action='store_true',
)
parser.add_argument(
'--no-flake8',

Vincent Hatakeyama
committed
help="Do not run flake8 [default: %(default)s]",
action='store_true',
)

Vincent Hatakeyama
committed
parser.add_argument(
'--odoo-help',
help="Pass --help to odoo binary [default: %(default)s]",
action='store_true',
)

Vincent Hatakeyama
committed
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]",
action='store_true',
)

Vincent Hatakeyama
committed
network_group = parser.add_mutually_exclusive_group()
network_group.add_argument(
'--host-network',
help="Use host network [default: %(default)s]",
action='store_false',
)
network_group.add_argument(
'--docker-network',
help=("Use docker network (works better with"
" python-netifaces installed) [default: %(default)s]"),

Vincent Hatakeyama
committed
action='store_true',
)
# 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

Vincent Hatakeyama
committed
nmspc = parser.parse_args(argv)
verbose = nmspc.verbose
if not verbose:
logging.basicConfig(level=logging.WARN)
logging.basicConfig(level=logging.INFO)
logging.basicConfig(level=logging.DEBUG)
db_user = nmspc.db_user
db_password = nmspc.db_password

Vincent Hatakeyama
committed
use_host_network = nmspc.host_network

Vincent Hatakeyama
committed
start_postgresql = nmspc.start_postgresql
no_flake8 = nmspc.no_flake8

Vincent Hatakeyama
committed
odoo_help = nmspc.odoo_help
c = ConfigParser.ConfigParser()
logging.debug('setup file path %s', setup_path)
# TODO test that setup file exists
c.read(setup_path)
if c.has_option('odoo_scripts', 'addon_dirs'):
addon_dirs = c.get('odoo_scripts', 'addon_dirs', '').split()
else:
addon_dirs = ''
logging.debug("addon directories: %s", addon_dirs)
registry = (
c.has_section('odoo_scripts') and
c.has_option('odoo_scripts', 'registry') and
c.get('odoo_scripts', 'registry')) or 'dockerhub.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)

Vincent Hatakeyama
committed
odoo_type = (
c.has_section('odoo_scripts') and
c.has_option('odoo_scripts', 'odoo_type') and
c.get('odoo_scripts', 'odoo_type')) or 'odoo7'

Vincent Hatakeyama
committed
image = "%s/%s:latest" % (registry, project)

Vincent Hatakeyama
committed
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'
logging.debug("Docker image: %s", image)

Vincent Hatakeyama
committed
# detect if docker image already exists
docker_client = docker_api(base_url='unix://var/run/docker.sock')

Vincent Hatakeyama
committed
image_list = docker_client.images(name=image, quiet=True)
if not image_list:

Vincent Hatakeyama
committed
logging.info("Image %s does not exist", image)

Vincent Hatakeyama
committed
else:

Vincent Hatakeyama
committed
logging.info("Image %s exists", image)
if (not image_list and not nmspc.no_build) or nmspc.force_build:
logging.info("Building image %s", image)
import docker_build
arguments = [
'--dev'
]
if verbose == 1:
arguments.append('-v')
elif verbose and verbose > 1:
arguments.append('-vv')

Vincent Hatakeyama
committed
result = docker_build.main(arguments)
if result:
exit(result)

Vincent Hatakeyama
committed
# 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')

Vincent Hatakeyama
committed
binds = []
arg = [
'start',
]
if db_user:
arg.append('--db_user %s' % db_user)
if db_password:
arg.append('--db_password %s' % db_user)
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')
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 nmspc.database:
arg.append('-d %s' % nmspc.database)

Vincent Hatakeyama
committed
if nmspc.install or nmspc.install_default:
modules_to_install = []
if nmspc.install:
modules_to_install.extend(nmspc.install.split(','))

Vincent Hatakeyama
committed
if nmspc.install_default:
modules_to_install.extend((
c.has_section('odoo_scripts') and
c.has_option('odoo_scripts', 'module_list') and
c.get('odoo_scripts', 'module_list').split()) or [])
if modules_to_install:
arg.append('-i')
arg.append(','.join(modules_to_install))
if nmspc.without_demo:
arg.append('--without-demo %s' % nmspc.database)
# auto detect local ip

Vincent Hatakeyama
committed
if use_host_network:
local_ip = '127.0.0.1'

Vincent Hatakeyama
committed
options.append('--network')
options.append('host')
else:
local_ip = None
try:
from netifaces import ifaddresses, AF_INET # apt python-netifaces
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:
logging.warn("Consider installing python netifaces"
" to ease local IP detection")

Vincent Hatakeyama
committed
import socket
logging.info('Contacting Google Public DNS to find our IP')
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]

Vincent Hatakeyama
committed
logging.debug('IP found %s', local_ip)
arg.append('--db_host')
arg.append(local_ip)
if nmspc.dbport:
arg.append("--db_port")
# auto detect local conf

Vincent Hatakeyama
committed
local_conf_dir = os.path.join('conf', 'dev')
local_conf_path = os.path.join(local_conf_dir, 'odoo.conf')

Vincent Hatakeyama
committed
password = db_password
if os.path.isfile(local_conf_path):
logging.info('Local configuration file found: %s' % local_conf_path)

Vincent Hatakeyama
committed
binds.append('%s:/opt/odoo/etc' % os.path.join(
project_path, local_conf_dir))

Vincent Hatakeyama
committed
cp_local = ConfigParser.ConfigParser()
cp_local.read(local_conf_path)
if not user:
user = cp_local.get('options', 'db_user', 'pg')
if not password:
password = cp_local.get(
'options',
'db_password',
'THIS-IS-NOT-USED-DONOT-CHANGE',
)

Vincent Hatakeyama
committed

Vincent Hatakeyama
committed
# data volume handling
if odoo_type != 'odoo7':
data_volume_name = '{}_data'.format(project_name)
logging.debug('Using data volume %s', data_volume_name)
# make sure the permission in the volume are correct
mount_opts = '{}:/mnt/data'.format(data_volume_name)
# TODO replace by something cleaner if possible
call([
'docker', 'run', '--rm', '-v',
mount_opts, 'busybox', 'chmod', '777', '/mnt/data',
])

Vincent Hatakeyama
committed
binds.append('{}:/mnt/data'.format(data_volume_name))

Vincent Hatakeyama
committed
arg.append('--data-dir /mnt/data')
else:
logging.debug('No data volume for this odoo version')

Vincent Hatakeyama
committed
# avoid the duplication of unbind volumes with all addons
# additionnal_addons only in odoo < 11
for extra_volume in ['additional_addons', 'var', 'data']:
volume_name = '{}_opt_odoo_{}'.format(project_name, extra_volume)
binds.append('{}:/opt/odoo/{}'.format(volume_name, extra_volume))

Vincent Hatakeyama
committed

Vincent Hatakeyama
committed
if start_postgresql and not odoo_help:

Vincent Hatakeyama
committed
pg_repository = 'xcgd/postgresql'
pg_image = '{}:{}'.format(pg_repository, postgresql_version)
name = 'pg{}'.format(postgresql_version)
docker_client.pull(repository=pg_repository, tag=postgresql_version)
pg_data_volume_name = 'postgresql_{}-{}'.format(
postgresql_version, project_name)

Vincent Hatakeyama
committed
host_pg_port = 5433
# volume = createVolume(docker_client, pg_data_volume_name)

Vincent Hatakeyama
committed
host_config = docker_client.create_host_config(
binds=['{}:/var/lib/postgresql'.format(pg_data_volume_name)],
port_bindings={5432: host_pg_port},
)
logging.debug('Creating postgresql container')
pg = docker_client.create_container(
image=pg_image,
host_config=host_config,
name=name,
)

Vincent Hatakeyama
committed
logging.debug('Starting postgresql container')
docker_client.start(pg.get('Id'))
# give pg the time to start up
import time
time.sleep(5)
arg.append('--db_port')
arg.append(str(host_pg_port))

Vincent Hatakeyama
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
user=user,
password=password,
database='postgres',
host=local_ip,
port=port,
)

Vincent Hatakeyama
committed
else:
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
user=user,
password=password,
database='postgres',
)
except OperationalError as exception:
if nmspc.create_user:
logging.info('Cannot connect to database with user %s', user)
logging.info(exception)
logging.info('Creating user %s', user)
if start_postgresql:
connection = connect(
user='pg',
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:
logging.warn('Cannot connect to database with user %s', user)
logging.warn(exception)
# volume magic
for addons_dir in addon_dirs:
'%s:/mnt/addons/%s' % (
os.path.realpath(os.path.join(project_path, addons_dir)),
addons_dir))
# also check directory for symbolic links
for subdir, dirs, files in os.walk(
os.path.realpath(os.path.join(project_path, addons_dir))
):
for directory in dirs:
if os.path.islink(os.path.join(subdir, directory)):
binds.append(
'%s:/mnt/addons/%s' % (
os.path.realpath(os.path.join(
project_path, addons_dir, directory)),
os.path.join(addons_dir, directory))
)
all_addons_dir = []
if odoo_type != 'odoo11':
all_addons_dir.append('/opt/odoo/sources/odoo/addons')
all_addons_dir.extend(
'/mnt/addons/%s' % addons_dir for addons_dir in addon_dirs)
if all_addons_dir:
arg.append('--addons-path')
arg.append(','.join(all_addons_dir))
if not no_flake8:
if which('flake8'):
logging.info('Running flake8')
call(['flake8'])
else:
logging.warning('No flake8 in PATH')

Vincent Hatakeyama
committed
if odoo_type in ('odoo11', 'odoo10'):
# ipdb should not be run if not interactive
arg.append('--dev=xml,reload,ipdb')

Vincent Hatakeyama
committed
if odoo_type in ('odoo8', 'odoo7'):
arg.append('--auto-reload')
if odoo_help:
arg.append('--help')
if start_postgresql and not odoo_help:

Vincent Hatakeyama
committed
# use docker-py to be able to stop both the database and odoo
# add volumes
odoo_host_config = docker_client.create_host_config(
binds=binds,
port_bindings={8069: 8069},
network_mode='host' if use_host_network else 'bridge',
)
logging.debug('Creating odoo container')
odoo = docker_client.create_container(
name=project_name, host_config=odoo_host_config,
image=image, command=arg, tty=True)
logging.debug('Starting odoo container')
docker_client.start(odoo.get('Id'))

Vincent Hatakeyama
committed
def signal_handler(code, frame):
if code == signal.SIGINT:
logging.debug('You pressed Ctrl+C!')
if isRunning(docker_client, odoo.get('Id')):
logging.info('Stopping odoo')
docker_client.stop(odoo.get('Id'))
logging.info('Removing container odoo')
docker_client.remove_container(odoo.get('Id'))
if start_postgresql:
logging.info('Stopping postgresql')
docker_client.stop(pg.get('Id'))
logging.info('Removing container postgresql')
docker_client.remove_container(pg.get('Id'))
sys.exit(0)
# TODO add a kill of pg when crashing
signal.signal(signal.SIGINT, signal_handler)
logging.info('Press Ctrl+C to quit')

Vincent Hatakeyama
committed

Vincent Hatakeyama
committed
# print docker logs of odoo
stream = docker_client.logs(odoo.get('Id'), stream=True, follow=True)
while isRunning(docker_client, odoo.get('Id')):
try:
for log in stream:
sys.stdout.write(log)
except ConnectionError:
# If there is no log for some time requests throw some errors
# we ignore them
pass

Vincent Hatakeyama
committed

Vincent Hatakeyama
committed
# Clean up, just in case
signal_handler(signal.SIGINT, None)
# TODO add handling of signal to restart odoo
else:
# 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)
result = call(cmd)
return result

Vincent Hatakeyama
committed
def createVolume(docker_client, data_volume_name):

Vincent Hatakeyama
committed
"""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']:
logging.debug('Volume %s already exist', data_volume_name)
return volumes['Volumes'][0]
else:
logging.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
"""
return len(docker_client.containers(
filters={'id': container_id}, quiet=True))
if __name__ == "__main__":

Vincent Hatakeyama
committed
return_code = main(sys.argv[1:])

Vincent Hatakeyama
committed
if return_code:
exit(return_code)