Commit a42bc79e authored by Alexis PASQUIER's avatar Alexis PASQUIER
Browse files

Merge branch 'prepare-2.0.0' into 'master'

Prepare 2.0.0

See merge request !3
parents 984655eb a6797e4b
Pipeline #81823 failed with stages
in 1 minute and 39 seconds
include:
- project: odoo-addons/ci-files
ref: v2
file: pipeline/python-lib.yml
file:
- pipeline/python-lib.yml
- docker/build.yml
variables:
SUPPORT_PYTHON_VERSION: "3.8;3.9;10;latest"
stages:
- test
- build
- test_build
- deploy
#test config:14:
# image:
# name: registry.ndp-systemes.fr/dockers/odoo-cloud:14.0
# entrypoint: [""]
# stage: test
# script:
# - ./run_test.sh
# tags:
# - runbot2
#build auto Dockerfile 14:sha1:
# extends: .base_build_image_Dockerfile_sha1
# needs: ["test config:14"]
# before_script:
# - !reference [".base_build_image_Dockerfile_sha1", "before_script"]
# - ls
# - ./create_Dockerfile.py registry.ndp-systemes.fr/dockers/odoo-cloud:14.0
# rules:
# - exists:
# - create_Dockerfile.py
#
#runbot:14:
# stage: test_build
# when: on_success
# variables:
# ODOO_IMAGE_NAME: "registry.ndp-systemes.fr/dockers/odoo"
# USE_ADDONS_GIT: "True"
# VAR_ODOO_IMAGE_TAG: "CI_COMMIT_SHORT_SHA"
# ADDONS_GIT_COMMON_MODULES: "odoo-addons/blank" # Replace common by this project, keep community
# BEFORE_ODOO_MODULE: "stock"
# ODOO_MODULE: "sale,purchase"
# image:
# name: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
# entrypoint: [""]
# script:
# - alfred-runbot --start
# tags:
# - runbot2
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10" project-jdk-type="Python SDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/odoo_launcher.iml" filepath="$PROJECT_DIR$/odoo_launcher.iml" />
</modules>
</component>
</project>
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
......@@ -3,6 +3,7 @@ repos:
rev: v4.3.0
hooks:
- id: check-yaml
exclude: .gitlab-ci.yml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/quwac/pyproject-autoflake
......
......@@ -5,11 +5,11 @@
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/odoo_launcher" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/dist" />
<excludeFolder url="file://$MODULE_DIR$/odoo_launcher.egg-info" />
<excludeFolder url="file://$MODULE_DIR$/dist" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="jdk" jdkName="Python 3.9" jdkType="Python SDK" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
from __future__ import unicode_literals
import logging
import os
import sys
from . import laucher # noqa
from . import api, config_section, mapper # noqa
from . import launcher # noqa
from . import api # noqa
from . import config_section, mapper # noqa
from .odoo_config_maker import OdooConfig, OdooEnvConverter # noqa
_logger = logging.getLogger(__name__)
# _logger.setLevel(logging.DEBUG)
def main():
env_vars = dict(os.environ)
_logger.info("create config")
odoo_path = os.getenv("ODOO_PATH")
server_path = os.getenv("NDP_SERVER_PATH")
odoo_rc = os.getenv("ODOO_RC")
launcher = laucher.Launcher(sys.argv[1:], odoo_path=odoo_path, odoo_rc=odoo_rc, server_path=server_path)
launcher.init_addons(env_vars)
return_code = launcher.launch_config_file(env_vars).wait()
if return_code:
sys.exit(return_code)
if os.getenv("UPDATE") or os.getenv("INSTALL"):
_logger.info("Update or init detected")
maintenance_server_proc = launcher.launch_maintenance_server()
return_code = launcher.launch_update(env_vars).wait()
maintenance_server_proc.kill()
if return_code:
sys.exit(return_code)
_logger.info("#############################################")
_logger.info("Run Odoo")
sys.exit(launcher.launch({}).wait())
if __name__ == "__main__":
main()
import dataclasses
import logging
import os
import pprint
import random
import string
import subprocess
import sys
from . import launcher
_logger = logging.getLogger(__name__)
def get_random_string(length):
return "".join(random.choice(string.ascii_lowercase) for _ in range(length))
@dataclasses.dataclass
class _Config:
mode: launcher.OdooMode
config: str
odoo_path: str
def _main_parse_args() -> _Config:
custom_mode = (os.getenv("ODOO_SERVER_MODE") or "").upper()
if not custom_mode or custom_mode not in launcher.OdooMode.__members__.keys():
custom_mode = launcher.OdooMode.ODOO.name
mode = launcher.OdooMode[custom_mode]
config = os.getenv("ODOO_RC") or os.path.join("/tmp", "odoo_config_%s.ini" % get_random_string(5))
odoo_path = os.getenv("ODOO_PATH", "/odoo")
return _Config(mode=mode, config=config, odoo_path=odoo_path)
def _report_env(env):
_logger.debug("################## ENVIRON ###########################")
_logger.debug(pprint.pformat(env))
_logger.debug("################## ENVIRON ###########################")
def main() -> int:
environ = dict(os.environ)
config = _main_parse_args()
_logger.info("Starting Odoo in %s mode", config.mode.value)
_logger.info("#############################################")
_logger.info("1/ Install extra addons")
launcher.AddonsInstallerLauncher(odoo_path=config.odoo_path, odoo_rc=config.config).run(environ)
# _logger.info("#############################################")
# _logger.info("2/ Create config file %s", config.config)
# to_launch = launcher.ConfigLauncher(odoo_path=config.odoo_path, odoo_rc=config.config)
# return_code = to_launch.run(environ)
# if return_code:
# return return_code
if config.mode == launcher.OdooMode.CONFIG:
launcher.ConfigPrinterLauncher(odoo_path=config.odoo_path, odoo_rc=config.config).run(environ)
return 0
if config.mode == launcher.OdooMode.TESTS:
maintenance_server_proc = subprocess.Popen([sys.executable, "-m", "maintenance_server"])
to_launch = launcher.TestLauncher(odoo_path=config.odoo_path, odoo_rc=config.config)
return_code = to_launch.run(environ)
maintenance_server_proc.kill()
if return_code:
return return_code
if config.mode == launcher.OdooMode.WEB:
return launcher.WebLauncher(odoo_path=config.odoo_path, odoo_rc=config.config).run(environ)
if config.mode == launcher.OdooMode.UPDATE:
maintenance_server_proc = subprocess.Popen([sys.executable, "-m", "maintenance_server"])
to_launch = launcher.UpdateLauncher(odoo_path=config.odoo_path, odoo_rc=config.config)
return_code = to_launch.run(environ)
maintenance_server_proc.kill()
if return_code:
return return_code
if config.mode == launcher.OdooMode.INSTALL:
maintenance_server_proc = subprocess.Popen([sys.executable, "-m", "maintenance_server"])
to_launch = launcher.InstallLauncher(odoo_path=config.odoo_path, odoo_rc=config.config)
return_code = to_launch.run(environ)
maintenance_server_proc.kill()
if return_code:
return return_code
if config.mode == launcher.OdooMode.CONSOLE:
_logger.info("#############################################")
_logger.info("NOT SUPPORTED, SORRY :-(")
_logger.info("#############################################")
return 1
if __name__ == "__main__":
handler = logging.StreamHandler()
_logger.addHandler(handler)
_logger.setLevel(logging.DEBUG)
sys.exit(main())
from __future__ import annotations
import abc
import logging
import os
import subprocess
import sys
from collections import OrderedDict
from typing import Any, Dict, List, Optional, Union
from os import path
from typing import Any, Dict, List, Optional, Set, TypeVar, Union
if sys.version_info[0] == 2: # Python 2
from typing_extensions import Self
class ABC(object):
__meta_class__ = abc.ABCMeta
__slots__ = ()
_logger_level = getattr(logging, os.environ.get("NDP_SERVER_LOG_LEVEL", "INFO"), logging.INFO)
else:
ABC = abc.ABC
_logger = logging.getLogger(__name__)
_logger.setLevel(logging.DEBUG)
_logger.addHandler(logging.StreamHandler())
class Dictable(ABC):
def to_dict(self):
# type: () -> Dict[str, Any]
raise NotImplementedError()
class Dictable(abc.ABC):
@abc.abstractmethod
def to_dict(self) -> Dict[str, Any]:
...
@staticmethod
def clean_config_dict(values):
# type: (Union[Dict[str, Any], Dictable]) -> Dict[str, str]
def clean_config_dict(values: Union[Dict[str, Any], Dictable]) -> Dict[str, str]:
new_values = OrderedDict()
if isinstance(values, Dictable):
values = values.to_dict()
......@@ -30,13 +34,12 @@ class Dictable(ABC):
value = Dictable.clean_config_dict(value)
elif isinstance(value, (list, tuple, set)):
value = ",".join([str(x) for x in value]) or ""
if value and value is not None and not isinstance(value, dict):
if value is not False and value is not None and not isinstance(value, dict):
new_values[key] = str(value)
return new_values
@staticmethod
def clean_none_env_vars(dict_value):
# type: (Union[Dict[str, Any], Dictable]) -> Dict[str, Any]
def clean_none_env_vars(dict_value: Union[Dict[str, Any], Dictable]) -> Dict[str, Any]:
result = OrderedDict()
if isinstance(dict_value, Dictable):
dict_value = dict_value.to_dict()
......@@ -47,15 +50,15 @@ class Dictable(ABC):
return result
class ConfigConvert(ABC):
def is_true(self, any):
# type: (Union[str, bool, int, None]) -> bool
class ConfigConvert:
@staticmethod
def is_true(any: Union[str, bool, int, float, None]) -> bool:
if not any or not isinstance(any, (str, bool, int)):
return False
return bool(any) and (str(any).isdigit() and bool(int(any))) or (str(any).capitalize() == str(True)) or False
def to_int(self, any):
# type: (Union[str, bool, int, None]) -> int
@staticmethod
def to_int(any: Union[str, bool, int, float, None]) -> int:
if not any or not isinstance(any, (str, bool, int)):
return 0
if isinstance(any, str):
......@@ -63,13 +66,19 @@ class ConfigConvert(ABC):
return int(any)
class EnvMapper(ConfigConvert):
def map_vars(self, env_vars):
# type: (Dict[str, str]) -> Dict[str, str]
raise NotImplementedError()
class EnvMapper(ConfigConvert, abc.ABC):
_auto_register = True
def get_value(self, env_vars, possible_value, default=None):
# type: (Dict[str, str], List[str], str) -> Optional[str]
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if getattr(cls, "_auto_register", True):
Registry.mappers.add(cls)
@abc.abstractmethod
def map_vars(self, env_vars: Dict[str, str]) -> Dict[str, str]:
...
def get_value(self, env_vars: Dict[str, str], possible_value: List[str], default: str = None) -> Optional[str]:
for value in possible_value:
res = env_vars.get(value)
if res:
......@@ -77,24 +86,172 @@ class EnvMapper(ConfigConvert):
return default
class OdooConfigABC(ConfigConvert, ABC):
class OdooConfigABC(ConfigConvert, abc.ABC):
def __init__(self, main_instance=True):
super(OdooConfigABC, self).__init__()
self.main_instance = main_instance
@property
def odoo_version(self):
raise NotImplementedError
@abc.abstractmethod
def odoo_version(self) -> int:
...
def get_odoo_version(env: Dict[str, Any]) -> int:
return ConfigConvert.to_int(env.get("ODOO_VERSION"))
def is_main_instance(env: Dict[str, Any]) -> bool:
return ConfigConvert.to_int(env.get("INSTANCE_NUMER")) == 0
OdooCliFlag = Dict[str, Any]
class OdooConfigSection(ConfigConvert, Dictable, abc.ABC):
_auto_register = True
class OdooConfigSection(ConfigConvert, Dictable, ABC):
def __init__(self, odoo_config_maker, env_vars):
# type: (OdooConfigABC, Dict[str, Union[str, bool, int, None]]) -> None
def __init__(self, odoo_config_maker: OdooConfigABC) -> None:
self.config_maker = odoo_config_maker
self.enable = True
self.disable_dict = {}
def to_dict(self):
# type: () -> Dict[str, Any]
def to_dict(self) -> Dict[str, Any]:
if not self.enable:
return {}
return self.get_info()
return self.disable_dict
return self._to_flag()
@abc.abstractmethod
def _to_flag(self) -> OdooCliFlag:
...
@abc.abstractmethod
def populate_from_env(self, env_vars: Dict[str, Union[str, bool, int, None]]) -> Self:
...
def __repr__(self) -> str:
desc = f"{str(self.__class__)}#{id(self)}"
params = f"{self.enable}, {', '.join([str(i[0]) + '=' + str(i[1]) for i in self.to_dict().items()])}"
return f"{desc}({params})"
def __init_subclass__(cls: OdooConfigSection, **kwargs):
super().__init_subclass__(**kwargs)
if getattr(cls, "_auto_register", True):
Registry.sections.add(cls)
class ToOdooArgs(abc.ABC):
@abc.abstractmethod
def to_odoo_args(self) -> List[str]:
...
@staticmethod
def dicatable_to_args(*dictables: Dictable) -> ToOdooArgs:
result = _ListToOdooArgs()
for dictable in dictables:
dict_values = Dictable.clean_none_env_vars(dictable.to_dict())
dict_values = Dictable.clean_config_dict(dict_values)
for key, value in dict_values.items():
if not value:
continue
if value == str(True):
result.append(key)
else:
result.append("%s=%s" % (key, value))
return result
class _ListToOdooArgs(ToOdooArgs, list):
def to_odoo_args(self) -> List[str]:
return list(self)
DEFAULT_ODOO_CLI = "ndpserver"
TYPE_LAUNCHER = TypeVar("TYPE_LAUNCHER", bound="AbstractLauncher")
class AbstractLauncher(object):
_auto_register: bool = True
_server_mode: str
_odoo_cli: str = DEFAULT_ODOO_CLI
_timeout: Optional[int] = None
server_path = path.join(path.dirname(__file__), "odoo_modules")
depends: List[TYPE_LAUNCHER] = []
def __init__(self, *, odoo_path: str = None, odoo_rc: str = None):
self.odoo_path = path.abspath(path.expanduser(odoo_path))
self.odoo_rc = path.abspath(path.expanduser(odoo_rc))
self.ensure_path()
def ensure_path(self):
if not path.exists(self.odoo_path) or not path.exists(path.join(self.odoo_path, "odoo-bin")):
raise FileNotFoundError("File 'odoo-bin' not found in %s" % self.odoo_path)
if not path.exists(self.server_path):
raise FileNotFoundError("Directory '%s' not exist" % self.server_path)
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if getattr(cls, "_auto_register", True):
Registry.launcher.add(cls)
def normalize_args(self, config: ToOdooArgs) -> List[str]:
"""
return as a list a valid subprocess.Popen command.
This commande launch Odoo with the custom command provide by `odoo_cli`
Args:
odoo_cli: The odoo cli, like configcreate, customserver, customshell and other
config: The odoo config to convert to odoo cli arguments
Return:
The argument for Popen
"""
return self._get_python_server_execute() + config.to_odoo_args()
def execute_odoo_cli(self, config: Optional[ToOdooArgs]) -> subprocess.Popen:
return self._execute_popen(self.normalize_args(config))
def _get_python_server_execute(self):
cmd_args = [sys.executable, path.join(self.odoo_path, "odoo-bin")]
if self._odoo_cli:
cmd_args.append("--addons-path=%s" % self.server_path)
cmd_args.append(self._odoo_cli)
return cmd_args
@staticmethod
def _execute_popen(cmd: List[str]) -> subprocess.Popen:
_logger.info("Run -> %s", " ".join([str(s) for s in cmd]))
return subprocess.Popen(cmd)
@abc.abstractmethod
def get_config(self, env: Dict[str, str]) -> Optional[ToOdooArgs]:
...
def run_without_config(self, cli: str, *, env: Dict[str, str]) -> int:
return 0
def run(self, env: Dict[str, str]) -> int:
if self.depends:
_logger.info("Run depends of %s", self)
for depend_type in self.depends:
depend = depend_type(odoo_path=self.odoo_path, odoo_rc=self.odoo_rc)
depend.run(env)
config = self.get_config(env)
if not config or not config.to_odoo_args():
_logger.info("Run without_config %s", self)
return self.run_without_config(self._odoo_cli, env=env)
return self.execute_odoo_cli(config).wait(self._timeout)
TYPE_SECTION = TypeVar("TYPE_SECTION", bound=OdooConfigSection)
TYPE_MAPPER = TypeVar("TYPE_MAPPER", bound=EnvMapper)
class _Registry:
def __init__(self):
self.sections: Set[TYPE_SECTION] = set()
self.mappers: Set[TYPE_MAPPER] = set()
self.launcher: Set[TYPE_LAUNCHER] = set()
Registry = _Registry()
This diff is collapsed.
class OdooLauncherException(Exception):
pass
class MissingDBCredential(OdooLauncherException):
def __init__(self) -> None:
super(MissingDBCredential, self).__init__()
self.msg = """Can't start Odoo without a db name
Please add the one of the following environment variable
- DATABASE
- DB_NAME
- POSTGRESQL_ADDON_DB""".lstrip()
from __future__ import unicode_literals
import argparse
import logging
import os
import subprocess
import sys
if sys.version_info >= (3,):
import configparser
else:
import ConfigParser as configparser
import uuid
from typing import Dict, List, Optional
from addons_installer import AddonsInstaller, AddonsRegistry
from .odoo_config_maker import OdooConfig, OdooConfigFileRef, ToOdooArgs
_logger = logging.getLogger("launch")
_logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
_logger.addHandler(handler)
class Launcher(object):
def __init__(self, args, odoo_path=None, odoo_rc=None, server_path=None):
# type: (List[str], Optional[str], Optional[str], Optional[str]) -> Launcher
parser = self.get_parser()
ns, other = parser.parse_known_args(args=args)
odoo_path = ns.odoo_path or odoo_path
assert odoo_path, "No Odoo path is provided"
self.odoo_path = os.path.abspath(os.path.expanduser(odoo_path))
self.other_args = other
odoo_rc = ns.odoo_rc or odoo_rc