Skip to content
Snippets Groups Projects
Commit 8c4c4394 authored by Christophe Monniez's avatar Christophe Monniez
Browse files

[REF] packaging: use docker to build packages


Actually, docker is used to test Linux packages once they are built.
The packaging process is done on the host system where package.py is
run.

Consequences:
    * difficult to reproduce an exact same build on another host
    * changing a Debian/Ubuntu target version means some upgrade steps
      on the build host (mix that with the fact that the host also
      could build old Odoo versions)
    * Fedora rpm package is built on an Ubuntu system (which is not a
      real problem but it could be)

With this commit, the package.py utility is refactored to use Docker
containers to build the Gnu/Linux packages.

Also, the Debian package is adapted for Debian Buster, the RPM package
is built based on Fedora 30.

Some minor changes are also made in the windows packaging system.

closes odoo/odoo#37766

Signed-off-by: default avatarChristophe Monniez (moc) <moc@odoo.com>
parent e10a4025
No related branches found
No related tags found
No related merge requests found
[install]
optimize=1
[bdist_rpm]
no-autoreq = yes
install-script = setup/redhat/install.sh
......@@ -48,3 +51,4 @@ requires =
python3-werkzeug
python3-xlwt
python3-xlrd
# Please note that this Dockerfile is used for testing nightly builds and should
# not be used to deploy Odoo
FROM debian:stretch
FROM debian:buster
MAINTAINER Odoo S.A. <info@odoo.com>
RUN apt-get update && \
......@@ -12,11 +12,14 @@ RUN dpkg-reconfigure locales && \
locale-gen C.UTF-8 && \
/usr/sbin/update-locale LANG=C.UTF-8
ENV LC_ALL C.UTF-8
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update -qq && \
apt-get upgrade -qq -y && \
apt-get install -qq -y\
adduser \
dh-python \
packaging-dev \
postgresql \
postgresql-client \
python3 \
......@@ -51,7 +54,8 @@ RUN apt-get update -qq && \
python3-vatnumber \
python3-vobject \
python3-werkzeug \
python3-xlsxwriter && \
python3-xlsxwriter \
rsync && \
rm -rf /var/lib/apt/lists/*
RUN echo "PS1=\"[\u@nightly-tests] # \"" > ~/.bashrc
# Please note that this Dockerfile is used for testing nightly builds and should
# not be used to deploy Odoo
FROM fedora:26
FROM fedora:30
MAINTAINER Odoo S.A. <info@odoo.com>
# Dependencies and postgres
......@@ -45,17 +45,17 @@ RUN dnf update -d 0 -e 0 -y && \
python3-werkzeug \
python3-xlwt \
python3-xlrd \
python3-xlsxwriter \
libsass \
pytz \
postgresql \
postgresql-server \
postgresql-libs \
postgresql-contrib \
postgresql-devel -y && \
postgresql-devel \
rpmdevtools -y && \
dnf clean all
RUN pip3 install XlsxWriter
# Postgres configuration
RUN mkdir -p /var/lib/postgres/data
RUN chown -R postgres:postgres /var/lib/postgres/data
......
# Please note that this Dockerfile is used for testing nightly builds and should
# not be used to deploy Odoo
FROM debian:stretch
FROM debian:buster
MAINTAINER Odoo S.A. <info@odoo.com>
RUN apt-get update && \
......@@ -20,7 +20,7 @@ RUN apt-get update -qq && \
postgresql-server-dev-all \
postgresql-client \
adduser \
libsass0 \
libsass1 \
libxml2-dev \
libxslt1-dev \
libldap2-dev \
......
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from __future__ import print_function
import argparse
import logging
import optparse
import os
import pexpect
import shutil
import signal
import subprocess
import sys
import tempfile
import time
import traceback
try:
from xmlrpc import client as xmlrpclib
except ImportError:
import xmlrpclib
from contextlib import contextmanager
from glob import glob
from os.path import abspath, dirname, join
from sys import stdout, stderr
from tempfile import NamedTemporaryFile
from xmlrpc import client as xmlrpclib
# apt-get install rsync python-pexpect debhelper python-setuptools
from glob import glob
#----------------------------------------------------------
# Utils
#----------------------------------------------------------
exec(open(join(dirname(__file__), '..', 'odoo', 'release.py'), 'rb').read())
version = version.split('-')[0].replace('saas~','')
docker_version = version.replace('+', '')
timestamp = time.strftime("%Y%m%d", time.gmtime())
ROOTDIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
TSTAMP = time.strftime("%Y%m%d", time.gmtime())
exec(open(os.path.join(ROOTDIR, 'odoo', 'release.py'), 'rb').read())
VERSION = version.split('-')[0].replace('saas~', '')
GPGPASSPHRASE = os.getenv('GPGPASSPHRASE')
GPGID = os.getenv('GPGID')
PUBLISH_DIRS = {
'debian': 'deb',
'redhat': 'rpm',
'tarball': 'src',
'windows': 'exe',
}
ADDONS_NOT_TO_PUBLISH = [
]
def move_glob(source, wildcards, destination):
"""Move files matched by wildcards from source to destination
wildcards can be a single string wildcard like '*.deb' or a list of wildcards
"""
if not os.path.isdir(destination):
raise BaseException('Destination "{}" is not a directory'.format(destination))
if isinstance(wildcards, str):
wildcards = [wildcards]
for wc in wildcards:
for file_path in glob(os.path.join(source, wc)):
shutil.move(file_path, destination)
def mkdir(d):
if not os.path.isdir(d):
os.makedirs(d)
def system(l, chdir=None):
logging.info("System call: {}".format(l))
if chdir:
cwd = os.getcwd()
os.chdir(chdir)
if isinstance(l, list):
rc = os.spawnvp(os.P_WAIT, l[0], l)
elif isinstance(l, str):
tmp = ['sh', '-c', l]
rc = os.spawnvp(os.P_WAIT, tmp[0], tmp)
if chdir:
os.chdir(cwd)
return rc
DOCKERVERSION = VERSION.replace('+', '')
INSTALL_TIMEOUT = 600
DOCKERUSER = """
RUN mkdir /var/lib/odoo && \
groupadd -g %(group_id)s odoo && \
useradd -u %(user_id)s -g odoo odoo -d /var/lib/odoo && \
mkdir /data && \
chown odoo:odoo /var/lib/odoo /data
USER odoo
""" % {'group_id': os.getgid(), 'user_id': os.getuid()}
def run_cmd(cmd, chdir=None, timeout=None):
logging.info("Running command %s", cmd)
return subprocess.run(cmd, cwd=chdir, timeout=timeout)
def _rpc_count_modules(addr='http://127.0.0.1', port=8069, dbname='mycompany'):
time.sleep(5)
uid = xmlrpclib.ServerProxy('%s:%s/xmlrpc/common' % (addr, port)).authenticate(
uid = xmlrpclib.ServerProxy('%s:%s/xmlrpc/2/common' % (addr, port)).authenticate(
dbname, 'admin', 'admin', {}
)
modules = xmlrpclib.ServerProxy('%s:%s/xmlrpc/object' % (addr, port)).execute(
modules = xmlrpclib.ServerProxy('%s:%s/xmlrpc/2/object' % (addr, port)).execute(
dbname, uid, 'admin', 'ir.module.module', 'search', [('state', '=', 'installed')]
)
if modules and len(modules) > 1:
if len(modules) > 1:
time.sleep(1)
toinstallmodules = xmlrpclib.ServerProxy('%s:%s/xmlrpc/object' % (addr, port)).execute(
toinstallmodules = xmlrpclib.ServerProxy('%s:%s/xmlrpc/2/object' % (addr, port)).execute(
dbname, uid, 'admin', 'ir.module.module', 'search', [('state', '=', 'to install')]
)
if toinstallmodules:
......@@ -93,22 +65,26 @@ def _rpc_count_modules(addr='http://127.0.0.1', port=8069, dbname='mycompany'):
logging.info("Package test: successfuly installed %s modules" % len(modules))
else:
logging.error("Package test: FAILED. Not able to install base.")
raise Exception("Installation of package failed")
raise Exception("Package test: FAILED. Not able to install base.")
def publish(o, type, extensions):
def _publish(o, release):
arch = ''
filename = release.split(os.path.sep)[-1]
release_dir = PUBLISH_DIRS[type]
release_path = join(o.pub, release_dir, filename)
system('mkdir -p %s' % join(o.pub, release_dir))
shutil.move(join(o.build_dir, release), release_path)
def publish(args, pub_type, extensions):
"""Publish builded package (move builded files and generate a symlink to the latests)
:args: parsed program args
:pub_type: one of [deb, rpm, src, exe]
:extensions: list of extensions to publish
:returns: published files
"""
def _publish(release):
build_path = os.path.join(args.build_dir, release)
filename = release.split(os.path.sep)[-1]
release_dir = os.path.join(args.pub, pub_type)
release_path = os.path.join(release_dir, filename)
os.renames(build_path, release_path)
# Latest/symlink handler
release_abspath = abspath(release_path)
latest_abspath = release_abspath.replace(timestamp, 'latest')
release_abspath = os.path.abspath(release_path)
latest_abspath = release_abspath.replace(TSTAMP, 'latest')
if os.path.islink(latest_abspath):
os.unlink(latest_abspath)
......@@ -119,82 +95,307 @@ def publish(o, type, extensions):
published = []
for extension in extensions:
release = glob("%s/odoo_*.%s" % (o.build_dir, extension))
release = glob("%s/odoo_*.%s" % (args.build_dir, extension))
if release:
published.append(_publish(o, release[0]))
published.append(_publish(release[0]))
return published
class OdooDocker(object):
def __init__(self):
self.log_file = NamedTemporaryFile(mode='w+b', prefix="bash", suffix=".txt", delete=False)
self.port = 8069 # TODO sle: reliable way to get a free port?
self.prompt_re = '[root@nightly-tests] # '
self.timeout = 600
def system(self, command):
self.docker.sendline(command)
self.docker.expect_exact(self.prompt_re)
def start(self, docker_image, build_dir, pub_dir):
self.build_dir = build_dir
self.pub_dir = pub_dir
self.docker = pexpect.spawn(
'docker run -v %s:/opt/release -p 127.0.0.1:%s:8069'
' -t -i %s /bin/bash --noediting' % (self.build_dir, self.port, docker_image),
timeout=self.timeout,
searchwindowsize=len(self.prompt_re) + 1,
)
time.sleep(2) # let the bash start
self.docker.logfile_read = self.log_file
self.id = subprocess.check_output('docker ps -l -q', shell=True).strip().decode('ascii')
def end(self):
try:
_rpc_count_modules(port=str(self.port))
except Exception as e:
logging.error('Exception during docker execution: %s:' % str(e))
logging.error('Error during docker execution: printing the bash output:')
with open(self.log_file.name) as f:
print('\n'.join(f.readlines()), file=stderr)
raise
finally:
self.docker.close()
system('docker rm -f %s' % self.id)
self.log_file.close()
os.remove(self.log_file.name)
@contextmanager
def docker(docker_image, build_dir, pub_dir):
_docker = OdooDocker()
try:
_docker.start(docker_image, build_dir, pub_dir)
try:
yield _docker
except Exception:
raise
finally:
_docker.end()
# ---------------------------------------------------------
# Generates Packages, Sources and Release files of debian package
# ---------------------------------------------------------
def gen_deb_package(args, published_files):
# Executes command to produce file_name in path, and moves it to args.pub/deb
def _gen_file(args, command, file_name, path):
cur_tmp_file_path = os.path.join(path, file_name)
with open(cur_tmp_file_path, 'w') as out:
subprocess.call(command, stdout=out, cwd=path)
shutil.copy(cur_tmp_file_path, os.path.join(args.pub, 'deb', file_name))
# Copy files to a temp directory (required because the working directory must contain only the
# files of the last release)
temp_path = tempfile.mkdtemp(suffix='debPackages')
for pub_file_path in published_files:
shutil.copy(pub_file_path, temp_path)
commands = [
(['dpkg-scanpackages', '.'], "Packages"), # Generate Packages file
(['dpkg-scansources', '.'], "Sources"), # Generate Sources file
(['apt-ftparchive', 'release', '.'], "Release") # Generate Release file
]
# Generate files
for command in commands:
_gen_file(args, command[0], command[-1], temp_path)
# Remove temp directory
shutil.rmtree(temp_path)
if args.sign:
# Generate Release.gpg (= signed Release)
# Options -abs: -a (Create ASCII armored output), -b (Make a detach signature), -s (Make a signature)
subprocess.call(['gpg', '--default-key', GPGID, '--passphrase', GPGPASSPHRASE, '--yes', '-abs', '--no-tty', '-o', 'Release.gpg', 'Release'], cwd=os.path.join(args.pub, 'deb'))
# ---------------------------------------------------------
# Generates an RPM repo
# ---------------------------------------------------------
def gen_rpm_repo(args, file_name):
"""Genereate a rpm repo in publish directory"""
# Sign the RPM
rpmsign = pexpect.spawn('/bin/bash', ['-c', 'rpm --resign %s' % file_name], cwd=os.path.join(args.pub, 'rpm'))
rpmsign.expect_exact('Enter pass phrase: ')
rpmsign.send(GPGPASSPHRASE + '\r\n')
rpmsign.expect(pexpect.EOF)
# Removes the old repodata
shutil.rmtree(os.path.join(args.pub, 'rpm', 'repodata'))
# Copy files to a temp directory (required because the working directory must contain only the
# files of the last release)
temp_path = tempfile.mkdtemp(suffix='rpmPackages')
shutil.copy(file_name, temp_path)
run_cmd(['createrepo', temp_path]).check_returncode() # creates a repodata folder in temp_path
shutil.copytree(os.path.join(temp_path, "repodata"), os.path.join(args.pub, 'rpm'))
# Remove temp directory
shutil.rmtree(temp_path)
def _prepare_build_dir(args, win32=False):
"""Copy files to the build directory"""
logging.info('Preparing build dir "%s"', args.build_dir)
cmd = ['rsync', '-a', '--delete', '--exclude', '.git', '--exclude', '*.pyc', '--exclude', '*.pyo']
if win32 is False:
cmd += ['--exclude', 'setup/win32']
run_cmd(cmd + ['%s/' % args.odoo_dir, args.build_dir])
for addon_path in glob(os.path.join(args.build_dir, 'addons/*')):
if args.blacklist is None or os.path.basename(addon_path) not in args.blacklist:
try:
shutil.move(addon_path, os.path.join(args.build_dir, 'odoo/addons'))
except shutil.Error as e:
logging.warning("Warning '%s' while moving addon '%s", e, addon_path)
if addon_path.startswith(args.build_dir) and os.path.isdir(addon_path):
logging.info("Removing ''".format(addon_path))
try:
shutil.rmtree(addon_path)
except shutil.Error as rm_error:
logging.warning("Cannot remove '{}': {}".format(addon_path, rm_error))
# Docker stuffs
class OdooTestTimeoutError(Exception):
pass
class OdooTestError(Exception):
pass
class Docker():
"""Base Docker class. Must be inherited by specific Docker builder class"""
arch = None
def __init__(self, args):
"""
:param args: argparse parsed arguments
"""
self.args = args
self.tag = 'odoo-%s-%s-nightly-tests' % (DOCKERVERSION, self.arch)
self.container_name = None
self.exposed_port = None
dockerfiles = {
'tgz': os.path.join(args.build_dir, 'setup/package.dfsrc'),
'deb': os.path.join(args.build_dir, 'setup/package.dfdebian'),
'rpm': os.path.join(args.build_dir, 'setup/package.dffedora'),
}
self.dockerfile = dockerfiles[self.arch]
self.test_log_file = '/data/src/test-%s.log' % self.arch
self.build_image()
def build_image(self):
"""Build the dockerimage by copying Dockerfile into build_dir/docker"""
docker_dir = os.path.join(self.args.build_dir, 'docker')
docker_file_path = os.path.join(docker_dir, 'Dockerfile')
os.mkdir(docker_dir)
shutil.copy(self.dockerfile, docker_file_path)
with open(docker_file_path, 'a') as dockerfile:
dockerfile.write(DOCKERUSER)
shutil.copy(os.path.join(self.args.build_dir, 'requirements.txt'), docker_dir)
run_cmd(["docker", "build", "--rm=True", "-t", self.tag, "."], chdir=docker_dir, timeout=1200).check_returncode()
shutil.rmtree(docker_dir)
def run(self, cmd, build_dir, container_name, user='odoo', exposed_port=None, detach=False, timeout=None):
self.container_name = container_name
docker_cmd = [
"docker",
"run",
"--user=%s" % user,
"--name=%s" % container_name,
"--rm",
"--volume=%s:/data/src" % build_dir
]
if exposed_port:
docker_cmd.extend(['-p', '127.0.0.1:%s:%s' % (exposed_port, exposed_port)])
self.exposed_port = exposed_port
if detach:
docker_cmd.append('-d')
# preserve logs in case of detached docker container
cmd = '(%s) > %s 2>&1' % (cmd, self.test_log_file)
docker_cmd.extend([
self.tag,
"/bin/bash",
"-c",
"cd /data/src && %s" % cmd
])
run_cmd(docker_cmd, timeout=timeout).check_returncode()
def is_running(self):
dinspect = subprocess.run(['docker', 'container', 'inspect', self.container_name], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
return True if dinspect.returncode == 0 else False
def stop(self):
run_cmd(["docker", "stop", self.container_name]).check_returncode()
def test_odoo(self):
logging.info('Starting to test Odoo install test')
start_time = time.time()
while self.is_running() and (time.time() - start_time) < INSTALL_TIMEOUT:
time.sleep(5)
if os.path.exists(os.path.join(args.build_dir, 'odoo.pid')):
_rpc_count_modules(port=self.exposed_port)
self.stop()
return
if self.is_running():
self.stop()
raise OdooTestTimeoutError('Odoo pid file never appeared after %s sec' % INSTALL_TIMEOUT)
raise OdooTestError('Error while installing/starting Odoo after %s sec.\nSee testlogs.txt in build dir' % int(time.time() - start_time))
def build(self):
"""To be overriden by specific builder"""
pass
def start_test(self):
"""To be overriden by specific builder"""
pass
class DockerTgz(Docker):
"""Docker class to build python src package"""
arch = 'tgz'
def build(self):
logging.info('Start building python tgz package')
self.run('python3 setup.py sdist --quiet --formats=gztar,zip', self.args.build_dir, 'odoo-src-build-%s' % TSTAMP)
os.rename(glob('%s/dist/odoo-*.tar.gz' % self.args.build_dir)[0], '%s/odoo_%s.%s.tar.gz' % (self.args.build_dir, VERSION, TSTAMP))
os.rename(glob('%s/dist/odoo-*.zip' % self.args.build_dir)[0], '%s/odoo_%s.%s.zip' % (self.args.build_dir, VERSION, TSTAMP))
logging.info('Finished building python tgz package')
def start_test(self):
if not self.args.test:
return
logging.info('Start testing python tgz package')
cmds = [
'service postgresql start',
'pip3 install /data/src/odoo_%s.%s.tar.gz' % (VERSION, TSTAMP),
'su postgres -s /bin/bash -c "createuser -s odoo"',
'su postgres -s /bin/bash -c "createdb mycompany"',
'su odoo -s /bin/bash -c "odoo -d mycompany -i base --stop-after-init"',
'su odoo -s /bin/bash -c "odoo -d mycompany --pidfile=/data/src/odoo.pid"',
]
self.run(' && '.join(cmds), self.args.build_dir, 'odoo-src-test-%s' % TSTAMP, user='root', detach=True, exposed_port=8069, timeout=300)
self.test_odoo()
logging.info('Finished testing tgz package')
class DockerDeb(Docker):
"""Docker class to build debian package"""
arch = 'deb'
def build(self):
logging.info('Start building debian package')
# Append timestamp to version for the .dsc to refer the right .tar.gz
cmds = ["sed -i '1s/^.*$/odoo (%s.%s) stable; urgency=low/' debian/changelog" % (VERSION, TSTAMP)]
cmds.append('dpkg-buildpackage -rfakeroot -uc -us -tc')
# As the packages are built in the parent of the buildir, we move them back to build_dir
cmds.append('mv ../odoo_* ./')
self.run(' && '.join(cmds), self.args.build_dir, 'odoo-deb-build-%s' % TSTAMP)
logging.info('Finished building debian package')
def start_test(self):
if not self.args.test:
return
logging.info('Start testing debian package')
cmds = [
'service postgresql start',
'su postgres -s /bin/bash -c "createdb mycompany"',
'/usr/bin/apt-get update -y',
'/usr/bin/dpkg -i /data/src/odoo_%s.%s_all.deb ; /usr/bin/apt-get install -f -y' % (VERSION, TSTAMP),
'su odoo -s /bin/bash -c "odoo -d mycompany -i base --stop-after-init"',
'su odoo -s /bin/bash -c "odoo -d mycompany --pidfile=/data/src/odoo.pid"',
]
self.run(' && '.join(cmds), self.args.build_dir, 'odoo-deb-test-%s' % TSTAMP, user='root', detach=True, exposed_port=8069, timeout=300)
self.test_odoo()
logging.info('Finished testing debian package')
class DockerRpm(Docker):
"""Docker class to build rpm package"""
arch = 'rpm'
def build(self):
logging.info('Start building fedora rpm package')
self.run('python3 setup.py --quiet bdist_rpm', self.args.build_dir, 'odoo-rpm-build-%s' % TSTAMP)
os.rename(glob('%s/dist/odoo-*.noarch.rpm' % self.args.build_dir)[0], '%s/odoo_%s.%s.rpm' % (self.args.build_dir, VERSION, TSTAMP))
logging.info('Finished building fedora rpm package')
def start_test(self):
if not self.args.test:
return
logging.info('Start testing rpm package')
cmds = [
'su postgres -c "/usr/bin/pg_ctl -D /var/lib/postgres/data start"',
'sleep 5',
'su postgres -c "createdb mycompany"',
'dnf install -d 0 -e 0 /data/src/odoo_%s.%s.rpm -y' % (VERSION, TSTAMP),
'su odoo -s /bin/bash -c "odoo -c /etc/odoo/odoo.conf -d mycompany -i base --stop-after-init"',
'su odoo -s /bin/bash -c "odoo -c /etc/odoo/odoo.conf -d mycompany --pidfile=/data/src/odoo.pid"',
]
self.run(' && '.join(cmds), args.build_dir, 'odoo-rpm-test-%s' % TSTAMP, user='root', detach=True, exposed_port=8069, timeout=300)
self.test_odoo()
logging.info('Finished testing rpm package')
# KVM stuffs
class KVM(object):
def __init__(self, o, image, ssh_key='', login='openerp'):
self.o = o
self.image = image
self.ssh_key = ssh_key
self.login = login
def __init__(self, args):
self.args = args
self.image = args.vm_winxp_image
self.ssh_key = args.vm_winxp_ssh_key
self.login = args.vm_winxp_login
def timeout(self,signum,frame):
logging.warning("vm timeout kill (pid: {})".format(self.pid))
os.kill(self.pid,15)
def timeout(self, signum, frame):
logging.warning("vm timeout kill (pid: {})".format(self.kvm_proc.pid))
self.kvm_proc.terminate()
def start(self):
l="kvm -cpu core2duo -smp 2,sockets=2,cores=1,threads=1 -net nic,model=rtl8139 -net user,hostfwd=tcp:127.0.0.1:10022-:22,hostfwd=tcp:127.0.0.1:18069-:8069,hostfwd=tcp:127.0.0.1:15432-:5432 -m 1024 -drive".split(" ")
#l.append('file=%s,if=virtio,index=0,boot=on,snapshot=on'%self.image)
l.append('file=%s,snapshot=on'%self.image)
#l.extend(['-vnc','127.0.0.1:1'])
l.append('-nographic')
logging.info("Starting kvm: {}".format( " ".join(l)))
self.pid=os.spawnvp(os.P_NOWAIT, l[0], l)
kvm_cmd = [
"kvm",
"-cpu", "core2duo",
"-smp", "2,sockets=2,cores=1,threads=1",
"-net", "nic,model=rtl8139",
"-net", "user,hostfwd=tcp:127.0.0.1:10022-:22,hostfwd=tcp:127.0.0.1:18069-:8069,hostfwd=tcp:127.0.0.1:15432-:5432",
"-m", "1024",
"-drive", "file=%s,snapshot=on" % self.image,
"-nographic"
]
logging.info("Starting kvm: {}".format(" ".join(kvm_cmd)))
self.kvm_proc = subprocess.Popen(kvm_cmd)
time.sleep(50)
signal.alarm(2400)
signal.signal(signal.SIGALRM, self.timeout)
......@@ -202,335 +403,165 @@ class KVM(object):
self.run()
finally:
signal.signal(signal.SIGALRM, signal.SIG_DFL)
os.kill(self.pid,15)
self.kvm_proc.terminate()
time.sleep(10)
def ssh(self,cmd):
l=['ssh','-o','UserKnownHostsFile=/dev/null','-o','StrictHostKeyChecking=no','-p','10022','-i',self.ssh_key,'%s@127.0.0.1'%self.login,cmd]
system(l)
def rsync(self,args,options='--delete --exclude .git --exclude .tx --exclude __pycache__'):
cmd ='rsync -rt -e "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -p 10022 -i %s" %s %s' % (self.ssh_key, options, args)
system(cmd)
def ssh(self, cmd):
run_cmd([
'ssh',
'-o', 'UserKnownHostsFile=/dev/null',
'-o', 'StrictHostKeyChecking=no',
'-p', '10022',
'-i', self.ssh_key,
'%s@127.0.0.1' % self.login,
cmd
]).check_returncode()
def rsync(self, rsync_args, options=['--delete', '--exclude', '.git', '--exclude', '.tx', '--exclude', '__pycache__']):
cmd = [
'rsync',
'-rt',
'-e', 'ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -p 10022 -i %s' % self.ssh_key
]
cmd.extend(options)
cmd.extend(rsync_args)
run_cmd(cmd).check_returncode()
def run(self):
pass
class KVMWinBuildExe(KVM):
def run(self):
with open(join(self.o.build_dir, 'setup/win32/Makefile.version'), 'w') as f:
f.write("VERSION=%s\n" % version.replace('~', '_').replace('+', ''))
with open(join(self.o.build_dir, 'setup/win32/Makefile.python'), 'w') as f:
f.write("PYTHON_VERSION=%s\n" % self.o.vm_winxp_python_version.replace('.', ''))
with open(join(self.o.build_dir, 'setup/win32/Makefile.servicename'), 'w') as f:
logging.info('Start building Windows package')
with open(os.path.join(self.args.build_dir, 'setup/win32/Makefile.version'), 'w') as f:
f.write("VERSION=%s\n" % VERSION.replace('~', '_').replace('+', ''))
with open(os.path.join(self.args.build_dir, 'setup/win32/Makefile.python'), 'w') as f:
f.write("PYTHON_VERSION=%s\n" % self.args.vm_winxp_python_version.replace('.', ''))
with open(os.path.join(self.args.build_dir, 'setup/win32/Makefile.servicename'), 'w') as f:
f.write("SERVICENAME=%s\n" % nt_service_name)
remote_build_dir = '/cygdrive/c/odoobuild/server/'
self.ssh("mkdir -p build")
logging.info("Syncing Odoo files to virtual machine...")
self.rsync('%s/ %s@127.0.0.1:%s' % (self.o.build_dir, self.login, remote_build_dir))
self.rsync(['%s/' % self.args.build_dir, '%s@127.0.0.1:%s' % (self.login, remote_build_dir)])
self.ssh("cd {}setup/win32;time make allinone;".format(remote_build_dir))
self.rsync('%s@127.0.0.1:%ssetup/win32/release/ %s/' % (self.login, remote_build_dir, self.o.build_dir), '')
logging.info("KVMWinBuildExe.run(): done")
self.rsync(['%s@127.0.0.1:%ssetup/win32/release/' % (self.login, remote_build_dir), '%s/' % self.args.build_dir])
logging.info('Finished building Windows package')
class KVMWinTestExe(KVM):
def run(self):
setuppath = glob("%s/openerp-server-setup-*.exe" % self.o.build_dir)[0]
setupfile = setuppath.split('/')[-1]
logging.info('Start testing Windows package')
setup_path = glob("%s/openerp-server-setup-*.exe" % self.args.build_dir)[0]
setupfile = setup_path.split('/')[-1]
setupversion = setupfile.split('openerp-server-setup-')[1].split('.exe')[0]
self.rsync('"%s" %s@127.0.0.1:' % (setuppath, self.login))
self.rsync(['"%s"' % setup_path, '%s@127.0.0.1:' % self.login])
self.ssh("TEMP=/tmp ./%s /S" % setupfile)
self.ssh('PGPASSWORD=openpgpwd /cygdrive/c/"Program Files"/"Odoo %s"/PostgreSQL/bin/createdb.exe -e -U openpg mycompany' % setupversion)
self.ssh('netsh advfirewall set publicprofile state off')
self.ssh('/cygdrive/c/"Program Files"/"Odoo {sv}"/python/python.exe \'c:\\Program Files\\Odoo {sv}\\server\\odoo-bin\' -d mycompany -i base --stop-after-init'.format(sv=setupversion))
_rpc_count_modules(port=18069)
logging.info('Finished testing Windows package')
#----------------------------------------------------------
# Stage: building
#----------------------------------------------------------
def _prepare_build_dir(o, win32=False):
cmd = ['rsync', '-a', '--exclude', '.git', '--exclude', '*.pyc', '--exclude', '*.pyo']
if not win32:
cmd += ['--exclude', 'setup/win32']
system(cmd + ['%s/' % o.odoo_dir, o.build_dir])
for addon_path in glob(join(o.build_dir, 'addons/*')):
if addon_path.split(os.path.sep)[-1] not in ADDONS_NOT_TO_PUBLISH:
try:
shutil.move(addon_path, join(o.build_dir, 'odoo/addons'))
except shutil.Error as e:
# Thrown when the add-on is already in odoo/addons (if _prepare_build_dir
# has already been called once)
logging.warning("Warning '{}' while moving addon '{}'".format(e,addon_path))
if addon_path.startswith(o.build_dir) and os.path.isdir(addon_path):
logging.info("Removing '{}'".format(addon_path))
try:
shutil.rmtree(addon_path)
except shutil.Error as rm_error:
logging.warning("Cannot remove '{}': {}".format(addon_path, rm_error))
def build_tgz(o):
system(['python3', 'setup.py', 'sdist', '--quiet', '--formats=gztar,zip'], o.build_dir)
system(['mv', glob('%s/dist/odoo-*.tar.gz' % o.build_dir)[0], '%s/odoo_%s.%s.tar.gz' % (o.build_dir, version, timestamp)])
system(['mv', glob('%s/dist/odoo-*.zip' % o.build_dir)[0], '%s/odoo_%s.%s.zip' % (o.build_dir, version, timestamp)])
def build_deb(o):
# Append timestamp to version for the .dsc to refer the right .tar.gz
cmd=['sed', '-i', '1s/^.*$/odoo (%s.%s) stable; urgency=low/'%(version,timestamp), 'debian/changelog']
subprocess.call(cmd, cwd=o.build_dir)
if not o.no_debsign:
deb = pexpect.spawn('dpkg-buildpackage -rfakeroot -k%s' % GPGID, cwd=o.build_dir)
deb.logfile = stdout.buffer
if GPGPASSPHRASE:
deb.expect_exact('Enter passphrase: ', timeout=1200)
deb.send(GPGPASSPHRASE + '\r\n')
deb.expect_exact('Enter passphrase: ')
deb.send(GPGPASSPHRASE + '\r\n')
deb.expect(pexpect.EOF, timeout=1200)
else:
subprocess.call(['dpkg-buildpackage', '-rfakeroot', '-uc', '-us'], cwd=o.build_dir)
# As the packages are built in the parent of the buildir, we move them back to build_dir
build_dir_parent = '{}/../'.format(o.build_dir)
wildcards = ['odoo_{}'.format(wc) for wc in ('*.deb', '*.dsc', '*_amd64.changes', '*.tar.gz', '*.tar.xz')]
move_glob(build_dir_parent, wildcards, o.build_dir)
def build_exe(args):
KVMWinBuildExe(args).start()
shutil.copy(glob('%s/openerp*.exe' % args.build_dir)[0], '%s/odoo_%s.%s.exe' % (args.build_dir, VERSION, TSTAMP))
def build_rpm(o):
system(['python3', 'setup.py', '--quiet', 'bdist_rpm'], o.build_dir)
system(['mv', glob('%s/dist/odoo-*.noarch.rpm' % o.build_dir)[0], '%s/odoo_%s.%s.noarch.rpm' % (o.build_dir, version, timestamp)])
def build_exe(o):
KVMWinBuildExe(o, o.vm_winxp_image, o.vm_winxp_ssh_key, o.vm_winxp_login).start()
system(['cp', glob('%s/openerp*.exe' % o.build_dir)[0], '%s/odoo_%s.%s.exe' % (o.build_dir, version, timestamp)])
def test_exe(args):
if args.test:
KVMWinTestExe(args).start()
#----------------------------------------------------------
# Stage: testing
#----------------------------------------------------------
def _prepare_testing(o):
logging.info('Preparing testing')
if not o.no_tarball:
logging.info('Preparing docker container instance for tarball')
subprocess.call(["mkdir", "docker_src"], cwd=o.build_dir)
subprocess.call(["cp", "package.dfsrc", os.path.join(o.build_dir, "docker_src", "Dockerfile")],
cwd=os.path.join(o.odoo_dir, "setup"))
# Use rsync to copy requirements.txt in order to keep original permissions
subprocess.call(["rsync", "-a", "requirements.txt", os.path.join(o.build_dir, "docker_src")],
cwd=os.path.join(o.odoo_dir))
subprocess.call(["docker", "build", "-t", "odoo-%s-src-nightly-tests" % docker_version, "."],
cwd=os.path.join(o.build_dir, "docker_src"))
if not o.no_debian:
logging.info('Preparing docker container instance for debian')
subprocess.call(["mkdir", "docker_debian"], cwd=o.build_dir)
subprocess.call(["cp", "package.dfdebian", os.path.join(o.build_dir, "docker_debian", "Dockerfile")],
cwd=os.path.join(o.odoo_dir, "setup"))
# Use rsync to copy requirements.txt in order to keep original permissions
subprocess.call(["rsync", "-a", "requirements.txt", os.path.join(o.build_dir, "docker_debian")],
cwd=os.path.join(o.odoo_dir))
subprocess.call(["docker", "build", "-t", "odoo-%s-debian-nightly-tests" % docker_version, "."],
cwd=os.path.join(o.build_dir, "docker_debian"))
if not o.no_rpm:
logging.info('Preparing docker container instance for RPM')
subprocess.call(["mkdir", "docker_fedora"], cwd=o.build_dir)
subprocess.call(["cp", "package.dffedora", os.path.join(o.build_dir, "docker_fedora", "Dockerfile")],
cwd=os.path.join(o.odoo_dir, "setup"))
subprocess.call(["docker", "build", "-t", "odoo-%s-fedora-nightly-tests" % docker_version, "."],
cwd=os.path.join(o.build_dir, "docker_fedora"))
def test_tgz(o):
logging.info('Testing tarball in docker')
with docker('odoo-%s-src-nightly-tests' % docker_version, o.build_dir, o.pub) as wheezy:
wheezy.release = '*.tar.gz'
wheezy.system("service postgresql start")
wheezy.system('pip3 install /opt/release/%s' % wheezy.release)
wheezy.system("useradd --system --no-create-home odoo")
wheezy.system('su postgres -s /bin/bash -c "createuser -s odoo"')
wheezy.system('su postgres -s /bin/bash -c "createdb mycompany"')
wheezy.system('mkdir /var/lib/odoo')
wheezy.system('chown odoo:odoo /var/lib/odoo')
wheezy.system('su odoo -s /bin/bash -c "odoo -d mycompany -i base --stop-after-init"')
wheezy.system('su odoo -s /bin/bash -c "odoo -d mycompany &"')
def test_deb(o):
logging.info('Testing deb package in docker')
with docker('odoo-%s-debian-nightly-tests' % docker_version, o.build_dir, o.pub) as wheezy:
wheezy.release = '*.deb'
wheezy.system("service postgresql start")
wheezy.system('su postgres -s /bin/bash -c "createdb mycompany"')
wheezy.system('/usr/bin/dpkg -i /opt/release/%s' % wheezy.release)
wheezy.system('/usr/bin/apt-get install -f -y')
wheezy.system('su odoo -s /bin/bash -c "odoo -c /etc/odoo/odoo.conf -d mycompany -i base --stop-after-init"')
wheezy.system('su odoo -s /bin/bash -c "odoo -c /etc/odoo/odoo.conf -d mycompany &"')
def test_rpm(o):
logging.info('Testing rpm in docker')
with docker('odoo-%s-fedora-nightly-tests' % docker_version, o.build_dir, o.pub) as fedora24:
fedora24.release = '*.noarch.rpm'
# Start postgresql
fedora24.system('su postgres -c "/usr/bin/pg_ctl -D /var/lib/postgres/data start"')
fedora24.system('sleep 5')
fedora24.system('su postgres -c "createdb mycompany"')
# Odoo install
fedora24.system('dnf install -d 0 -e 0 /opt/release/%s -y' % fedora24.release)
fedora24.system('su odoo -s /bin/bash -c "odoo -c /etc/odoo/odoo.conf -d mycompany -i base --stop-after-init"')
fedora24.system('su odoo -s /bin/bash -c "odoo -c /etc/odoo/odoo.conf -d mycompany &"')
def test_exe(o):
logging.info('Testng windows installer in KVM')
KVMWinTestExe(o, o.vm_winxp_image, o.vm_winxp_ssh_key, o.vm_winxp_login).start()
#---------------------------------------------------------
# Generates Packages, Sources and Release files of debian package
#---------------------------------------------------------
def gen_deb_package(o, published_files):
# Executes command to produce file_name in path, and moves it to o.pub/deb
def _gen_file(o, command, file_name, path):
cur_tmp_file_path = os.path.join(path, file_name)
with open(cur_tmp_file_path, 'w') as out:
subprocess.call(command, stdout=out, cwd=path)
system(['cp', cur_tmp_file_path, os.path.join(o.pub, 'deb', file_name)])
# Copy files to a temp directory (required because the working directory must contain only the
# files of the last release)
temp_path = tempfile.mkdtemp(suffix='debPackages')
for pub_file_path in published_files:
system(['cp', pub_file_path, temp_path])
def parse_args():
ap = argparse.ArgumentParser()
build_dir = "%s-%s" % (ROOTDIR, TSTAMP)
log_levels = {"debug": logging.DEBUG, "info": logging.INFO, "warning": logging.WARN, "error": logging.ERROR, "critical": logging.CRITICAL}
commands = [
(['dpkg-scanpackages', '.'], "Packages"), # Generate Packages file
(['dpkg-scansources', '.'], "Sources"), # Generate Sources file
(['apt-ftparchive', 'release', '.'], "Release") # Generate Release file
]
# Generate files
for command in commands:
_gen_file(o, command[0], command[-1], temp_path)
# Remove temp directory
shutil.rmtree(temp_path)
ap.add_argument("-b", "--build-dir", default=build_dir, help="build directory (%(default)s)", metavar="DIR")
ap.add_argument("-p", "--pub", default=None, help="pub directory %(default)s", metavar="DIR")
ap.add_argument("--logging", action="store", choices=list(log_levels.keys()), default="info", help="Logging level")
ap.add_argument("--build-deb", action="store_true")
ap.add_argument("--build-rpm", action="store_true")
ap.add_argument("--build-tgz", action="store_true")
ap.add_argument("--build-win", action="store_true")
if not o.no_debsign:
# Generate Release.gpg (= signed Release)
# Options -abs: -a (Create ASCII armored output), -b (Make a detach signature), -s (Make a signature)
subprocess.call(['gpg', '--default-key', GPGID, '--passphrase', GPGPASSPHRASE, '--yes', '-abs', '--no-tty', '-o', 'Release.gpg', 'Release'], cwd=os.path.join(o.pub, 'deb'))
#---------------------------------------------------------
# Generates an RPM repo
#---------------------------------------------------------
def gen_rpm_repo(o, file_name):
# Sign the RPM
rpmsign = pexpect.spawn('/bin/bash', ['-c', 'rpm --resign %s' % file_name], cwd=os.path.join(o.pub, 'rpm'))
rpmsign.expect_exact('Enter pass phrase: ')
rpmsign.send(GPGPASSPHRASE + '\r\n')
rpmsign.expect(pexpect.EOF)
# Removes the old repodata
subprocess.call(['rm', '-rf', os.path.join(o.pub, 'rpm', 'repodata')])
# Copy files to a temp directory (required because the working directory must contain only the
# files of the last release)
temp_path = tempfile.mkdtemp(suffix='rpmPackages')
subprocess.call(['cp', file_name, temp_path])
subprocess.call(['createrepo', temp_path]) # creates a repodata folder in temp_path
subprocess.call(['cp', '-r', os.path.join(temp_path, "repodata"), os.path.join(o.pub, 'rpm')])
# Remove temp directory
shutil.rmtree(temp_path)
#----------------------------------------------------------
# Options and Main
#----------------------------------------------------------
def options():
op = optparse.OptionParser()
root = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
build_dir = "%s-%s" % (root, timestamp)
# Windows VM
ap.add_argument("--vm-winxp-image", default='/home/odoo/vm/win1036/win10_winpy36.qcow2', help="%(default)s")
ap.add_argument("--vm-winxp-ssh-key", default='/home/odoo/vm/win1036/id_rsa', help="%(default)s")
ap.add_argument("--vm-winxp-login", default='Naresh', help="Windows login %(default)s")
ap.add_argument("--vm-winxp-python-version", default='3.6', help="Windows Python version installed in the VM (default: %(default)s)")
log_levels = { "debug" : logging.DEBUG, "info": logging.INFO, "warning": logging.WARN, "error": logging.ERROR, "critical": logging.CRITICAL }
ap.add_argument("-t", "--test", action="store_true", default=False, help="Test built packages")
ap.add_argument("-s", "--sign", action="store_true", default=False, help="Sign Debian package / generate Rpm repo")
ap.add_argument("--no-remove", action="store_true", help="don't remove build dir")
ap.add_argument("--blacklist", nargs="*", help="Modules to blacklist in package")
op.add_option("-b", "--build-dir", default=build_dir, help="build directory (%default)", metavar="DIR")
op.add_option("-p", "--pub", default=None, help="pub directory (%default)", metavar="DIR")
op.add_option("", "--no-testing", action="store_true", help="don't test the built packages")
parsed_args = ap.parse_args()
logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %I:%M:%S', level=log_levels[parsed_args.logging])
parsed_args.odoo_dir = ROOTDIR
return parsed_args
op.add_option("", "--no-debian", action="store_true", help="don't build the debian package")
op.add_option("", "--no-debsign", action="store_true", help="don't sign the debian package")
op.add_option("", "--no-rpm", action="store_true", help="don't build the rpm package")
op.add_option("", "--no-tarball", action="store_true", help="don't build the tarball")
op.add_option("", "--no-windows", action="store_true", help="don't build the windows package")
# Windows VM
op.add_option("", "--vm-winxp-image", default='/home/odoo/vm/win1036/win10_winpy36.qcow2', help="%default")
op.add_option("", "--vm-winxp-ssh-key", default='/home/odoo/vm/win1036/id_rsa', help="%default")
op.add_option("", "--vm-winxp-login", default='Naresh', help="Windows login (%default)")
op.add_option("", "--vm-winxp-python-version", default='3.6', help="Windows Python version installed in the VM (default: %default)")
op.add_option("", "--no-remove", action="store_true", help="don't remove build dir")
op.add_option("", "--logging", action="store", type="choice", choices=list(log_levels.keys()), default="info", help="Logging level")
(o, args) = op.parse_args()
logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %I:%M:%S', level=log_levels[o.logging])
# derive other options
o.odoo_dir = root
o.pkg = join(o.build_dir, 'pkg')
o.work = join(o.build_dir, 'openerp-%s' % version)
o.work_addons = join(o.work, 'odoo', 'addons')
return o
def main():
o = options()
_prepare_build_dir(o)
if not o.no_testing:
_prepare_testing(o)
def main(args):
try:
if not o.no_tarball:
build_tgz(o)
if args.build_tgz:
_prepare_build_dir(args)
docker_tgz = DockerTgz(args)
docker_tgz.build()
try:
if not o.no_testing:
test_tgz(o)
published_files = publish(o, 'tarball', ['tar.gz', 'zip'])
docker_tgz.start_test()
published_files = publish(args, 'tgz', ['tar.gz', 'zip'])
except Exception as e:
logging.error("Won't publish the tgz release.\n Exception: %s" % str(e))
if not o.no_debian:
build_deb(o)
if args.build_rpm:
_prepare_build_dir(args)
docker_rpm = DockerRpm(args)
docker_rpm.build()
try:
if not o.no_testing:
test_deb(o)
published_files = publish(o, 'debian', ['deb', 'dsc', 'changes', 'tar.xz'])
gen_deb_package(o, published_files)
docker_rpm.start_test()
published_files = publish(args, 'rpm', ['rpm'])
if args.sign:
gen_rpm_repo(args, published_files[0])
except Exception as e:
logging.error("Won't publish the deb release.\n Exception: %s" % str(e))
traceback.print_exc()
if not o.no_rpm:
build_rpm(o)
logging.error("Won't publish the rpm release.\n Exception: %s" % str(e))
if args.build_deb:
_prepare_build_dir(args)
docker_deb = DockerDeb(args)
docker_deb.build()
try:
if not o.no_testing:
test_rpm(o)
published_files = publish(o, 'redhat', ['noarch.rpm'])
gen_rpm_repo(o, published_files[0])
docker_deb.start_test()
published_files = publish(args, 'deb', ['deb', 'dsc', 'changes', 'tar.xz'])
gen_deb_package(args, published_files)
except Exception as e:
logging.error("Won't publish the rpm release.\n Exception: %s" % str(e))
if not o.no_windows:
_prepare_build_dir(o, win32=True)
build_exe(o)
logging.error("Won't publish the deb release.\n Exception: %s" % str(e))
if args.build_win:
_prepare_build_dir(args, win32=True)
build_exe(args)
try:
if not o.no_testing:
test_exe(o)
published_files = publish(o, 'windows', ['exe'])
test_exe(args)
published_files = publish(args, 'windows', ['exe'])
except Exception as e:
logging.error("Won't publish the exe release.\n Exception: %s" % str(e))
except Exception as e:
logging.error('Something bad happened ! : {}'.format(e))
traceback.print_exc()
finally:
if o.no_remove:
logging.info('Build dir "{}" not removed'.format(o.build_dir))
if args.no_remove:
logging.info('Build dir "{}" not removed'.format(args.build_dir))
else:
shutil.rmtree(o.build_dir)
logging.info('Build dir %s removed' % o.build_dir)
if not o.no_testing and not (o.no_debian and o.no_rpm and o.no_tarball):
system("docker rm -f `docker ps -a | awk '{print $1 }'` 2>>/dev/null")
logging.info('Remaining dockers removed')
if os.path.exists(args.build_dir):
shutil.rmtree(args.build_dir)
logging.info('Build dir %s removed' % args.build_dir)
if __name__ == '__main__':
main()
args = parse_args()
if os.path.exists(args.build_dir):
logging.error('Build dir "%s" already exists.', args.build_dir)
sys.exit(1)
main(args)
#!/bin/sh
set -e
python3 setup.py install --prefix=/usr --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES --install-lib usr/lib/python3.6/site-packages/
python3 setup.py install --prefix=/usr --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES --install-lib usr/lib/python3.7/site-packages/
......@@ -10,12 +10,6 @@ ODOO_LOG_DIR=/var/log/odoo
ODOO_LOG_FILE=$ODOO_LOG_DIR/odoo-server.log
ODOO_USER="odoo"
if [ -d /usr/lib/python3.7 ]; then
SITE_PACK_DIR37=/usr/lib/python3.7/site-packages
[[ ! -d ${SITE_PACK_DIR37} ]] && mkdir -p ${SITE_PACK_DIR37}
ln -s /usr/lib/python3.6/site-packages/odoo ${SITE_PACK_DIR37}/odoo
fi
if ! getent passwd | grep -q "^odoo:"; then
groupadd $ODOO_GROUP
adduser --system --no-create-home $ODOO_USER -g $ODOO_GROUP
......@@ -34,7 +28,7 @@ db_host = False
db_port = False
db_user = $ODOO_USER
db_password = False
addons_path = /usr/lib/python3.6/site-packages/odoo/addons
addons_path = /usr/lib/python3.7/site-packages/odoo/addons
" > $ODOO_CONFIGURATION_FILE
chown $ODOO_USER:$ODOO_GROUP $ODOO_CONFIGURATION_FILE
chmod 0640 $ODOO_CONFIGURATION_FILE
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment