#!/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 import pwd from subprocess import call import sys import os import ConfigParser import docker # apt python-docker (1.9) from psycopg2 import connect, OperationalError # apt python-psycopg2 # TODO auto create list of module _logger = logging.getLogger(__name__) __version__ = '0.1.0' __date__ = '2017-08-11' __updated__ = '2017-08-28' # Check the version of the "docker" package if docker.__version__.split('.') < [2]: print("the version of the docker package is {} instead of 1.9" .format(docker.__version__)) sys.exit(1) def main(argv=None): # IGNORE:C0111 """Parse arguments and launch conversion """ if argv is None: argv = sys.argv else: sys.argv.extend(argv) 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 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 ''' % ( program_shortdesc, str(__date__), ) # TODO the script assume it is launched from the parent super project project_path = os.path.realpath('.') project_name = os.path.basename(project_path) 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, ) 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", action='store_true', ) 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, ) parser.add_argument( '--create-user', help="Create user in database (using your user) [default: %(default)s]", action='store_true', ) parser.add_argument( '--install-default', help="Append the module_list from setup.cfg to the modules to install [default: %(default)s]", action='store_true', ) 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 (work better with python-netifaces installed) [default: %(default)s]", 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 nmspc = parser.parse_args() verbose = nmspc.verbose if not verbose: logging.basicConfig(level=logging.WARN) if verbose == 1: logging.basicConfig(level=logging.INFO) if verbose and verbose > 1: logging.basicConfig(level=logging.DEBUG) db_user = nmspc.db_user db_password = nmspc.db_password use_host_network = nmspc.host_network if project_name == 'odoo_scripts': logging.fatal( "You must run this script from the super project" " (./odoo_scripts/docker_dev_start.py)") return c = ConfigParser.ConfigParser() logging.debug('setup file path %s', setup_path) # TODO test that setup file exists c.read(setup_path) addon_dirs = c.get('odoo_scripts', 'addon_dirs', '').split() 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) 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) logging.debug("Docker image: %s", image) # detect if docker image already exists docker_client = docker.Client(base_url='unix://var/run/docker.sock') image_list = docker_client.images(name=image, quiet=True) if not image_list: logging.warn("Image %s does not exist", image) else: logging.info("Test image found") options = [ '--name', project_name, '--rm', '--publish', '8069:8069', '--tty', '--interactive', ] 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') arg.append('--log-level=test') if nmspc.test_default: test_modules = c.get('odoo_scripts', 'module_list_tests').split() or [] arg.append('-u %s' % test_modules) arg.append('--test-enable') arg.append('--log-level=test') if nmspc.database: arg.append('-d %s' % nmspc.database) if nmspc.install or nmspc.install_default: modules_to_install = [] if nmspc.install: modules_to_install.extend(nmspc.install) 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 if use_host_network: local_ip = '127.0.0.1' options.append('--network') options.append('host') else: local_ip = None try: from netifaces import ifaddresses, AF_INET # apt python-netifaces ifaceName = 'docker0' addresses = [i['addr'] for i in ifaddresses(ifaceName).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') if not local_ip: 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] logging.debug('IP found %s', local_ip) arg.append('--db_host') arg.append(local_ip) # auto detect local conf local_conf_path = 'conf/dev/odoo.conf' user = db_user password = db_password if os.path.isfile(local_conf_path): logging.info('Local configuration file found: %s' % local_conf_path) options.append('--volume') options.append('%s:/opt/odoo/etc/odoo.conf' % os.path.join( project_path, local_conf_path)) 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') # data volume handling if odoo_type != 'odoo7': options.append('--mount') data_volume_name = '{}_data'.format(project_name) logging.debug('Using data volume %s', data_volume_name) volumes = docker_client.volumes(filters={'name': data_volume_name}) if volumes['Volumes']: logging.debug('Volume %s already exist', data_volume_name) volume = volumes['Volumes'][0] else: logging.debug('Creating volume %s', data_volume_name) volume = docker_client.create_volume(name=data_volume_name) mount_opts = 'source={},target=/mnt/data'.format(data_volume_name) # make sure the permission in the volume are correct # TODO replace by something cleaner if possible call(['docker', 'run', '--rm', '--mount', mount_opts, 'busybox', 'chmod', '777', '/mnt/data']) options.append(mount_opts) arg.append('--data-dir /mnt/data') else: logging.debug('No data volume for this odoo version') # Check that connection can be done, try to create user if asked to try: test_connection = connect(user=user, password=password, database='postgres', host=local_ip) 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) 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: binds.append( '%s:/mnt/addons/%s' % (os.path.join(project_path, addons_dir), addons_dir)) all_addons_dir = [ '/opt/odoo/sources/odoo/addons', ] all_addons_dir.extend( '/mnt/addons/%s' % addons_dir for addons_dir in addon_dirs) arg.append('--addons-path') arg.append(','.join(all_addons_dir)) cmd = ['docker', 'run'] cmd.extend(options) cmd.append(image) cmd.extend(arg) logging.debug(cmd) # TODO directly use docker-py instead call(cmd) # TODO add handling of signal to reload if __name__ == "__main__": return_code = main() if return_code: exit(return_code)