# HG changeset patch # User Vincent Hatakeyama <vincent.hatakeyama@xcg-consulting.fr> # Date 1679502616 -3600 # Wed Mar 22 17:30:16 2023 +0100 # Node ID c78b0972b6f24b85fab58d41bd301975d6ecd078 # Parent 1ad60f46050d62e0377b74b074aa36cef5c5db54 ✨ add redis star. add start redis option and make it usable for flanker and session_redis diff --git a/NEWS.rst b/NEWS.rst --- a/NEWS.rst +++ b/NEWS.rst @@ -2,6 +2,11 @@ History ======= +20.1.0 +------ + +docker_dev_start: Add ability to run a redis container. + 20.0.1 ------ diff --git a/odoo_scripts/docker_client.py b/odoo_scripts/docker_client.py --- a/odoo_scripts/docker_client.py +++ b/odoo_scripts/docker_client.py @@ -4,6 +4,7 @@ import os import re import tarfile +from functools import partial from multiprocessing import Process from typing import Any, Dict, List, Mapping, Optional, Pattern, Tuple @@ -331,3 +332,157 @@ ) ) return mounts + + +class DockerService: + """Represent a service in a docker and allows common methods to manipulate it.""" + + def __init__( + self, + docker_client: docker.client.DockerClient, + name: str, + repository: str, + tag: str, + stop_remove_if_exists: bool, + immediate_pull: bool = False, + attach: bool = True, + stream: bool = True, + stop_at_exit: bool = True, + force_pull: bool = False, + ): + """ + :param docker_client: instance of docker client + :param name: name for the container + :param repository: repository to use for the image + :param tag: repository tag to use for the image + :param immediate_pull: pull now (rather than do it later, like during start) + :param stream: if not attached, indicate to stream output or not + """ + super().__init__() + self.docker_cli = docker_client + self.name = name + self.repository: str = repository + self.tag = tag + self.base_image = f"{self.repository}:{self.tag}" + if immediate_pull: + self.pull_image() + self.attach = attach + self.stream = stream + self.stop_at_exit = stop_at_exit + # Retrieve the container if it already exists + self._container: Optional[docker.models.containers.Container] + try: + self._container = self.docker_cli.containers.get(self.name) + except docker.errors.NotFound: + self._container = None + self._log_process: Optional[Process] = None + self.stop_remove_if_exists = stop_remove_if_exists + self.force_pull: bool = force_pull + + def start(self) -> bool: + """Start the container""" + # TODO avoid pull if already done with immediate pull + self.pull_image() + if self._container is not None: + # TODO should it crash? + _logger.info("Container %s already exist", self.name) + if self.stop_remove_if_exists: + # stop and remove + self.stop_and_remove() + elif not self.is_running: + _logger.info("Container %s is not running", self.name) + self.remove() + else: + return True + self._start() + if self.stop_at_exit: + atexit.register(partial(self.stop_and_remove, False)) + if self.attach: + DockerClient.print_log(self._container) + elif self.stream: + process = Process(target=DockerClient.print_log, args=(self._container,)) + process.start() + self._log_process = process + + return True + + def _create_param(self) -> dict: + return dict( + name=self.name, + network_mode="host", + image=self.base_image, + ) + + def _start(self) -> None: + self._container = self.docker_cli.containers.create(**self._create_param()) + _logger.info("Starting container %s", self.name) + self._container.start() + + def stop_and_remove(self, expect_running: bool = True) -> None: + """Stop and remove the container""" + try: + self.stop(expect_running) # call stop func + self.remove() # call remove func + except docker.errors.NotFound as exception: + _logger.info("Stop and remove container, got: %s", exception) + raise exception + + def stop(self, expect_running: bool = True) -> None: + """Stop service container.""" + try: + if not self.is_running: + if expect_running: + _logger.warning("Container %s is not running", self.name) + elif self._container is not None: + _logger.info("Stopping %s container", self.name) + self._container.stop() + self._container.wait() + except docker.errors.NotFound as exception: + _logger.error("Stopping container %s, got: %s", self.name, exception) + raise + self.join_log_process() + + def stop_log_process(self): + if self._log_process and self._log_process.is_alive(): + self._log_process.terminate() + + def join_log_process(self): + if self._log_process and self._log_process.is_alive(): + _logger.info("Waiting for log process to finish") + self._log_process.join() + + def remove(self): + """Remove the service container created""" + try: + _logger.info("Removing %s container", self.name) + self._container.remove() + except docker.errors.NotFound as exception: + _logger.error("Removing container %s, got: %s", self.name, exception) + raise exception + + def pull_image(self): + """Pulls image if needed or forced.""" + if self.force_pull or not self.docker_cli.images.list(self.base_image): + _logger.info("Pulling %s", self.base_image) + self.docker_cli.images.pull(repository=self.repository, tag=self.tag) + + @property + def is_running(self) -> bool: + """Verify the container status + :return: true if the status is running + """ + if self._container is None: + return False + result = self.docker_cli.containers.list(filters={"id": self._container.id}) + return result and result[0].status == "running" + + @property + def is_exited(self) -> bool: + """ + Verify the rednerd container status + :return: Boolean if the status is exited + """ + if self._container is None: + return True + result = self.docker_cli.containers.list(filters={"id": self._container.id}) + return result and result[0].status == "exited" diff --git a/odoo_scripts/docker_dev_start.py b/odoo_scripts/docker_dev_start.py --- a/odoo_scripts/docker_dev_start.py +++ b/odoo_scripts/docker_dev_start.py @@ -46,6 +46,7 @@ from .docker_postgresql import POSTGRES_PASSWORD, docker_run_postgresql from .docker_py3o import start_py3o from .docker_pylint import apply_pylint, parser_add_pylint_group +from .docker_redis import DockerRedis from .docker_rednerd import DockerRednerd from .parsing import apply, basic_parser @@ -53,9 +54,9 @@ _logger = logging.getLogger(__name__) -__version__ = "3.10.0" +__version__ = "3.11.0" __date__ = "2017-08-11" -__updated__ = "2023-02-27" +__updated__ = "2023-03-22" def __parser(project_name: str) -> ArgumentParser: @@ -336,6 +337,23 @@ "%(default)s]", default="redner-odoo-password", ) + redis_group = parser.add_mutually_exclusive_group() + redis_group.add_argument( + "--start-redis", + help="start redis docker [default: %(default)s]", + action="store_const", + default=None, + const=1, + dest="start_redis", + ) + redis_group.add_argument( + "--no-start-redis", + help="do not start redis docker [default: %(default)s]", + action="store_const", + default=None, + const=-1, + dest="start_redis", + ) parser.add_argument( "--odoo-sources", @@ -495,6 +513,9 @@ else: start_rednerd_stack = config.start_rednerd _logger.debug("No command line start rednerd %d", start_rednerd_stack) + if nmspc.start_redis: + start_redis = nmspc.start_redis == 1 + # TODO ajouter l’option de démarrer redis dans Configuration _logger.debug("Docker image: %s", image) # detect if docker image already exists @@ -714,7 +735,7 @@ "/usr/local/lib/python2.7/dist-packages/" "openerp-7.0-py2.7.egg/openerp/data" ) - filestore_volume_name = "{}{}".format(base_data_volume_name, "_filestore") + filestore_volume_name = "{}_filestore".format(base_data_volume_name) client.create_volume(filestore_volume_name, {"mounted in": target}) mounts.append(Mount(target, filestore_volume_name)) else: @@ -1105,6 +1126,28 @@ cursor, redner_user, redner_docker.api_key, server_url ) + # --- Redis start --- + + redis_docker: Optional[DockerRedis] = None + if start_redis: + # TODO stop at exit à vrai sinon s’éteint pas? + redis_docker = DockerRedis(DockerClient.client, project_name, attach=False) + redis_docker.start() + # bind the socket dir in Odoo + mounts.append( + Mount(redis_docker.socket_dir, redis_docker.socket_dir, type="bind") + ) + options["environment"].update( + # Variables used by flanker. + REDIS_PORT="0", + REDIS_DB="0", + # Rely on a flanker patch to work. + REDIS_UNIX_SOCKET_PATH=redis_docker.socket_path, + # Variables used by session_redis + ODOO_SESSION_REDIS="1", + ODOO_SESSION_REDIS_URL=f"unix://{redis_docker.socket_path}?db=1", + ) + # volume magic mount_list, _ = modules_mount(config) if mount_list: @@ -1331,6 +1374,10 @@ # only remove after getting the return code odoo_container.remove() + # Stop redis (python does not exit as the log process is still running) + if redis_docker: + _logger.info("Stopping redis") + redis_docker.stop() return result diff --git a/odoo_scripts/docker_redis.py b/odoo_scripts/docker_redis.py new file mode 100644 --- /dev/null +++ b/odoo_scripts/docker_redis.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +"""Run a redis docker with volume based on project name. +""" +import argparse +import logging +import os +import sys +import tempfile +import time +from typing import List, Optional + +from docker.types import Mount # type: ignore[import] + +from .docker_client import DockerClient, DockerService +from .parsing import apply, basic_parser + +__version__ = "1.0.0" +__date__ = "2023-03-20" +__updated__ = "2023-03-22" + +_logger = logging.getLogger(__name__) + + +class DockerRedis(DockerService): + """Redis service""" + + def __init__( + self, + docker_client, + project_name: str, + attach: bool = True, + stop_at_exit: bool = True, + ): + super().__init__( + docker_client, + f"redis_{project_name}", + "redis", + "latest", + True, + attach=attach, + stop_at_exit=stop_at_exit, + ) + self.socket_dir = tempfile.mkdtemp(prefix="odoo_scripts_redis_socket-") + """Redis socket’s directory.""" + os.chmod(self.socket_dir, 0o777) + self.socket_path = os.path.join(self.socket_dir, "redis.sock") + + def _create_param(self) -> dict: + result = super()._create_param() + target = "/data" + result["mounts"] = [ + Mount( + target, + DockerClient.create_volume( + f"{self.name}{target.replace('/', '_')}", {"mounted in": target} + )[0].name, + "volume", + ), + Mount("/var/run/redis", self.socket_dir, "bind"), + ] + result["command"] = [ + "redis-server", + "--save", + "--unixsocket", + "/var/run/redis/redis.sock", + "--port", + "0", + ] + return result + + def start(self) -> bool: + result = super().start() + # by default the socket does not have permissions that will make it easy to use + # (by odoo for example). User is 999, so that does not help much either. + # Let redis start (alternative would be to run a command that check that the + # socket exists, but it needs to be done inside the container due to permission + # issue) + time.sleep(0.01) + DockerClient.exec( + self._container, ["chmod", "777", "/var/run/redis/redis.sock"], "redis" + ) + return result + + def _start(self) -> None: + super()._start() + if os.path.exists("/proc/sys/vm/overcommit_memory"): + with open("/proc/sys/vm/overcommit_memory", "r") as f: + data = f.read() + if data == "0\n": + _logger.info( + "The warning in redis about memory overcommit applies to the " + "host system" + ) + + def __del__(self): + os.rmdir(self.socket_dir) + + +def __parser() -> argparse.ArgumentParser: + program_version_message = f"%(prog)s {__version__} ({__updated__})" + program_short_description = __doc__.split(".")[0] + program_license = f"""{program_short_description} + + Created by Vincent Hatakeyama on {__date__}. + Copyright 2023 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 + """ + parser = basic_parser(program_license, program_version_message) + project_path = os.path.realpath(".") + project_name = os.path.basename(project_path) + parser.add_argument( + "--name", + help="Container name to use [default: %(default)s]", + default=project_name, + ) + return parser + + +def main(argv: Optional[List[str]] = None) -> int: + parser = __parser() + namespace = parser.parse_args(argv) + apply(namespace) + docker_redis = DockerRedis(DockerClient.client, namespace.name) + docker_redis.start() + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/odoo_scripts/docker_rednerd.py b/odoo_scripts/docker_rednerd.py --- a/odoo_scripts/docker_rednerd.py +++ b/odoo_scripts/docker_rednerd.py @@ -4,7 +4,6 @@ Documentation: http://127.0.0.1:12345/docs if the server is started with port 12345 """ import argparse -import atexit import logging import os import sys @@ -20,7 +19,7 @@ from docker.types import Mount # type: ignore[import] from .config import Config -from .docker_client import DockerClient +from .docker_client import DockerClient, DockerService from .parsing import apply, basic_parser _logger = logging.getLogger(__name__) @@ -34,7 +33,7 @@ SERVE = "serve" -class DockerRednerd: +class DockerRednerd(DockerService): """ This class allow to start the rednerd server """ @@ -86,14 +85,15 @@ ): assert argument is not None, "Missing required argument %s" % i # db_password can be none, in that case expect that any password would work - super().__init__() - self.name = f"rednerd_{project_name}" - self.repository: str = repository - self.version = version - self.attach = attach - self.base_image = f"{self.repository}:{self.version}" - self.stop_at_exit = stop_at_exit - self.docker_cli = docker_client + super().__init__( + docker_client, + f"rednerd_{project_name}", + repository, + version, + False, + attach, + stop_at_exit, + ) self.redner_user = redner_user self.redner_password = redner_password self.redner_admin_password = redner_admin_password @@ -119,27 +119,12 @@ ).name """Rednerd socket’s directory.""" self.socket_path = os.path.join(self.socket_dir, "rednerd.sock") - - # Retrieve the container if it already exists - try: - self.container = self.docker_cli.containers.get(self.name) - except docker.errors.NotFound: - self.container = None self.force_pull = force_pull self.api_key = None """Store the API key""" - self._log_process: Optional[Process] = None - def start(self) -> bool: + def _start(self) -> None: """Start rednerd container""" - if self.force_pull or not self.docker_cli.images.list(self.base_image): - _logger.info("Pulling %s", self.base_image) - self.pull_image() - if self.container is not None: - # probably to make it work as a restart - _logger.info("Container %s already exist", self.name) - # stop and remove - self.stop_and_remove() # base parameters for container creation ports: Dict[str, str] = {} volumes: Dict[str, Dict[str, str]] = {} @@ -158,7 +143,7 @@ _logger.info("migrate") DockerClient.run( repository=self.repository, - tag=self.version, + tag=self.tag, run_kwargs=dict(create_param, command=[MIGRATE]), ) # setup admin password if provided @@ -166,7 +151,7 @@ _logger.info("admin-password") DockerClient.run( repository=self.repository, - tag=self.version, + tag=self.tag, run_kwargs=dict( create_param, command=[ADMIN_PASSWORD, self.redner_admin_password] ), @@ -194,10 +179,10 @@ "bind": self.socket_dir, "mode": "rw", } - self.container = self.docker_cli.containers.create(**create_param) + self._container = self.docker_cli.containers.create(**create_param) _logger.info("Starting container %s", self.name) _logger.debug(create_param) - self.container.start() + self._container.start() session: requests.sessions.Session base_url: str if hasattr(self, "socket_path"): @@ -208,7 +193,7 @@ base_url = f"http://localhost:{self.redner_port}" # if there is neither port or socket, no session nor base_url if not self.attach: - process = Process(target=DockerClient.print_log, args=(self.container,)) + process = Process(target=DockerClient.print_log, args=(self._container,)) process.start() self._log_process = process # wait for redner to be started @@ -233,7 +218,7 @@ if hasattr(self, "socket_path"): _logger.info("Changing socket permission") DockerClient.exec( - self.container, ["chmod", "-R", "777", self.socket_path], "root" + self._container, ["chmod", "-R", "777", self.socket_path], "root" ) if ( @@ -311,77 +296,6 @@ ) self.api_key = request.json()["value"] - if self.stop_at_exit: - atexit.register(self.stop_and_remove) - - if self.attach: - DockerClient.print_log(self.container) - - return True - - def stop_and_remove(self): - """Stop and remove the rednerd container""" - try: - self.stop() # call rednerd stop func - self.remove() # call rednerd remove func - except docker.errors.NotFound as exception: - _logger.info("Stop and remove rednerd container, got: %s", exception) - raise exception - - def stop(self): - """ - Stop a rednerd container. - """ - try: - if not self.is_running: - _logger.warning("Container %s is not running", self.name) - _logger.info("Stopping %s container", self.name) - self.container.stop() - self.container.wait() - except docker.errors.NotFound as exception: - _logger.error("Stopping Redner container, got: %s", exception) - raise - if self._log_process and self._log_process.is_alive(): - _logger.info("Waiting for log process to finish") - self._log_process.join() - - def remove(self): - """Remove the rednerd container created""" - try: - _logger.info("Removing %s container", self.name) - self.container.remove() - except docker.errors.NotFound as exception: - _logger.error("Removing rednerd container, got: %s", exception) - raise exception - - def pull_image(self): - """ - Pulls rednerd image - """ - self.docker_cli.images.pull(repository=self.repository, tag=self.version) - - @property - def is_running(self): - """ - Verify the rednerd container status - :return: Boolean if the status is running - """ - if self.container is None: - return False - result = self.docker_cli.containers.list(filters={"id": self.container.id}) - return result and result[0].status == "running" - - @property - def is_exited(self): - """ - Verify the rednerd container status - :return: Boolean if the status is exited - """ - if self.container is None: - return True - result = self.docker_cli.containers.list(filters={"id": self.container.id}) - return result and result[0].status == "exited" - def __parser() -> argparse.ArgumentParser: """Return a parser for docker_rednerd""" diff --git a/pyproject.toml b/pyproject.toml --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,7 @@ odoo_conf_inject_env_var = "odoo_scripts.odoo_conf_inject_env_var:main" list_modules = "odoo_scripts.list_modules:main" update_duplicate_sources = "odoo_scripts.update_duplicate_sources:main [source_control]" +docker_redis = "odoo_scripts.docker_redis:main [docker]" [project.urls] repository = "https://orus.io/xcg/odoo_scripts"