summaryrefslogtreecommitdiff
path: root/src/services/vyos-configd
diff options
context:
space:
mode:
Diffstat (limited to 'src/services/vyos-configd')
-rw-r--r--src/services/vyos-configd340
1 files changed, 340 insertions, 0 deletions
diff --git a/src/services/vyos-configd b/src/services/vyos-configd
new file mode 100644
index 0000000..cb23642
--- /dev/null
+++ b/src/services/vyos-configd
@@ -0,0 +1,340 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020-2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# pylint: disable=redefined-outer-name
+
+import os
+import sys
+import grp
+import re
+import json
+import typing
+import logging
+import signal
+import traceback
+import importlib.util
+import io
+from contextlib import redirect_stdout
+
+import zmq
+
+from vyos.defaults import directories
+from vyos.utils.boot import boot_configuration_complete
+from vyos.configsource import ConfigSourceString
+from vyos.configsource import ConfigSourceError
+from vyos.configdiff import get_commit_scripts
+from vyos.config import Config
+from vyos import ConfigError
+
+CFG_GROUP = 'vyattacfg'
+
+script_stdout_log = '/tmp/vyos-configd-script-stdout'
+
+debug = True
+
+logger = logging.getLogger(__name__)
+logs_handler = logging.StreamHandler()
+logger.addHandler(logs_handler)
+
+if debug:
+ logger.setLevel(logging.DEBUG)
+else:
+ logger.setLevel(logging.INFO)
+
+SOCKET_PATH = 'ipc:///run/vyos-configd.sock'
+MAX_MSG_SIZE = 65535
+
+# Response error codes
+R_SUCCESS = 1
+R_ERROR_COMMIT = 2
+R_ERROR_DAEMON = 4
+R_PASS = 8
+
+vyos_conf_scripts_dir = directories['conf_mode']
+configd_include_file = os.path.join(directories['data'], 'configd-include.json')
+configd_env_set_file = os.path.join(directories['data'], 'vyos-configd-env-set')
+configd_env_unset_file = os.path.join(directories['data'], 'vyos-configd-env-unset')
+# sourced on entering config session
+configd_env_file = '/etc/default/vyos-configd-env'
+
+def key_name_from_file_name(f):
+ return os.path.splitext(f)[0]
+
+def module_name_from_key(k):
+ return k.replace('-', '_')
+
+def path_from_file_name(f):
+ return os.path.join(vyos_conf_scripts_dir, f)
+
+
+# opt-in to be run by daemon
+with open(configd_include_file) as f:
+ try:
+ include = json.load(f)
+ except OSError as e:
+ logger.critical(f'configd include file error: {e}')
+ sys.exit(1)
+ except json.JSONDecodeError as e:
+ logger.critical(f'JSON load error: {e}')
+ sys.exit(1)
+
+
+# import conf_mode scripts
+(_, _, filenames) = next(iter(os.walk(vyos_conf_scripts_dir)))
+filenames.sort()
+
+load_filenames = [f for f in filenames if f in include]
+imports = [key_name_from_file_name(f) for f in load_filenames]
+module_names = [module_name_from_key(k) for k in imports]
+paths = [path_from_file_name(f) for f in load_filenames]
+to_load = list(zip(module_names, paths))
+
+modules = []
+
+for x in to_load:
+ spec = importlib.util.spec_from_file_location(x[0], x[1])
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ modules.append(module)
+
+conf_mode_scripts = dict(zip(imports, modules))
+
+exclude_set = {key_name_from_file_name(f) for f in filenames if f not in include}
+include_set = {key_name_from_file_name(f) for f in filenames if f in include}
+
+
+def write_stdout_log(file_name, msg):
+ if boot_configuration_complete():
+ return
+ with open(file_name, 'a') as f:
+ f.write(msg)
+
+
+def run_script(script_name, config, args) -> tuple[int, str]:
+ # pylint: disable=broad-exception-caught
+
+ script = conf_mode_scripts[script_name]
+ script.argv = args
+ config.set_level([])
+ try:
+ c = script.get_config(config)
+ script.verify(c)
+ script.generate(c)
+ script.apply(c)
+ except ConfigError as e:
+ logger.error(e)
+ return R_ERROR_COMMIT, str(e)
+ except Exception:
+ tb = traceback.format_exc()
+ logger.error(tb)
+ return R_ERROR_COMMIT, tb
+
+ return R_SUCCESS, ''
+
+
+def initialization(socket):
+ # pylint: disable=broad-exception-caught,too-many-locals
+
+ # Reset config strings:
+ active_string = ''
+ session_string = ''
+ # check first for resent init msg, in case of client timeout
+ while True:
+ msg = socket.recv().decode('utf-8', 'ignore')
+ try:
+ message = json.loads(msg)
+ if message['type'] == 'init':
+ resp = 'init'
+ socket.send(resp.encode())
+ except Exception:
+ break
+
+ # zmq synchronous for ipc from single client:
+ active_string = msg
+ resp = 'active'
+ socket.send(resp.encode())
+ session_string = socket.recv().decode('utf-8', 'ignore')
+ resp = 'session'
+ socket.send(resp.encode())
+ pid_string = socket.recv().decode('utf-8', 'ignore')
+ resp = 'pid'
+ socket.send(resp.encode())
+ sudo_user_string = socket.recv().decode('utf-8', 'ignore')
+ resp = 'sudo_user'
+ socket.send(resp.encode())
+ temp_config_dir_string = socket.recv().decode('utf-8', 'ignore')
+ resp = 'temp_config_dir'
+ socket.send(resp.encode())
+ changes_only_dir_string = socket.recv().decode('utf-8', 'ignore')
+ resp = 'changes_only_dir'
+ socket.send(resp.encode())
+
+ logger.debug(f'config session pid is {pid_string}')
+ logger.debug(f'config session sudo_user is {sudo_user_string}')
+
+ os.environ['SUDO_USER'] = sudo_user_string
+ if temp_config_dir_string:
+ os.environ['VYATTA_TEMP_CONFIG_DIR'] = temp_config_dir_string
+ if changes_only_dir_string:
+ os.environ['VYATTA_CHANGES_ONLY_DIR'] = changes_only_dir_string
+
+ try:
+ configsource = ConfigSourceString(running_config_text=active_string,
+ session_config_text=session_string)
+ except ConfigSourceError as e:
+ logger.debug(e)
+ return None
+
+ config = Config(config_source=configsource)
+ dependent_func: dict[str, list[typing.Callable]] = {}
+ setattr(config, 'dependent_func', dependent_func)
+
+ commit_scripts = get_commit_scripts(config)
+ logger.debug(f'commit_scripts: {commit_scripts}')
+
+ scripts_called = []
+ setattr(config, 'scripts_called', scripts_called)
+
+ return config
+
+
+def process_node_data(config, data, _last: bool = False) -> tuple[int, str]:
+ if not config:
+ out = 'Empty config'
+ logger.critical(out)
+ return R_ERROR_DAEMON, out
+
+ script_name = None
+ os.environ['VYOS_TAGNODE_VALUE'] = ''
+ args = []
+ config.dependency_list.clear()
+
+ res = re.match(r'^(VYOS_TAGNODE_VALUE=[^/]+)?.*\/([^/]+).py(.*)', data)
+ if res.group(1):
+ env = res.group(1).split('=')
+ os.environ[env[0]] = env[1]
+ if res.group(2):
+ script_name = res.group(2)
+ if not script_name:
+ out = 'Missing script_name'
+ logger.critical(out)
+ return R_ERROR_DAEMON, out
+ if res.group(3):
+ args = res.group(3).split()
+ args.insert(0, f'{script_name}.py')
+
+ tag_value = os.getenv('VYOS_TAGNODE_VALUE', '')
+ tag_ext = f'_{tag_value}' if tag_value else ''
+ script_record = f'{script_name}{tag_ext}'
+ scripts_called = getattr(config, 'scripts_called', [])
+ scripts_called.append(script_record)
+
+ if script_name not in include_set:
+ return R_PASS, ''
+
+ with redirect_stdout(io.StringIO()) as o:
+ result, err_out = run_script(script_name, config, args)
+ amb_out = o.getvalue()
+ o.close()
+
+ out = amb_out + err_out
+
+ return result, out
+
+
+def send_result(sock, err, msg):
+ msg_size = min(MAX_MSG_SIZE, len(msg)) if msg else 0
+
+ err_rep = err.to_bytes(1, byteorder=sys.byteorder)
+ logger.debug(f'Sending reply: {err}')
+ sock.send(err_rep)
+
+ # size req from vyshim client
+ size_req = sock.recv().decode()
+ logger.debug(f'Received request: {size_req}')
+ msg_size_rep = hex(msg_size).encode()
+ sock.send(msg_size_rep)
+ logger.debug(f'Sending reply: {msg_size}')
+
+ if msg_size > 0:
+ # send req is sent from vyshim client only if msg_size > 0
+ send_req = sock.recv().decode()
+ logger.debug(f'Received request: {send_req}')
+ sock.send(msg.encode())
+ logger.debug('Sending reply with output')
+
+ write_stdout_log(script_stdout_log, msg)
+
+
+def remove_if_file(f: str):
+ try:
+ os.remove(f)
+ except FileNotFoundError:
+ pass
+
+
+def shutdown():
+ remove_if_file(configd_env_file)
+ os.symlink(configd_env_unset_file, configd_env_file)
+ sys.exit(0)
+
+
+if __name__ == '__main__':
+ context = zmq.Context()
+ socket = context.socket(zmq.REP)
+
+ # Set the right permissions on the socket, then change it back
+ o_mask = os.umask(0)
+ socket.bind(SOCKET_PATH)
+ os.umask(o_mask)
+
+ cfg_group = grp.getgrnam(CFG_GROUP)
+ os.setgid(cfg_group.gr_gid)
+
+ os.environ['VYOS_CONFIGD'] = 't'
+
+ def sig_handler(signum, frame):
+ # pylint: disable=unused-argument
+ shutdown()
+
+ signal.signal(signal.SIGTERM, sig_handler)
+ signal.signal(signal.SIGINT, sig_handler)
+
+ # Define the vyshim environment variable
+ remove_if_file(configd_env_file)
+ os.symlink(configd_env_set_file, configd_env_file)
+
+ config = None
+
+ while True:
+ # Wait for next request from client
+ msg = socket.recv().decode()
+ logger.debug(f'Received message: {msg}')
+ message = json.loads(msg)
+
+ if message['type'] == 'init':
+ resp = 'init'
+ socket.send(resp.encode())
+ config = initialization(socket)
+ elif message['type'] == 'node':
+ res, out = process_node_data(config, message['data'], message['last'])
+ send_result(socket, res, out)
+
+ if message['last'] and config:
+ scripts_called = getattr(config, 'scripts_called', [])
+ logger.debug(f'scripts_called: {scripts_called}')
+ else:
+ logger.critical(f'Unexpected message: {message}')