diff --git a/dockers/docker-base-bullseye/Dockerfile.j2 b/dockers/docker-base-bullseye/Dockerfile.j2 index 6f124b7cdb..bbf8603468 100644 --- a/dockers/docker-base-bullseye/Dockerfile.j2 +++ b/dockers/docker-base-bullseye/Dockerfile.j2 @@ -81,7 +81,7 @@ RUN pip3 install supervisor==4.2.1 # Add support for supervisord to handle startup dependencies RUN pip3 install supervisord-dependent-startup==1.4.0 -RUN mkdir -p /etc/supervisor /var/log/supervisor +RUN mkdir -p /var/log/supervisor /etc/supervisor/conf.d RUN apt-get -y purge \ exim4 \ @@ -104,10 +104,10 @@ RUN apt-get clean -y && \ apt-get autoremove -y && \ rm -rf /var/lib/apt/lists/* /tmp/* ~/.cache -COPY ["etc/rsyslog.conf", "/etc/rsyslog.conf"] COPY ["etc/rsyslog.d/*", "/etc/rsyslog.d/"] COPY ["root/.vimrc", "/root/.vimrc"] RUN ln /usr/bin/vim.tiny /usr/bin/vim COPY ["etc/supervisor/supervisord.conf", "/etc/supervisor/"] +COPY ["etc/supervisor/containercfgd.conf", "/etc/supervisor/conf.d/"] diff --git a/dockers/docker-base-bullseye/etc/supervisor/containercfgd.conf b/dockers/docker-base-bullseye/etc/supervisor/containercfgd.conf new file mode 100644 index 0000000000..704b5490c3 --- /dev/null +++ b/dockers/docker-base-bullseye/etc/supervisor/containercfgd.conf @@ -0,0 +1,9 @@ +[program:containercfgd] +command=python3 /usr/local/bin/containercfgd +priority=99 +autostart=false +autorestart=unexpected +stdout_logfile=syslog +stderr_logfile=syslog +dependent_startup=true +dependent_startup_wait_for=rsyslogd:running diff --git a/dockers/docker-base-buster/Dockerfile.j2 b/dockers/docker-base-buster/Dockerfile.j2 index 6977b44d34..3b76ef6917 100644 --- a/dockers/docker-base-buster/Dockerfile.j2 +++ b/dockers/docker-base-buster/Dockerfile.j2 @@ -94,7 +94,7 @@ RUN pip3 install supervisor==4.2.1 # Add support for supervisord to handle startup dependencies RUN pip3 install supervisord-dependent-startup==1.4.0 -RUN mkdir -p /etc/supervisor /var/log/supervisor +RUN mkdir -p /var/log/supervisor /etc/supervisor/conf.d RUN apt-get -y purge \ exim4 \ @@ -117,10 +117,10 @@ RUN apt-get clean -y && \ apt-get autoremove -y && \ rm -rf /var/lib/apt/lists/* /tmp/* ~/.cache/ -COPY ["etc/rsyslog.conf", "/etc/rsyslog.conf"] COPY ["etc/rsyslog.d/*", "/etc/rsyslog.d/"] COPY ["root/.vimrc", "/root/.vimrc"] RUN ln /usr/bin/vim.tiny /usr/bin/vim COPY ["etc/supervisor/supervisord.conf", "/etc/supervisor/"] +COPY ["etc/supervisor/containercfgd.conf", "/etc/supervisor/conf.d/"] diff --git a/dockers/docker-base-buster/etc/supervisor/containercfgd.conf b/dockers/docker-base-buster/etc/supervisor/containercfgd.conf new file mode 100644 index 0000000000..704b5490c3 --- /dev/null +++ b/dockers/docker-base-buster/etc/supervisor/containercfgd.conf @@ -0,0 +1,9 @@ +[program:containercfgd] +command=python3 /usr/local/bin/containercfgd +priority=99 +autostart=false +autorestart=unexpected +stdout_logfile=syslog +stderr_logfile=syslog +dependent_startup=true +dependent_startup_wait_for=rsyslogd:running diff --git a/dockers/docker-base-stretch/Dockerfile.j2 b/dockers/docker-base-stretch/Dockerfile.j2 index 9e11c4ea35..5db96e37ba 100644 --- a/dockers/docker-base-stretch/Dockerfile.j2 +++ b/dockers/docker-base-stretch/Dockerfile.j2 @@ -90,7 +90,7 @@ RUN pip install supervisor>=3.4.0 # Add support for supervisord to handle startup dependencies RUN pip install supervisord-dependent-startup==1.4.0 -RUN mkdir -p /etc/supervisor /var/log/supervisor +RUN mkdir -p /var/log/supervisor /etc/supervisor/conf.d RUN apt-get -y purge \ exim4 \ @@ -113,10 +113,10 @@ RUN apt-get clean -y && \ apt-get autoremove -y && \ rm -rf /var/lib/apt/lists/* /tmp/* -COPY ["etc/rsyslog.conf", "/etc/rsyslog.conf"] COPY ["etc/rsyslog.d/*", "/etc/rsyslog.d/"] COPY ["root/.vimrc", "/root/.vimrc"] RUN ln /usr/bin/vim.tiny /usr/bin/vim COPY ["etc/supervisor/supervisord.conf", "/etc/supervisor/"] +COPY ["etc/supervisor/containercfgd.conf", "/etc/supervisor/conf.d/"] diff --git a/dockers/docker-base-stretch/etc/supervisor/containercfgd.conf b/dockers/docker-base-stretch/etc/supervisor/containercfgd.conf new file mode 100644 index 0000000000..8d938e6f0f --- /dev/null +++ b/dockers/docker-base-stretch/etc/supervisor/containercfgd.conf @@ -0,0 +1,9 @@ +[program:containercfgd] +command=python /usr/local/bin/containercfgd +priority=99 +autostart=false +autorestart=unexpected +stdout_logfile=syslog +stderr_logfile=syslog +dependent_startup=true +dependent_startup_wait_for=rsyslogd:running diff --git a/dockers/docker-base/Dockerfile.j2 b/dockers/docker-base/Dockerfile.j2 index 15df3fe8a7..cd839e6075 100644 --- a/dockers/docker-base/Dockerfile.j2 +++ b/dockers/docker-base/Dockerfile.j2 @@ -49,7 +49,6 @@ RUN apt-get -y install \ rsyslog \ less -COPY ["etc/rsyslog.conf", "/etc/rsyslog.conf"] COPY ["etc/rsyslog.d/*", "/etc/rsyslog.d/"] COPY ["root/.vimrc", "/root/.vimrc"] @@ -64,10 +63,11 @@ RUN pip install wheel # Install supervisor RUN pip install supervisor>=3.4.0 -RUN mkdir -p /etc/supervisor +RUN mkdir -p /etc/supervisor/conf.d RUN mkdir -p /var/log/supervisor COPY ["etc/supervisor/supervisord.conf", "/etc/supervisor/"] +COPY ["etc/supervisor/containercfgd.conf", "/etc/supervisor/conf.d/"] RUN apt-get -y purge \ exim4 \ diff --git a/dockers/docker-base/etc/supervisor/containercfgd.conf b/dockers/docker-base/etc/supervisor/containercfgd.conf new file mode 100644 index 0000000000..8d938e6f0f --- /dev/null +++ b/dockers/docker-base/etc/supervisor/containercfgd.conf @@ -0,0 +1,9 @@ +[program:containercfgd] +command=python /usr/local/bin/containercfgd +priority=99 +autostart=false +autorestart=unexpected +stdout_logfile=syslog +stderr_logfile=syslog +dependent_startup=true +dependent_startup_wait_for=rsyslogd:running diff --git a/dockers/docker-database/supervisord.conf.j2 b/dockers/docker-database/supervisord.conf.j2 index b063016eb6..c73c6e783e 100644 --- a/dockers/docker-database/supervisord.conf.j2 +++ b/dockers/docker-database/supervisord.conf.j2 @@ -3,6 +3,15 @@ logfile_maxbytes=1MB logfile_backups=2 nodaemon=true +[eventlistener:dependent-startup] +command=python3 -m supervisord_dependent_startup +autostart=true +autorestart=unexpected +startretries=0 +exitcodes=0,3 +events=PROCESS_STATE +buffer_size=1024 + [eventlistener:supervisor-proc-exit-listener] command=/usr/bin/supervisor-proc-exit-listener --container-name database events=PROCESS_STATE_EXITED,PROCESS_STATE_RUNNING @@ -13,14 +22,15 @@ buffer_size=1024 [program:rsyslogd] command=/usr/sbin/rsyslogd -n -iNONE priority=1 -autostart=true +autostart=false autorestart=false stdout_logfile=syslog stderr_logfile=syslog +dependent_startup=true {% if INSTANCES %} {% for redis_inst, redis_items in INSTANCES.items() %} -[program: {{ redis_inst }}] +[program:{{ redis_inst }}] {% if redis_items['hostname'] != '127.0.0.1' and redis_inst != 'redis_chassis' %} {%- set LOOPBACK_IP = '127.0.0.1' -%} {%- else -%} @@ -28,17 +38,21 @@ stderr_logfile=syslog {%- endif -%} command=/bin/bash -c "{ [[ -s /var/lib/{{ redis_inst }}/dump.rdb ]] || rm -f /var/lib/{{ redis_inst }}/dump.rdb; } && mkdir -p /var/lib/{{ redis_inst }} && exec /usr/bin/redis-server /etc/redis/redis.conf --bind {{ LOOPBACK_IP }} {{ redis_items['hostname'] }} --port {{ redis_items['port'] }} --unixsocket {{ redis_items['unix_socket_path'] }} --pidfile /var/run/redis/{{ redis_inst }}.pid --dir /var/lib/{{ redis_inst }}" priority=2 -autostart=true +autostart=false autorestart=false stdout_logfile=syslog stderr_logfile=syslog +dependent_startup=true +dependent_startup_wait_for=rsyslogd:running {% endfor %} {% endif %} [program:flushdb] command=/bin/bash -c "sleep 300 && /usr/local/bin/flush_unused_database" priority=3 -autostart=true +autostart=false autorestart=false stdout_logfile=syslog stderr_logfile=syslog +dependent_startup=true +dependent_startup_wait_for=rsyslogd:running diff --git a/files/build_templates/docker_image_ctl.j2 b/files/build_templates/docker_image_ctl.j2 index ace39df4e5..6ba11ce650 100644 --- a/files/build_templates/docker_image_ctl.j2 +++ b/files/build_templates/docker_image_ctl.j2 @@ -34,16 +34,23 @@ function updateSyslogConf() # Also update the container name if [[ ($NUM_ASIC -gt 1) ]]; then TARGET_IP=$(docker network inspect bridge --format={{ "'{{(index .IPAM.Config 0).Gateway}}'" }}) - CONTAINER_NAME="$DOCKERNAME" - TMP_FILE="/tmp/rsyslog.$CONTAINER_NAME.conf" - {%- if docker_container_name == "database" %} - python -c "import jinja2, os; paths=['/usr/share/sonic/templates']; loader = jinja2.FileSystemLoader(paths); env = jinja2.Environment(loader=loader, trim_blocks=True); template_file='/usr/share/sonic/templates/rsyslog-container.conf.j2'; template = env.get_template(os.path.basename(template_file)); data=template.render({\"target_ip\":\"$TARGET_IP\",\"container_name\":\"$CONTAINER_NAME\"}); print(data)" > $TMP_FILE - {%- else %} - sonic-cfggen -t /usr/share/sonic/templates/rsyslog-container.conf.j2 -a "{\"target_ip\": \"$TARGET_IP\", \"container_name\": \"$CONTAINER_NAME\" }" > $TMP_FILE - {%- endif %} - docker cp $TMP_FILE ${DOCKERNAME}:/etc/rsyslog.conf - rm -rf $TMP_FILE + else + if [ "$CONTAINER_EXISTS" = "yes" ]; then + # database configuration has been synced to /etc/rsyslog.conf + # no need generate it to save boot time + return + fi + TARGET_IP="127.0.0.1" fi + CONTAINER_NAME="$DOCKERNAME" + TMP_FILE="/tmp/rsyslog.$CONTAINER_NAME.conf" + {%- if docker_container_name == "database" %} + python -c "import jinja2, os; paths=['/usr/share/sonic/templates']; loader = jinja2.FileSystemLoader(paths); env = jinja2.Environment(loader=loader, trim_blocks=True); template_file='/usr/share/sonic/templates/rsyslog-container.conf.j2'; template = env.get_template(os.path.basename(template_file)); data=template.render({\"target_ip\":\"$TARGET_IP\",\"container_name\":\"$CONTAINER_NAME\"}); print(data)" > $TMP_FILE + {%- else %} + sonic-cfggen -d -t /usr/share/sonic/templates/rsyslog-container.conf.j2 -a "{\"target_ip\": \"$TARGET_IP\", \"container_name\": \"$CONTAINER_NAME\" }" > $TMP_FILE + {%- endif %} + docker cp $TMP_FILE ${DOCKERNAME}:/etc/rsyslog.conf + rm -rf $TMP_FILE } function ebtables_config() { @@ -342,6 +349,7 @@ start() { DOCKERMOUNT=`getMountPoint "$DOCKERCHECK"` {%- endif %} if [ x"$DOCKERMOUNT" == x"$MOUNTPATH" ]; then + CONTAINER_EXISTS="yes" preStartAction {%- if docker_container_name == "database" %} echo "Starting existing ${DOCKERNAME} container" @@ -536,6 +544,7 @@ start() { {%- endif %} $REDIS_MNT \ -v /usr/share/sonic/device/$PLATFORM:/usr/share/sonic/platform:ro \ + -v /usr/share/sonic/templates/rsyslog-container.conf.j2:/usr/share/sonic/templates/rsyslog-container.conf.j2:ro \ {%- if sonic_asic_platform != "mellanox" %} {%- if mount_default_tmpfs|default("n") == "y" %} --tmpfs /tmp \ @@ -547,6 +556,7 @@ start() { --env "NAMESPACE_ID"="$DEV" \ --env "NAMESPACE_PREFIX"="$NAMESPACE_PREFIX" \ --env "NAMESPACE_COUNT"=$NUM_ASIC \ + --env "CONTAINER_NAME"=$DOCKERNAME \ --name=$DOCKERNAME \ {%- if docker_container_name == "gbsyncd" %} -v /var/run/docker-syncd$DEV:/var/run/sswsyncd \ @@ -617,6 +627,7 @@ fi {%- endif %} NAMESPACE_PREFIX="asic" DOCKERNAME=$DOCKERNAME$DEV +CONTAINER_EXISTS="no" if [ "$DEV" ]; then NET_NS="$NAMESPACE_PREFIX$DEV" #name of the network namespace @@ -639,4 +650,4 @@ case "$1" in echo "Usage: $0 {start namespace(optional)|wait namespace(optional)|stop namespace(optional)}" exit 1 ;; -esac +esac \ No newline at end of file diff --git a/files/image_config/rsyslog/rsyslog-container.conf.j2 b/files/image_config/rsyslog/rsyslog-container.conf.j2 index d17fbb6767..4e2c90b548 100644 --- a/files/image_config/rsyslog/rsyslog-container.conf.j2 +++ b/files/image_config/rsyslog/rsyslog-container.conf.j2 @@ -14,8 +14,24 @@ $ModLoad imuxsock # provides support for local system logging # # Set a rate limit on messages from the container # -$SystemLogRateLimitInterval 300 -$SystemLogRateLimitBurst 20000 + +{% if SYSLOG_CONFIG_FEATURE is defined %} +{% if container_name in SYSLOG_CONFIG_FEATURE %} +{% if 'rate_limit_interval' in SYSLOG_CONFIG_FEATURE[container_name]%} +{% set rate_limit_interval = SYSLOG_CONFIG_FEATURE[container_name]['rate_limit_interval'] %} +{% endif %} +{% if 'rate_limit_burst' in SYSLOG_CONFIG_FEATURE[container_name]%} +{% set rate_limit_burst = SYSLOG_CONFIG_FEATURE[container_name]['rate_limit_burst'] %} +{% endif %} +{% endif %} +{% endif %} + +{% if rate_limit_interval is defined %} +$SystemLogRateLimitInterval {{ rate_limit_interval }} +{% endif %} +{% if rate_limit_burst is defined %} +$SystemLogRateLimitBurst {{ rate_limit_burst }} +{% endif %} #$ModLoad imklog # provides kernel logging support #$ModLoad immark # provides --MARK-- message capability @@ -73,4 +89,4 @@ $RepeatedMsgReduction on ############### #### RULES #### -############### +############### \ No newline at end of file diff --git a/rules/docker-config-engine-bullseye.mk b/rules/docker-config-engine-bullseye.mk index ea0ae43b54..9548391a69 100644 --- a/rules/docker-config-engine-bullseye.mk +++ b/rules/docker-config-engine-bullseye.mk @@ -11,8 +11,9 @@ $(DOCKER_CONFIG_ENGINE_BULLSEYE)_DEPENDS += $(LIBSWSSCOMMON) \ $(SONIC_DB_CLI) \ $(SONIC_EVENTD) $(DOCKER_CONFIG_ENGINE_BULLSEYE)_PYTHON_WHEELS += $(SONIC_PY_COMMON_PY3) \ - $(SONIC_YANG_MGMT_PY3) \ - $(SONIC_YANG_MODELS_PY3) + $(SONIC_YANG_MGMT_PY3) \ + $(SONIC_YANG_MODELS_PY3) \ + $(SONIC_CONTAINERCFGD) $(DOCKER_CONFIG_ENGINE_BULLSEYE)_PYTHON_WHEELS += $(SONIC_CONFIG_ENGINE_PY3) $(DOCKER_CONFIG_ENGINE_BULLSEYE)_LOAD_DOCKERS += $(DOCKER_BASE_BULLSEYE) $(DOCKER_CONFIG_ENGINE_BULLSEYE)_FILES += $(SWSS_VARS_TEMPLATE) diff --git a/rules/docker-config-engine-buster.mk b/rules/docker-config-engine-buster.mk index 38a94bae4c..cd6d6f43b4 100644 --- a/rules/docker-config-engine-buster.mk +++ b/rules/docker-config-engine-buster.mk @@ -11,7 +11,8 @@ $(DOCKER_CONFIG_ENGINE_BUSTER)_DEPENDS += $(LIBSWSSCOMMON) \ $(SONIC_DB_CLI) $(DOCKER_CONFIG_ENGINE_BUSTER)_PYTHON_WHEELS += $(SONIC_PY_COMMON_PY3) \ $(SONIC_YANG_MGMT_PY3) \ - $(SONIC_YANG_MODELS_PY3) + $(SONIC_YANG_MODELS_PY3) \ + $(SONIC_CONTAINERCFGD) $(DOCKER_CONFIG_ENGINE_BUSTER)_PYTHON_WHEELS += $(SONIC_CONFIG_ENGINE_PY3) $(DOCKER_CONFIG_ENGINE_BUSTER)_LOAD_DOCKERS += $(DOCKER_BASE_BUSTER) $(DOCKER_CONFIG_ENGINE_BUSTER)_FILES += $(SWSS_VARS_TEMPLATE) diff --git a/rules/sonic-containercfgd.dep b/rules/sonic-containercfgd.dep new file mode 100644 index 0000000000..48f8d320af --- /dev/null +++ b/rules/sonic-containercfgd.dep @@ -0,0 +1,10 @@ +SPATH := $($(SONIC_CONTAINERCFGD)_SRC_PATH) +DEP_FILES := $(SONIC_COMMON_FILES_LIST) rules/sonic-containercfgd.mk rules/sonic-containercfgd.dep +DEP_FILES += $(SONIC_COMMON_BASE_FILES_LIST) +SMDEP_FILES := $(addprefix $(SPATH)/,$(shell cd $(SPATH) && git ls-files)) + +$(SONIC_CONTAINERCFGD)_CACHE_MODE := GIT_CONTENT_SHA +$(SONIC_CONTAINERCFGD)_DEP_FLAGS := $(SONIC_COMMON_FLAGS_LIST) +$(SONIC_CONTAINERCFGD)_DEP_FILES := $(DEP_FILES) +$(SONIC_CONTAINERCFGD)_SMDEP_FILES := $(SMDEP_FILES) +$(SONIC_CONTAINERCFGD)_SMDEP_PATHS := $(SPATH) diff --git a/rules/sonic-containercfgd.mk b/rules/sonic-containercfgd.mk new file mode 100644 index 0000000000..e59f3bbd0b --- /dev/null +++ b/rules/sonic-containercfgd.mk @@ -0,0 +1,8 @@ +# sonic-bgpcfgd package + +SONIC_CONTAINERCFGD = sonic_containercfgd-1.0-py3-none-any.whl +$(SONIC_CONTAINERCFGD)_SRC_PATH = $(SRC_PATH)/sonic-containercfgd +$(SONIC_CONTAINERCFGD)_DEPENDS += $(SONIC_PY_COMMON_PY3) +$(SONIC_CONTAINERCFGD)_DEBS_DEPENDS = $(LIBSWSSCOMMON) $(PYTHON3_SWSSCOMMON) +$(SONIC_CONTAINERCFGD)_PYTHON_VERSION = 3 +SONIC_PYTHON_WHEELS += $(SONIC_CONTAINERCFGD) diff --git a/src/sonic-containercfgd/.gitignore b/src/sonic-containercfgd/.gitignore new file mode 100644 index 0000000000..5ae7a596bf --- /dev/null +++ b/src/sonic-containercfgd/.gitignore @@ -0,0 +1,13 @@ +# Compiled Python files +*.pyc + +# Generated by packaging +*.egg-info/ +.eggs/ +build/ +dist/ + +# Unit test coverage +.coverage +coverage.xml +htmlcov/ diff --git a/src/sonic-containercfgd/containercfgd/__init__.py b/src/sonic-containercfgd/containercfgd/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/sonic-containercfgd/containercfgd/containercfgd.py b/src/sonic-containercfgd/containercfgd/containercfgd.py new file mode 100644 index 0000000000..14b47db3c0 --- /dev/null +++ b/src/sonic-containercfgd/containercfgd/containercfgd.py @@ -0,0 +1,198 @@ +import os +import re +import signal +import subprocess +import sys + +from sonic_py_common import daemon_base, logger +from swsscommon.swsscommon import ConfigDBConnector, RestartWaiter + +SYSLOG_IDENTIFIER = "containercfgd" +logger = logger.Logger(SYSLOG_IDENTIFIER) + +# Table names +FEATURE_TABLE = 'FEATURE' +SYSLOG_CONFIG_FEATURE_TABLE = 'SYSLOG_CONFIG_FEATURE' + +# Table field names +SYSLOG_RATE_LIMIT_INTERVAL = 'rate_limit_interval' +SYSLOG_RATE_LIMIT_BURST = 'rate_limit_burst' + +# Container name +container_name = None + + +def run_command(command): + """ + Utility function to run an shell command and return the output. + :param command: Shell command string. + :return: Output of the shell command. + """ + return subprocess.check_output(command, text=True, stderr=subprocess.PIPE) + + +class ContainerConfigDaemon(daemon_base.DaemonBase): + handlers = {} + + def __init__(self): + super(ContainerConfigDaemon, self).__init__(SYSLOG_IDENTIFIER) + + def run(self): + """Register config handlers and listen to CONFIG DB changes + """ + config_db = ConfigDBConnector() + config_db.connect(wait_for_init=True, retry_on=True) + self.log_notice(f'Connected to CONFIG DB') + for table_name, handler in self.handlers.items(): + config_db.subscribe(table_name, handler.handle_config) + config_db.listen(init_data_handler=self.init_data_handler) + + def init_data_handler(self, init_data): + """Handle initial data in CONFIG DB + + Args: + init_data (dict): Initial data when first time connecting to CONFIG DB. {: {: }} + """ + for handler in self.handlers.values(): + handler.handle_init_data(init_data) + + @classmethod + def register_handler(cls, table_name, object_type): + """Register CONFIG DB handler + + Args: + table_name (str): CONFIG DB table name + object_type (class): Class of CONFIG DB handler + """ + cls.handlers[table_name] = object_type() + + def signal_handler(self, sig, frame): + if sig == signal.SIGHUP: + self.log_info("ContainerCfgd: Caught SIGHUP - ignoring...") + elif sig == signal.SIGINT: + self.log_info("ContainerCfgd: Caught SIGINT - exiting...") + sys.exit(128 + sig) + elif sig == signal.SIGTERM: + self.log_info("ContainerCfgd: Caught SIGTERM - exiting...") + sys.exit(128 + sig) + else: + self.log_warning("ContainerCfgd: Caught unhandled signal '{}'".format(sig)) + + +def config_handler(table_name): + """Decorator to register CONFIG DB handler + + Args: + table_name (str): CONFIG DB table name + """ + def wrapper(object_type): + ContainerConfigDaemon.register_handler(table_name, object_type) + return object_type + return wrapper + + +@config_handler(SYSLOG_CONFIG_FEATURE_TABLE) +class SyslogHandler: + # syslog conf file path in docker + SYSLOG_CONF_PATH = '/etc/rsyslog.conf' + # temporary syslog conf file path in docker + TMP_SYSLOG_CONF_PATH = '/tmp/rsyslog.conf' + + # Regular expressions to extract value from rsyslog.conf + INTERVAL_PATTERN = '.*SystemLogRateLimitInterval\s+(\d+).*' + BURST_PATTERN = '.*SystemLogRateLimitBurst\s+(\d+).*' + TARGET_IP_PATTERN = '.*target="(.*?)".*' + + def __init__(self): + self.current_interval, self.current_burst, self.target_ip = self.parse_syslog_conf() + + def handle_config(self, table, key, data): + """Handle CONFIG DB change. Callback by ConfigDBConnector. + + Args: + table (str): CONFIG DB table name + key (str): Key of the changed entry + data (dict): Data of the entry: {: } + """ + try: + if key != container_name: + return + self.update_syslog_config(data) + except Exception as e: + logger.log_error('Failed to config syslog for container {} with data {} - {}'.format(key, data, e)) + + def handle_init_data(self, init_data): + """Handle initial data in CONFIG DB. Callback by ConfigDBConnector. + + Args: + init_data (dict): Initial data when first time connecting to CONFIG DB. {: {: }} + """ + if SYSLOG_CONFIG_FEATURE_TABLE in init_data: + if container_name in init_data[SYSLOG_CONFIG_FEATURE_TABLE]: + self.update_syslog_config(init_data[SYSLOG_CONFIG_FEATURE_TABLE][container_name]) + + def update_syslog_config(self, data): + """Parse existing syslog conf and apply new syslog conf. + + Args: + data (dict): Data of the entry: {: } + """ + new_interval = '0' if not data else data.get(SYSLOG_RATE_LIMIT_INTERVAL, '0') + new_burst = '0' if not data else data.get(SYSLOG_RATE_LIMIT_BURST, '0') + + if new_interval == self.current_interval and new_burst == self.current_burst: + logger.log_notice('Syslog rate limit configuration does not change, ignore it') + return + + logger.log_notice(f'Configure syslog rate limit interval={new_interval}, burst={new_burst}') + + if os.path.exists(self.TMP_SYSLOG_CONF_PATH): + os.remove(self.TMP_SYSLOG_CONF_PATH) + with open(self.TMP_SYSLOG_CONF_PATH, 'w+') as f: + json_args = f'{{"target_ip": "127.0.0.1", "{self.target_ip}": "{container_name}" }}' + output = run_command(['sonic-cfggen', '-d', '-t', '/usr/share/sonic/templates/rsyslog-container.conf.j2', '-a', json_args]) + f.write(output) + run_command(['cp', self.TMP_SYSLOG_CONF_PATH, self.SYSLOG_CONF_PATH]) + run_command(['supervisorctl', 'restart', 'rsyslogd']) + self.current_interval = new_interval + self.current_burst = new_burst + + def parse_syslog_conf(self): + """Passe existing syslog conf and extract config values + + Returns: + tuple: interval,burst,target_ip + """ + interval = '0' + burst = '0' + target_ip = None + + with open(self.SYSLOG_CONF_PATH, 'r') as f: + content = f.read() + pattern = re.compile(self.INTERVAL_PATTERN) + for match in pattern.finditer(content): + interval = match.group(1) + break + + pattern = re.compile(self.BURST_PATTERN) + for match in pattern.finditer(content): + burst = match.group(1) + break + + pattern = re.compile(self.TARGET_IP_PATTERN) + for match in pattern.finditer(content): + target_ip = match.group(1) + break + return interval, burst, target_ip + + +def main(): + RestartWaiter.waitAdvancedBootDone() + global container_name + container_name = os.environ['CONTAINER_NAME'] + daemon = ContainerConfigDaemon() + daemon.run() + + +if __name__ == '__main__': + main() diff --git a/src/sonic-containercfgd/pytest.ini b/src/sonic-containercfgd/pytest.ini new file mode 100644 index 0000000000..2fc0931f27 --- /dev/null +++ b/src/sonic-containercfgd/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = --cov=containercfgd --cov-report html --cov-report term --cov-report xml diff --git a/src/sonic-containercfgd/setup.cfg b/src/sonic-containercfgd/setup.cfg new file mode 100644 index 0000000000..b7e478982c --- /dev/null +++ b/src/sonic-containercfgd/setup.cfg @@ -0,0 +1,2 @@ +[aliases] +test=pytest diff --git a/src/sonic-containercfgd/setup.py b/src/sonic-containercfgd/setup.py new file mode 100644 index 0000000000..9f60c58135 --- /dev/null +++ b/src/sonic-containercfgd/setup.py @@ -0,0 +1,48 @@ +from setuptools import setup + +dependencies = [ + 'sonic_py_common', +] + +setup( + name='sonic-containercfgd', + version='1.0', + description='SONiC container config daemon package', + license='Apache 2.0', + author='SONiC Team', + author_email='linuxnetdev@microsoft.com', + url='https://github.com/Azure/sonic-buildimage', + maintainer='Junchao Chen', + maintainer_email='junchaow@nvidia.com', + install_requires=dependencies, + entry_points={ + 'console_scripts': [ + 'containercfgd = containercfgd.containercfgd:main', + ] + }, + packages=[ + 'containercfgd', + 'tests' + ], + setup_requires=[ + 'pytest-runner' + ], + tests_require=[ + 'pytest', + 'mock>=2.0.0' + ], + classifiers=[ + 'Development Status :: 4 - Beta', + 'Environment :: No Input/Output (Daemon)', + 'Intended Audience :: Developers', + 'Intended Audience :: Information Technology', + 'Intended Audience :: System Administrators', + 'License :: OSI Approved :: Apache Software License', + 'Natural Language :: English', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python :: 3.7', + 'Topic :: System :: Hardware', + ], + keywords='SONiC sonic container config daemon', + test_suite='setup.get_test_suite' +) diff --git a/src/sonic-containercfgd/tests/conftest.py b/src/sonic-containercfgd/tests/conftest.py new file mode 100644 index 0000000000..0ac7be4265 --- /dev/null +++ b/src/sonic-containercfgd/tests/conftest.py @@ -0,0 +1,7 @@ +import os + + +def init_env(): + # Just make sure there is an environment variable CONTAINER_NAME + # The value of this environment variable is not important + os.environ["CONTAINER_NAME"] = "some_container" diff --git a/src/sonic-containercfgd/tests/mock_empty_rsyslog.conf b/src/sonic-containercfgd/tests/mock_empty_rsyslog.conf new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/sonic-containercfgd/tests/mock_rsyslog.conf b/src/sonic-containercfgd/tests/mock_rsyslog.conf new file mode 100644 index 0000000000..68e917b934 --- /dev/null +++ b/src/sonic-containercfgd/tests/mock_rsyslog.conf @@ -0,0 +1,29 @@ +$ModLoad imuxsock # provides support for local system logging + +# +# Set a rate limit on messages from the container +# + + +$SystemLogRateLimitInterval 50 +$SystemLogRateLimitBurst 10002 + +#$ModLoad imklog # provides kernel logging support +#$ModLoad immark # provides --MARK-- message capability + +# provides UDP syslog reception +#$ModLoad imudp +#$UDPServerRun 514 + +# provides TCP syslog reception +#$ModLoad imtcp +#$InputTCPServerRun 514 + + +########################### +#### GLOBAL DIRECTIVES #### +########################### + +# Set remote syslog server +template (name="ForwardFormatInContainer" type="string" string="<%PRI%>%TIMESTAMP:::date-rfc3339% %HOSTNAME% pmon#%syslogtag%%msg:::sp-if-no-1st-sp%%msg%") +*.* action(type="omfwd" target="127.0.0.1" port="514" protocol="udp" Template="ForwardFormatInContainer") diff --git a/src/sonic-containercfgd/tests/test_config_daemon.py b/src/sonic-containercfgd/tests/test_config_daemon.py new file mode 100644 index 0000000000..3604a32ab2 --- /dev/null +++ b/src/sonic-containercfgd/tests/test_config_daemon.py @@ -0,0 +1,54 @@ +import os +import sys +from unittest import mock + +test_path = os.path.dirname(os.path.abspath(__file__)) +modules_path = os.path.dirname(test_path) +sys.path.insert(0, modules_path) + +from containercfgd import containercfgd + + +@containercfgd.config_handler('MockTable') +class MockHandler: + def handle_init_data(self, init_data): + pass + + def handle_config(self, table, key, data): + pass + + +def test_handler_register(): + assert 'MockTable' in containercfgd.ContainerConfigDaemon.handlers + assert isinstance(containercfgd.ContainerConfigDaemon.handlers['MockTable'], MockHandler) + + +def test_init_data_handler(): + mock_handler_cls = mock.MagicMock() + mock_handler_instance = mock.MagicMock() + mock_handler_instance.handle_init_data = mock.MagicMock() + mock_handler_cls.return_value = mock_handler_instance + + containercfgd.ContainerConfigDaemon.register_handler('LoadMock', mock_handler_cls) + daemon = containercfgd.ContainerConfigDaemon() + daemon.init_data_handler({}) + mock_handler_instance.handle_init_data.assert_called_once() + containercfgd.ContainerConfigDaemon.handlers.pop('LoadMock') + + +@mock.patch('containercfgd.containercfgd.ConfigDBConnector') +def test_run(mock_connector): + mock_db = mock.MagicMock() + mock_db.connect = mock.MagicMock() + mock_db.subscribe = mock.MagicMock() + mock_db.listen = mock.MagicMock() + mock_connector.return_value = mock_db + + daemon = containercfgd.ContainerConfigDaemon() + daemon.run() + mock_db.connect.assert_called_once() + expected = [] + for table_name, handler in containercfgd.ContainerConfigDaemon.handlers.items(): + expected.append(mock.call(table_name, handler.handle_config)) + mock_db.subscribe.assert_has_calls(expected, any_order=True) + mock_db.listen.assert_called_once() diff --git a/src/sonic-containercfgd/tests/test_syslog_config.py b/src/sonic-containercfgd/tests/test_syslog_config.py new file mode 100644 index 0000000000..23e5887b9d --- /dev/null +++ b/src/sonic-containercfgd/tests/test_syslog_config.py @@ -0,0 +1,81 @@ +import os +import sys +from unittest import mock + +test_path = os.path.dirname(os.path.abspath(__file__)) +modules_path = os.path.dirname(test_path) +sys.path.insert(0, modules_path) + +from containercfgd import containercfgd + +containercfgd.container_name = 'swss' + + +def test_handle_config(): + handler = containercfgd.SyslogHandler() + handler.update_syslog_config = mock.MagicMock() + + handler.handle_config(containercfgd.SYSLOG_CONFIG_FEATURE_TABLE, + 'bgp', + None) + handler.update_syslog_config.assert_not_called() + + handler.handle_config(containercfgd.SYSLOG_CONFIG_FEATURE_TABLE, + 'swss', + None) + handler.update_syslog_config.assert_called_once() + + handler.update_syslog_config.side_effect = Exception('') + handler.handle_config(containercfgd.SYSLOG_CONFIG_FEATURE_TABLE, + 'swss', + None) + + +def test_handle_init_data(): + handler = containercfgd.SyslogHandler() + handler.update_syslog_config = mock.MagicMock() + + init_data = {} + handler.handle_init_data(init_data) + handler.update_syslog_config.assert_not_called() + + init_data = {containercfgd.SYSLOG_CONFIG_FEATURE_TABLE: {}} + handler.handle_init_data(init_data) + handler.update_syslog_config.assert_not_called() + + init_data = {containercfgd.SYSLOG_CONFIG_FEATURE_TABLE: {'swss': {}}} + handler.handle_init_data(init_data) + handler.update_syslog_config.assert_called_once() + + +@mock.patch('containercfgd.containercfgd.run_command') +@mock.patch('containercfgd.containercfgd.SyslogHandler.parse_syslog_conf', mock.MagicMock(return_value=('100', '200', '127.0.0.1'))) +def test_update_syslog_config(mock_run_cmd): + mock_run_cmd.return_value = "" + handler = containercfgd.SyslogHandler() + + data = {containercfgd.SYSLOG_RATE_LIMIT_INTERVAL: '100', + containercfgd.SYSLOG_RATE_LIMIT_BURST: '200'} + handler.update_syslog_config(data) + mock_run_cmd.assert_not_called() + + data = {containercfgd.SYSLOG_RATE_LIMIT_INTERVAL: '200', + containercfgd.SYSLOG_RATE_LIMIT_BURST: '200'} + + handler.update_syslog_config(data) + mock_run_cmd.assert_called() + + +def test_parse_syslog_conf(): + handler = containercfgd.SyslogHandler() + handler.SYSLOG_CONF_PATH = os.path.join(test_path, 'mock_rsyslog.conf') + interval, burst, target_ip = handler.parse_syslog_conf() + assert interval == '50' + assert burst == '10002' + assert target_ip == '127.0.0.1' + + handler.SYSLOG_CONF_PATH = os.path.join(test_path, 'mock_empty_rsyslog.conf') + interval, burst, target_ip = handler.parse_syslog_conf() + assert interval == '0' + assert burst == '0' + assert target_ip is None