summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Estabrook <jestabro@vyos.io>2020-08-31 11:23:06 -0500
committerGitHub <noreply@github.com>2020-08-31 11:23:06 -0500
commit621fcb078abb53888e995a007fb9ea31e95e34ab (patch)
treea4dda1096a267fed41ac90121ab6233e5b8b7d39
parentbd076f694a763991a0b0d3a7bb0fa5d194d56d7c (diff)
parentad69fb36201ee0930b76d80f0869284e26846991 (diff)
downloadvyos-1x-621fcb078abb53888e995a007fb9ea31e95e34ab.tar.gz
vyos-1x-621fcb078abb53888e995a007fb9ea31e95e34ab.zip
Merge pull request #535 from jestabro/vyos-configd
configd: T2582: vyos config script daemon configd: T2582: add scripts to include list for daemon configd: T2808: add smoketest to ensure script consistency with daemon configd: T2582: add utility to safely add/remove items from include file configd: T2582: add shim var to node.def configd: T2582: inject shim env variable into configsession configd: T2582: add shim as config daemon client configd: T2582: add mkjson for use by shim configd: T2582: add config daemon and supporting files
-rw-r--r--Makefile10
-rw-r--r--data/configd-include.json64
-rw-r--r--data/vyos-configd-env-set2
-rw-r--r--data/vyos-configd-env-unset2
-rw-r--r--debian/control1
-rwxr-xr-xdebian/rules1
-rw-r--r--debian/vyos-1x.install1
-rw-r--r--python/vyos/configsession.py7
-rwxr-xr-xscripts/build-command-templates6
-rwxr-xr-xscripts/update-configd-include-file298
-rwxr-xr-xsmoketest/scripts/cli/test_configd_inspect.py107
-rwxr-xr-xsrc/conf_mode/bcast_relay.py7
-rwxr-xr-xsrc/conf_mode/dhcp_relay.py7
-rwxr-xr-xsrc/conf_mode/dhcp_server.py7
-rwxr-xr-xsrc/conf_mode/dhcpv6_relay.py7
-rwxr-xr-xsrc/conf_mode/dhcpv6_server.py7
-rwxr-xr-xsrc/conf_mode/dynamic_dns.py7
-rwxr-xr-xsrc/conf_mode/firewall_options.py7
-rwxr-xr-xsrc/conf_mode/host_name.py7
-rwxr-xr-xsrc/conf_mode/http-api.py8
-rwxr-xr-xsrc/conf_mode/https.py8
-rwxr-xr-xsrc/conf_mode/igmp_proxy.py7
-rwxr-xr-xsrc/conf_mode/intel_qat.py7
-rwxr-xr-xsrc/conf_mode/interfaces-bonding.py7
-rwxr-xr-xsrc/conf_mode/interfaces-bridge.py7
-rwxr-xr-xsrc/conf_mode/interfaces-dummy.py7
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py7
-rwxr-xr-xsrc/conf_mode/interfaces-geneve.py7
-rwxr-xr-xsrc/conf_mode/interfaces-l2tpv3.py7
-rwxr-xr-xsrc/conf_mode/interfaces-loopback.py7
-rwxr-xr-xsrc/conf_mode/interfaces-macsec.py7
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py7
-rwxr-xr-xsrc/conf_mode/interfaces-pppoe.py7
-rwxr-xr-xsrc/conf_mode/interfaces-pseudo-ethernet.py7
-rwxr-xr-xsrc/conf_mode/interfaces-tunnel.py8
-rwxr-xr-xsrc/conf_mode/interfaces-vxlan.py7
-rwxr-xr-xsrc/conf_mode/interfaces-wireguard.py7
-rwxr-xr-xsrc/conf_mode/interfaces-wireless.py7
-rwxr-xr-xsrc/conf_mode/interfaces-wirelessmodem.py7
-rwxr-xr-xsrc/conf_mode/ipsec-settings.py7
-rwxr-xr-xsrc/conf_mode/lldp.py7
-rwxr-xr-xsrc/conf_mode/nat.py7
-rwxr-xr-xsrc/conf_mode/ntp.py7
-rwxr-xr-xsrc/conf_mode/protocols_igmp.py7
-rwxr-xr-xsrc/conf_mode/protocols_mpls.py7
-rwxr-xr-xsrc/conf_mode/protocols_pim.py7
-rwxr-xr-xsrc/conf_mode/protocols_rip.py7
-rwxr-xr-xsrc/conf_mode/protocols_static_multicast.py7
-rwxr-xr-xsrc/conf_mode/salt-minion.py7
-rwxr-xr-xsrc/conf_mode/service_console-server.py7
-rwxr-xr-xsrc/conf_mode/service_ids_fastnetmon.py7
-rwxr-xr-xsrc/conf_mode/service_ipoe-server.py7
-rwxr-xr-xsrc/conf_mode/service_mdns-repeater.py7
-rwxr-xr-xsrc/conf_mode/service_pppoe-server.py7
-rwxr-xr-xsrc/conf_mode/service_router-advert.py7
-rwxr-xr-xsrc/conf_mode/ssh.py7
-rwxr-xr-xsrc/conf_mode/system-ip.py7
-rwxr-xr-xsrc/conf_mode/system-ipv6.py7
-rwxr-xr-xsrc/conf_mode/system-login-banner.py7
-rwxr-xr-xsrc/conf_mode/system-login.py7
-rwxr-xr-xsrc/conf_mode/system-options.py7
-rwxr-xr-xsrc/conf_mode/system-syslog.py7
-rwxr-xr-xsrc/conf_mode/system-timezone.py7
-rwxr-xr-xsrc/conf_mode/system-wifi-regdom.py7
-rwxr-xr-xsrc/conf_mode/system_console.py7
-rwxr-xr-xsrc/conf_mode/system_lcd.py7
-rwxr-xr-xsrc/conf_mode/task_scheduler.py7
-rwxr-xr-xsrc/conf_mode/tftp_server.py7
-rwxr-xr-xsrc/conf_mode/vpn_l2tp.py7
-rwxr-xr-xsrc/conf_mode/vpn_pptp.py7
-rwxr-xr-xsrc/conf_mode/vpn_sstp.py7
-rwxr-xr-xsrc/conf_mode/vrf.py7
-rwxr-xr-xsrc/conf_mode/vrrp.py7
-rwxr-xr-xsrc/conf_mode/vyos_cert.py7
-rwxr-xr-xsrc/services/vyos-configd224
-rw-r--r--src/shim/Makefile20
-rw-r--r--src/shim/mkjson/LICENSE21
-rw-r--r--src/shim/mkjson/makefile30
-rw-r--r--src/shim/mkjson/mkjson.c307
-rw-r--r--src/shim/mkjson/mkjson.h50
-rw-r--r--src/shim/vyshim.c287
-rw-r--r--src/systemd/vyos-configd.service27
82 files changed, 1780 insertions, 129 deletions
diff --git a/Makefile b/Makefile
index 5b7e4da63..e85835eec 100644
--- a/Makefile
+++ b/Makefile
@@ -2,6 +2,9 @@ TMPL_DIR := templates-cfg
OP_TMPL_DIR := templates-op
BUILD_DIR := build
DATA_DIR := data
+SHIM_DIR := src/shim
+CC := gcc
+LIBS := -lzmq
CFLAGS :=
src = $(wildcard interface-definitions/*.xml.in)
@@ -114,14 +117,19 @@ op_mode_definitions:
component_versions: $(BUILD_DIR) $(obj)
$(CURDIR)/scripts/build-component-versions $(BUILD_DIR)/interface-definitions $(DATA_DIR)
+.PHONY: vyshim
+vyshim:
+ $(MAKE) -C $(SHIM_DIR)
+
.PHONY: all
-all: clean interface_definitions op_mode_definitions component_versions
+all: clean interface_definitions op_mode_definitions component_versions vyshim
.PHONY: clean
clean:
rm -rf $(BUILD_DIR)
rm -rf $(TMPL_DIR)
rm -rf $(OP_TMPL_DIR)
+ $(MAKE) -C $(SHIM_DIR) clean
.PHONY: test
test:
diff --git a/data/configd-include.json b/data/configd-include.json
new file mode 100644
index 000000000..11d550f59
--- /dev/null
+++ b/data/configd-include.json
@@ -0,0 +1,64 @@
+[
+"bcast_relay.py",
+"dhcp_relay.py",
+"dhcp_server.py",
+"dhcpv6_relay.py",
+"dhcpv6_server.py",
+"dynamic_dns.py",
+"firewall_options.py",
+"host_name.py",
+"http-api.py",
+"https.py",
+"igmp_proxy.py",
+"intel_qat.py",
+"interfaces-bonding.py",
+"interfaces-bridge.py",
+"interfaces-dummy.py",
+"interfaces-ethernet.py",
+"interfaces-geneve.py",
+"interfaces-l2tpv3.py",
+"interfaces-loopback.py",
+"interfaces-macsec.py",
+"interfaces-openvpn.py",
+"interfaces-pppoe.py",
+"interfaces-pseudo-ethernet.py",
+"interfaces-tunnel.py",
+"interfaces-vxlan.py",
+"interfaces-wireguard.py",
+"interfaces-wireless.py",
+"interfaces-wirelessmodem.py",
+"ipsec-settings.py",
+"lldp.py",
+"nat.py",
+"ntp.py",
+"protocols_igmp.py",
+"protocols_mpls.py",
+"protocols_pim.py",
+"protocols_rip.py",
+"protocols_static_multicast.py",
+"salt-minion.py",
+"service_console-server.py",
+"service_ids_fastnetmon.py",
+"service_ipoe-server.py",
+"service_mdns-repeater.py",
+"service_pppoe-server.py",
+"service_router-advert.py",
+"ssh.py",
+"system-ip.py",
+"system-ipv6.py",
+"system-login-banner.py",
+"system-options.py",
+"system-syslog.py",
+"system-timezone.py",
+"system-wifi-regdom.py",
+"system_console.py",
+"system_lcd.py",
+"task_scheduler.py",
+"tftp_server.py",
+"vpn_l2tp.py",
+"vpn_pptp.py",
+"vpn_sstp.py",
+"vrf.py",
+"vrrp.py",
+"vyos_cert.py"
+] \ No newline at end of file
diff --git a/data/vyos-configd-env-set b/data/vyos-configd-env-set
new file mode 100644
index 000000000..d6d421eba
--- /dev/null
+++ b/data/vyos-configd-env-set
@@ -0,0 +1,2 @@
+#
+export vyshim=/usr/sbin/vyshim
diff --git a/data/vyos-configd-env-unset b/data/vyos-configd-env-unset
new file mode 100644
index 000000000..9616f9858
--- /dev/null
+++ b/data/vyos-configd-env-unset
@@ -0,0 +1,2 @@
+#
+unset vyshim
diff --git a/debian/control b/debian/control
index 65f3be228..d9663d07b 100644
--- a/debian/control
+++ b/debian/control
@@ -6,6 +6,7 @@ Build-Depends:
debhelper (>= 9),
fakeroot,
libvyosconfig0 (>= 0.0.7),
+ libzmq3-dev,
python3,
python3-coverage,
python3-lxml,
diff --git a/debian/rules b/debian/rules
index 58bafd333..6b982fd8e 100755
--- a/debian/rules
+++ b/debian/rules
@@ -33,6 +33,7 @@ override_dh_auto_install:
mkdir -p $(DIR)/$(VYOS_SBIN_DIR)
mkdir -p $(DIR)/$(VYOS_BIN_DIR)
cp -r src/utils/* $(DIR)/$(VYOS_BIN_DIR)
+ cp src/shim/vyshim $(DIR)/$(VYOS_SBIN_DIR)
# Install conf mode scripts
mkdir -p $(DIR)/$(VYOS_LIBEXEC_DIR)/conf_mode
diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install
index 6cd678442..6d5026e91 100644
--- a/debian/vyos-1x.install
+++ b/debian/vyos-1x.install
@@ -7,6 +7,7 @@ etc/udev
etc/vyos
lib/
opt/
+usr/sbin/vyshim
usr/bin/initial-setup
usr/bin/vyos-config-file-query
usr/bin/vyos-config-to-commands
diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py
index 0994fd974..6e4214360 100644
--- a/python/vyos/configsession.py
+++ b/python/vyos/configsession.py
@@ -17,6 +17,8 @@ import re
import sys
import subprocess
+from vyos.util import call
+
CLI_SHELL_API = '/bin/cli-shell-api'
SET = '/opt/vyatta/sbin/my_set'
DELETE = '/opt/vyatta/sbin/my_delete'
@@ -69,6 +71,11 @@ def inject_vyos_env(env):
env['vyos_sbin_dir'] = '/usr/sbin'
env['vyos_validators_dir'] = '/usr/libexec/vyos/validators'
+ # if running the vyos-configd daemon, inject the vyshim env var
+ ret = call('systemctl is-active --quiet vyos-configd.service')
+ if not ret:
+ env['vyshim'] = '/usr/sbin/vyshim'
+
return env
diff --git a/scripts/build-command-templates b/scripts/build-command-templates
index 457adbec2..d6585b0cc 100755
--- a/scripts/build-command-templates
+++ b/scripts/build-command-templates
@@ -225,11 +225,13 @@ def make_node_def(props):
if "constraint" in props:
node_def += "syntax:expression: {0}\n".format(props["constraint"])
+ shim = '${vyshim}'
+
if "owner" in props:
if "tag" in props:
- node_def += "end: sudo sh -c \"VYOS_TAGNODE_VALUE='$VAR(@)' {0}\"\n".format(props["owner"])
+ node_def += "end: sudo sh -c \"{1} VYOS_TAGNODE_VALUE='$VAR(@)' {0}\"\n".format(props["owner"], shim)
else:
- node_def += "end: sudo sh -c \"{0}\"\n".format(props["owner"])
+ node_def += "end: sudo sh -c \"{1} {0}\"\n".format(props["owner"], shim)
if debug:
print("The contents of the node.def file:\n", node_def)
diff --git a/scripts/update-configd-include-file b/scripts/update-configd-include-file
new file mode 100755
index 000000000..6615e21ff
--- /dev/null
+++ b/scripts/update-configd-include-file
@@ -0,0 +1,298 @@
+#!/usr/bin/env python3
+###
+# A simple script for safely editing configd-include.json, the list of
+# scripts which are off-loaded to be run by the daemon.
+# Usage:
+# update-configd-include-file --add script1.py script2.py ...
+# --remove scriptA.py scriptB.py ...
+#
+# Additionally, it offers optional sanity checks by examining the signatures
+# of functions and placement of Config instance for consistency with configd
+# requirements.
+# Usage:
+# update-configd-include-file --check-current
+# to check the current include list
+# update-configd-include-file --check-file
+# to check arbitrary conf_mode scripts
+#
+# Note that this feature is the basis for the configd smoketest, but it is of
+# limited use in this script, as it requires an environment that has all script
+# (python) dependencies installed (e.g. installed image) so that the script may
+# be imported for introspection. Nonetheless, for testing and development, it has
+# its uses.
+
+import os
+import sys
+import json
+import argparse
+import datetime
+import importlib.util
+from inspect import signature, getsource
+
+from vyos.defaults import directories
+from vyos.version import get_version
+from vyos.util import cmd
+
+# Defaults
+
+installed_image = False
+
+include_file = 'configd-include.json'
+build_relative_include_file = '../data/configd-include.json'
+dirname = os.path.dirname(__file__)
+
+build_location_include_file = os.path.join(dirname, build_relative_include_file)
+image_location_include_file = os.path.join(directories['data'], include_file)
+
+build_relative_conf_dir = '../src/conf_mode'
+
+build_location_conf_dir = os.path.join(dirname, build_relative_conf_dir)
+image_location_conf_dir = directories['conf_mode']
+
+# Get arguments
+
+parser = argparse.ArgumentParser(description='Add or remove scripts from the list of scripts to be run be daemon')
+parser.add_argument('--add', nargs='*', default=[],
+ help='scripts to add to configd include list')
+parser.add_argument('--remove', nargs='*', default=[],
+ help='scripts to remove from configd include list')
+parser.add_argument('--show-diff', action='store_true',
+ help='show list of conf_mode scripts not in include list')
+parser.add_argument('--check-file', nargs='*', default=[],
+ help='check files for suitability to run under daemon')
+parser.add_argument('--check-current', action="store_true",
+ help='check current include list for suitability to run under daemon')
+
+args = vars(parser.parse_args())
+
+# Check if we are running within installed image; since this script is not
+# part of the distribution, there is no need to check if live cd
+if get_version():
+ installed_image = True
+
+if installed_image:
+ include_file = image_location_include_file
+ conf_dir = image_location_conf_dir
+else:
+ include_file = build_location_include_file
+ conf_dir = build_location_conf_dir
+
+# Utilities for checking function signature and body
+def import_script(s: str):
+ """
+ A compact form of the import code in vyos-configd
+ """
+ path = os.path.join(conf_dir, s)
+ if not os.path.exists(path):
+ print(f"script {s} is not in conf_mode directory")
+ return None
+
+ name = os.path.splitext(s)[0].replace('-', '_')
+
+ spec = importlib.util.spec_from_file_location(name, path)
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+
+ return module
+
+funcs = { 'get_config': False,
+ 'verify': False,
+ 'generate': False,
+ 'apply': False
+ }
+
+def check_signatures(s: str) -> bool:
+ """
+ Basic sanity check: script standard functions should all take one
+ argument, including get_config(config=None).
+ """
+ funcd = dict(funcs)
+ for i in list(funcd):
+ m = import_script(s)
+ f = getattr(m, i, None)
+ if not f:
+ funcd[i] = True
+ continue
+ sig = signature(f)
+ params = sig.parameters
+ if len(params) != 1:
+ continue
+ if i == 'get_config':
+ for p in params.values():
+ funcd[i] = True if (p.default is None) else False
+ else:
+ funcd[i] = True
+
+ res = True
+
+ for k, v in funcd.items():
+ if v is False:
+ if k == 'get_config':
+ print(f"function '{k}' will need the standard modification")
+ else:
+ print(f"function '{k}' in script '{s}' has wrong signature")
+ res = False
+
+ return res
+
+def check_instance_per_function(s: str) -> bool:
+ """
+ The standard function 'get_config' should have one instantiation of Config;
+ all other standard functions, zero.
+ """
+ funcd = dict(funcs)
+ for i in list(funcd):
+ m = import_script(s)
+ f = getattr(m, i, None)
+ if not f:
+ funcd[i] = True
+ continue
+ str_f = getsource(f)
+ n = str_f.count('Config()')
+ if n == 1 and i == 'get_config':
+ funcd[i] = True
+ if n == 0 and i != 'get_config':
+ funcd[i] = True
+
+ res = True
+
+ for k, v in funcd.items():
+ if v is False:
+ fi = 'zero' if k == 'get_config' else 'non-zero'
+ print(f"function '{k}' in script '{s}' has {fi} instances of Config")
+ res = False
+
+ return res
+
+def check_instance_total(s: str) -> bool:
+ """
+ A script should have at most one instantiation of Config.
+ """
+ m = import_script(s)
+ str_m = getsource(m)
+ n = str_m.count('Config()')
+ if n != 1:
+ print(f"instance of Config outside of 'get_config' in script '{s}'")
+ return False
+
+ return True
+
+def check_config_modification(s: str) -> bool:
+ """
+ Modification to the session config from within a script is necessary in
+ certain cases, but the script should then run as stand-alone.
+ """
+ m = import_script(s)
+ str_m = getsource(m)
+ n = str_m.count('my_set')
+ if n != 0:
+ print(f"modification of config within script")
+ return False
+
+ return True
+
+def check_viability(s: str) -> bool:
+ """
+ Check existence, and if on installed image, signatures, instances of
+ Config, and modification of session config
+ """
+ path = os.path.join(conf_dir, s)
+ if not os.path.exists(path):
+ print(f"script {s} is not in conf_mode directory")
+ return False
+
+ if not installed_image:
+ if args['check_file'] or args['check_current']:
+ print(f"In order to check script viability for offload, run this script on installed image")
+ return True
+
+ r1 = check_signatures(s)
+ r2 = check_instance_per_function(s)
+ r3 = check_instance_total(s)
+ r4 = check_config_modification(s)
+
+ if not r1 or not r2 or not r3 or not r4:
+ return False
+
+ return True
+
+def check_file(s: str) -> bool:
+ if not check_viability(s):
+ return False
+ return True
+
+def check_files(l: list) -> int:
+ check_list = l[:]
+ res = 0
+ for s in check_list:
+ if not check_file(s):
+ res = 1
+ return res
+
+# Status
+
+def show_diff(l: list):
+ print(conf_dir)
+ (_, _, filenames) = next(iter(os.walk(conf_dir)))
+ filenames.sort()
+ res = [i for i in filenames if i not in l]
+ print(res)
+
+# Read configd-include.json and add/remove/check/show scripts
+
+with open(include_file, 'r') as f:
+ try:
+ include_list = json.load(f)
+ except OSError as e:
+ print(f"configd include file error: {e}")
+ sys.exit(1)
+ except json.JSONDecodeError as e:
+ print(f"JSON load error: {e}")
+ sys.exit(1)
+
+if args['show_diff']:
+ show_diff(include_list)
+ sys.exit(0)
+
+if args['check_file']:
+ l = args['check_file']
+ ret = check_files(l)
+ if not ret:
+ print('pass')
+ sys.exit(ret)
+
+if args['check_current']:
+ ret = check_files(include_list)
+ if not ret:
+ print('pass')
+ sys.exit(ret)
+
+add_list = args['add']
+# drop redundencies
+add_list = [i for i in add_list if i not in include_list]
+# prune entries that don't pass check
+add_list = [i for i in add_list if check_file(i)]
+
+remove_list = args['remove']
+
+if not add_list and not remove_list:
+ sys.exit(0)
+
+separator = '.'
+backup_file_name = separator.join([include_file,
+ '{0:%Y-%m-%d-%H%M%S}'.format(datetime.datetime.now()), 'bak'])
+
+cmd(f'cp -p {include_file} {backup_file_name}')
+
+if add_list:
+ include_list.extend(add_list)
+ include_list.sort()
+if remove_list:
+ include_list = [i for i in include_list if i not in remove_list]
+
+with open(include_file, 'w') as f:
+ try:
+ json.dump(include_list, f, indent=0)
+ except OSError as e:
+ print(f"error writing configd include file: {e}")
+ sys.exit(1)
diff --git a/smoketest/scripts/cli/test_configd_inspect.py b/smoketest/scripts/cli/test_configd_inspect.py
new file mode 100755
index 000000000..5181187e9
--- /dev/null
+++ b/smoketest/scripts/cli/test_configd_inspect.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 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/>.
+
+import os
+import json
+import unittest
+import warnings
+import importlib.util
+from inspect import signature, getsource
+from functools import wraps
+
+from vyos.defaults import directories
+
+INC_FILE = '/usr/share/vyos/configd-include.json'
+CONF_DIR = directories['conf_mode']
+
+f_list = ['get_config', 'verify', 'generate', 'apply']
+
+def import_script(s):
+ path = os.path.join(CONF_DIR, s)
+ name = os.path.splitext(s)[0].replace('-', '_')
+ spec = importlib.util.spec_from_file_location(name, path)
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return module
+
+# importing conf_mode scripts imports jinja2 with deprecation warning
+def ignore_deprecation_warning(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ f(*args, **kwargs)
+ return decorated_function
+
+class TestConfigdInclude(unittest.TestCase):
+ def setUp(self):
+ with open(INC_FILE) as f:
+ self.inc_list = json.load(f)
+
+ @ignore_deprecation_warning
+ def test_signatures(self):
+ for s in self.inc_list:
+ m = import_script(s)
+ for i in f_list:
+ f = getattr(m, i, None)
+ if not f:
+ continue
+ sig = signature(f)
+ par = sig.parameters
+ l = len(par)
+ self.assertEqual(l, 1,
+ f"'{s}': '{i}' incorrect signature")
+ if i == 'get_config':
+ for p in par.values():
+ self.assertTrue(p.default is None,
+ f"'{s}': '{i}' incorrect signature")
+
+ @ignore_deprecation_warning
+ def test_function_instance(self):
+ for s in self.inc_list:
+ m = import_script(s)
+ for i in f_list:
+ f = getattr(m, i, None)
+ if not f:
+ continue
+ str_f = getsource(f)
+ n = str_f.count('Config()')
+ if i == 'get_config':
+ self.assertEqual(n, 1,
+ f"'{s}': '{i}' no instance of Config")
+ if i != 'get_config':
+ self.assertEqual(n, 0,
+ f"'{s}': '{i}' instance of Config")
+
+ @ignore_deprecation_warning
+ def test_file_instance(self):
+ for s in self.inc_list:
+ m = import_script(s)
+ str_m = getsource(m)
+ n = str_m.count('Config()')
+ self.assertEqual(n, 1,
+ f"'{s}' more than one instance of Config")
+
+ @ignore_deprecation_warning
+ def test_config_modification(self):
+ for s in self.inc_list:
+ m = import_script(s)
+ str_m = getsource(m)
+ n = str_m.count('my_set')
+ self.assertEqual(n, 0, f"'{s}' modifies config")
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/src/conf_mode/bcast_relay.py b/src/conf_mode/bcast_relay.py
index a3e141a00..4a47b9246 100755
--- a/src/conf_mode/bcast_relay.py
+++ b/src/conf_mode/bcast_relay.py
@@ -29,8 +29,11 @@ airbag.enable()
config_file_base = r'/etc/default/udp-broadcast-relay'
-def get_config():
- conf = Config()
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['service', 'broadcast-relay']
relay = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
diff --git a/src/conf_mode/dhcp_relay.py b/src/conf_mode/dhcp_relay.py
index f093a005e..352865b9d 100755
--- a/src/conf_mode/dhcp_relay.py
+++ b/src/conf_mode/dhcp_relay.py
@@ -36,9 +36,12 @@ default_config_data = {
'relay_agent_packets': 'forward'
}
-def get_config():
+def get_config(config=None):
relay = default_config_data
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
if not conf.exists(['service', 'dhcp-relay']):
return None
else:
diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py
index 0eaa14c5b..fd4e2ec61 100755
--- a/src/conf_mode/dhcp_server.py
+++ b/src/conf_mode/dhcp_server.py
@@ -126,9 +126,12 @@ def dhcp_static_route(static_subnet, static_router):
return string
-def get_config():
+def get_config(config=None):
dhcp = default_config_data
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
if not conf.exists('service dhcp-server'):
return None
else:
diff --git a/src/conf_mode/dhcpv6_relay.py b/src/conf_mode/dhcpv6_relay.py
index 6ef290bf0..d4212b8be 100755
--- a/src/conf_mode/dhcpv6_relay.py
+++ b/src/conf_mode/dhcpv6_relay.py
@@ -35,9 +35,12 @@ default_config_data = {
'options': [],
}
-def get_config():
+def get_config(config=None):
relay = deepcopy(default_config_data)
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
if not conf.exists('service dhcpv6-relay'):
return None
else:
diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py
index 53c8358a5..4ce4cada1 100755
--- a/src/conf_mode/dhcpv6_server.py
+++ b/src/conf_mode/dhcpv6_server.py
@@ -37,9 +37,12 @@ default_config_data = {
'shared_network': []
}
-def get_config():
+def get_config(config=None):
dhcpv6 = deepcopy(default_config_data)
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['service', 'dhcpv6-server']
if not conf.exists(base):
return None
diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py
index 5b1883c03..57c910a68 100755
--- a/src/conf_mode/dynamic_dns.py
+++ b/src/conf_mode/dynamic_dns.py
@@ -50,9 +50,12 @@ default_config_data = {
'deleted': False
}
-def get_config():
+def get_config(config=None):
dyndns = deepcopy(default_config_data)
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
base_level = ['service', 'dns', 'dynamic']
if not conf.exists(base_level):
diff --git a/src/conf_mode/firewall_options.py b/src/conf_mode/firewall_options.py
index 71b2a98b3..67bf5d0e2 100755
--- a/src/conf_mode/firewall_options.py
+++ b/src/conf_mode/firewall_options.py
@@ -32,9 +32,12 @@ default_config_data = {
'new_chain6': False
}
-def get_config():
+def get_config(config=None):
opts = copy.deepcopy(default_config_data)
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
if not conf.exists('firewall options'):
# bail out early
return opts
diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py
index 9d66bd434..f4c75c257 100755
--- a/src/conf_mode/host_name.py
+++ b/src/conf_mode/host_name.py
@@ -43,8 +43,11 @@ default_config_data = {
hostsd_tag = 'system'
-def get_config():
- conf = Config()
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
hosts = copy.deepcopy(default_config_data)
diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py
index b8a084a40..472eb77e4 100755
--- a/src/conf_mode/http-api.py
+++ b/src/conf_mode/http-api.py
@@ -39,7 +39,7 @@ dependencies = [
'https.py',
]
-def get_config():
+def get_config(config=None):
http_api = deepcopy(vyos.defaults.api_data)
x = http_api.get('api_keys')
if x is None:
@@ -48,7 +48,11 @@ def get_config():
default_key = x[0]
keys_added = False
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
if not conf.exists('service https api'):
return None
else:
diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py
index a13f131ab..dc51cb117 100755
--- a/src/conf_mode/https.py
+++ b/src/conf_mode/https.py
@@ -47,8 +47,12 @@ default_server_block = {
'certbot' : False
}
-def get_config():
- conf = Config()
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
if not conf.exists('service https'):
return None
diff --git a/src/conf_mode/igmp_proxy.py b/src/conf_mode/igmp_proxy.py
index 49aea9b7f..754f46566 100755
--- a/src/conf_mode/igmp_proxy.py
+++ b/src/conf_mode/igmp_proxy.py
@@ -36,9 +36,12 @@ default_config_data = {
'interfaces': [],
}
-def get_config():
+def get_config(config=None):
igmp_proxy = deepcopy(default_config_data)
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['protocols', 'igmp-proxy']
if not conf.exists(base):
return None
diff --git a/src/conf_mode/intel_qat.py b/src/conf_mode/intel_qat.py
index 742f09a54..1e5101a9f 100755
--- a/src/conf_mode/intel_qat.py
+++ b/src/conf_mode/intel_qat.py
@@ -30,8 +30,11 @@ airbag.enable()
# Define for recovering
gl_ipsec_conf = None
-def get_config():
- c = Config()
+def get_config(config=None):
+ if config:
+ c = config
+ else:
+ c = Config()
config_data = {
'qat_conf' : None,
'ipsec_conf' : None,
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
index 3b238f1ea..16e6e4f6e 100755
--- a/src/conf_mode/interfaces-bonding.py
+++ b/src/conf_mode/interfaces-bonding.py
@@ -53,12 +53,15 @@ def get_bond_mode(mode):
else:
raise ConfigError(f'invalid bond mode "{mode}"')
-def get_config():
+def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
interface name will be added or a deleted flag
"""
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['interfaces', 'bonding']
bond = get_interface_dict(conf, base)
diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py
index ee8e85e73..47c8c05f9 100755
--- a/src/conf_mode/interfaces-bridge.py
+++ b/src/conf_mode/interfaces-bridge.py
@@ -34,12 +34,15 @@ from vyos import ConfigError
from vyos import airbag
airbag.enable()
-def get_config():
+def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
interface name will be added or a deleted flag
"""
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['interfaces', 'bridge']
bridge = get_interface_dict(conf, base)
diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py
index 8df86c8ea..44fc9cb9e 100755
--- a/src/conf_mode/interfaces-dummy.py
+++ b/src/conf_mode/interfaces-dummy.py
@@ -28,12 +28,15 @@ from vyos import ConfigError
from vyos import airbag
airbag.enable()
-def get_config():
+def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
interface name will be added or a deleted flag
"""
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['interfaces', 'dummy']
dummy = get_interface_dict(conf, base)
return dummy
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index 10758e35a..a8df64cce 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -30,12 +30,15 @@ from vyos import ConfigError
from vyos import airbag
airbag.enable()
-def get_config():
+def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
interface name will be added or a deleted flag
"""
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['interfaces', 'ethernet']
ethernet = get_interface_dict(conf, base)
return ethernet
diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py
index 1104bd3c0..cc2cf025a 100755
--- a/src/conf_mode/interfaces-geneve.py
+++ b/src/conf_mode/interfaces-geneve.py
@@ -30,12 +30,15 @@ from vyos import ConfigError
from vyos import airbag
airbag.enable()
-def get_config():
+def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
interface name will be added or a deleted flag
"""
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['interfaces', 'geneve']
geneve = get_interface_dict(conf, base)
return geneve
diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py
index 0978df5b6..8250a3df8 100755
--- a/src/conf_mode/interfaces-l2tpv3.py
+++ b/src/conf_mode/interfaces-l2tpv3.py
@@ -35,12 +35,15 @@ airbag.enable()
k_mod = ['l2tp_eth', 'l2tp_netlink', 'l2tp_ip', 'l2tp_ip6']
-def get_config():
+def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
interface name will be added or a deleted flag
"""
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['interfaces', 'l2tpv3']
l2tpv3 = get_interface_dict(conf, base)
diff --git a/src/conf_mode/interfaces-loopback.py b/src/conf_mode/interfaces-loopback.py
index 0398cd591..30a27abb4 100755
--- a/src/conf_mode/interfaces-loopback.py
+++ b/src/conf_mode/interfaces-loopback.py
@@ -25,12 +25,15 @@ from vyos import ConfigError
from vyos import airbag
airbag.enable()
-def get_config():
+def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
interface name will be added or a deleted flag
"""
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['interfaces', 'loopback']
loopback = get_interface_dict(conf, base)
return loopback
diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py
index ca15212d4..2866ccc0a 100755
--- a/src/conf_mode/interfaces-macsec.py
+++ b/src/conf_mode/interfaces-macsec.py
@@ -35,12 +35,15 @@ airbag.enable()
# XXX: wpa_supplicant works on the source interface
wpa_suppl_conf = '/run/wpa_supplicant/{source_interface}.conf'
-def get_config():
+def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
interface name will be added or a deleted flag
"""
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['interfaces', 'macsec']
macsec = get_interface_dict(conf, base)
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 1420b4116..958b305dd 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -192,9 +192,12 @@ def getDefaultServer(network, topology, devtype):
return server
-def get_config():
+def get_config(config=None):
openvpn = deepcopy(default_config_data)
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
# determine tagNode instance
if 'VYOS_TAGNODE_VALUE' not in os.environ:
diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py
index 901ea769c..1b4b9e4ee 100755
--- a/src/conf_mode/interfaces-pppoe.py
+++ b/src/conf_mode/interfaces-pppoe.py
@@ -30,12 +30,15 @@ from vyos import ConfigError
from vyos import airbag
airbag.enable()
-def get_config():
+def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
interface name will be added or a deleted flag
"""
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['interfaces', 'pppoe']
pppoe = get_interface_dict(conf, base)
diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py
index fe2d7b1be..59edca1cc 100755
--- a/src/conf_mode/interfaces-pseudo-ethernet.py
+++ b/src/conf_mode/interfaces-pseudo-ethernet.py
@@ -34,12 +34,15 @@ from vyos import ConfigError
from vyos import airbag
airbag.enable()
-def get_config():
+def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at
least the interface name will be added or a deleted flag
"""
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['interfaces', 'pseudo-ethernet']
peth = get_interface_dict(conf, base)
diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
index ea15a7fb7..11d8d6edc 100755
--- a/src/conf_mode/interfaces-tunnel.py
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -397,12 +397,16 @@ def ip_proto (afi):
return 6 if afi == IP6 else 4
-def get_config():
+def get_config(config=None):
ifname = os.environ.get('VYOS_TAGNODE_VALUE','')
if not ifname:
raise ConfigError('Interface not specified')
- config = Config()
+ if config:
+ config = config
+ else:
+ config = Config()
+
conf = ConfigurationState(config, ['interfaces', 'tunnel ', ifname], default_config_data)
options = conf.options
changes = conf.changes
diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index 47c0bdcb8..bea3aa25b 100755
--- a/src/conf_mode/interfaces-vxlan.py
+++ b/src/conf_mode/interfaces-vxlan.py
@@ -30,12 +30,15 @@ from vyos import ConfigError
from vyos import airbag
airbag.enable()
-def get_config():
+def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
interface name will be added or a deleted flag
"""
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['interfaces', 'vxlan']
vxlan = get_interface_dict(conf, base)
diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py
index 8b64cde4d..e7c22da1a 100755
--- a/src/conf_mode/interfaces-wireguard.py
+++ b/src/conf_mode/interfaces-wireguard.py
@@ -33,12 +33,15 @@ from vyos import ConfigError
from vyos import airbag
airbag.enable()
-def get_config():
+def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
interface name will be added or a deleted flag
"""
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['interfaces', 'wireguard']
wireguard = get_interface_dict(conf, base)
diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py
index b6f247952..9861f72db 100755
--- a/src/conf_mode/interfaces-wireless.py
+++ b/src/conf_mode/interfaces-wireless.py
@@ -64,12 +64,15 @@ def find_other_stations(conf, base, ifname):
conf.set_level(old_level)
return dict
-def get_config():
+def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
interface name will be added or a deleted flag
"""
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['interfaces', 'wireless']
wifi = get_interface_dict(conf, base)
diff --git a/src/conf_mode/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py
index 6d168d918..7d8110096 100755
--- a/src/conf_mode/interfaces-wirelessmodem.py
+++ b/src/conf_mode/interfaces-wirelessmodem.py
@@ -31,12 +31,15 @@ airbag.enable()
k_mod = ['option', 'usb_wwan', 'usbserial']
-def get_config():
+def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
interface name will be added or a deleted flag
"""
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['interfaces', 'wirelessmodem']
wwan = get_interface_dict(conf, base)
return wwan
diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py
index 015d1a480..11a5b7aaa 100755
--- a/src/conf_mode/ipsec-settings.py
+++ b/src/conf_mode/ipsec-settings.py
@@ -41,8 +41,11 @@ delim_ipsec_l2tp_begin = "### VyOS L2TP VPN Begin ###"
delim_ipsec_l2tp_end = "### VyOS L2TP VPN End ###"
charon_pidfile = "/var/run/charon.pid"
-def get_config():
- config = Config()
+def get_config(config=None):
+ if config:
+ config = config
+ else:
+ config = Config()
data = {"install_routes": "yes"}
if config.exists("vpn ipsec options disable-route-autoinstall"):
diff --git a/src/conf_mode/lldp.py b/src/conf_mode/lldp.py
index 1b539887a..6b645857a 100755
--- a/src/conf_mode/lldp.py
+++ b/src/conf_mode/lldp.py
@@ -146,9 +146,12 @@ def get_location(config):
return intfs_location
-def get_config():
+def get_config(config=None):
lldp = deepcopy(default_config_data)
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
if not conf.exists(base):
return None
else:
diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py
index f79f0f42b..eb634fd78 100755
--- a/src/conf_mode/nat.py
+++ b/src/conf_mode/nat.py
@@ -167,9 +167,12 @@ def parse_configuration(conf, source_dest):
return ruleset
-def get_config():
+def get_config(config=None):
nat = deepcopy(default_config_data)
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
# read in current nftable (once) for further processing
tmp = cmd('nft -j list table raw')
diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py
index bba8f87a4..d6453ec83 100755
--- a/src/conf_mode/ntp.py
+++ b/src/conf_mode/ntp.py
@@ -27,8 +27,11 @@ airbag.enable()
config_file = r'/etc/ntp.conf'
systemd_override = r'/etc/systemd/system/ntp.service.d/override.conf'
-def get_config():
- conf = Config()
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['system', 'ntp']
ntp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
diff --git a/src/conf_mode/protocols_igmp.py b/src/conf_mode/protocols_igmp.py
index ca148fd6a..6f4fc784d 100755
--- a/src/conf_mode/protocols_igmp.py
+++ b/src/conf_mode/protocols_igmp.py
@@ -29,8 +29,11 @@ airbag.enable()
config_file = r'/tmp/igmp.frr'
-def get_config():
- conf = Config()
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
igmp_conf = {
'igmp_conf' : False,
'old_ifaces' : {},
diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py
index bcb16fa04..e515490d0 100755
--- a/src/conf_mode/protocols_mpls.py
+++ b/src/conf_mode/protocols_mpls.py
@@ -29,8 +29,11 @@ config_file = r'/tmp/ldpd.frr'
def sysctl(name, value):
call('sysctl -wq {}={}'.format(name, value))
-def get_config():
- conf = Config()
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
mpls_conf = {
'router_id' : None,
'mpls_ldp' : False,
diff --git a/src/conf_mode/protocols_pim.py b/src/conf_mode/protocols_pim.py
index 8aa324bac..6d333e19a 100755
--- a/src/conf_mode/protocols_pim.py
+++ b/src/conf_mode/protocols_pim.py
@@ -29,8 +29,11 @@ airbag.enable()
config_file = r'/tmp/pimd.frr'
-def get_config():
- conf = Config()
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
pim_conf = {
'pim_conf' : False,
'old_pim' : {
diff --git a/src/conf_mode/protocols_rip.py b/src/conf_mode/protocols_rip.py
index 95e8ce901..8ddd705f2 100755
--- a/src/conf_mode/protocols_rip.py
+++ b/src/conf_mode/protocols_rip.py
@@ -28,8 +28,11 @@ airbag.enable()
config_file = r'/tmp/ripd.frr'
-def get_config():
- conf = Config()
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['protocols', 'rip']
rip_conf = {
'rip_conf' : False,
diff --git a/src/conf_mode/protocols_static_multicast.py b/src/conf_mode/protocols_static_multicast.py
index 232d1e181..99157835a 100755
--- a/src/conf_mode/protocols_static_multicast.py
+++ b/src/conf_mode/protocols_static_multicast.py
@@ -30,8 +30,11 @@ airbag.enable()
config_file = r'/tmp/static_mcast.frr'
# Get configuration for static multicast route
-def get_config():
- conf = Config()
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
mroute = {
'old_mroute' : {},
'mroute' : {}
diff --git a/src/conf_mode/salt-minion.py b/src/conf_mode/salt-minion.py
index 3343d1247..841bf6a39 100755
--- a/src/conf_mode/salt-minion.py
+++ b/src/conf_mode/salt-minion.py
@@ -44,9 +44,12 @@ default_config_data = {
'master_key': ''
}
-def get_config():
+def get_config(config=None):
salt = deepcopy(default_config_data)
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['service', 'salt-minion']
if not conf.exists(base):
diff --git a/src/conf_mode/service_console-server.py b/src/conf_mode/service_console-server.py
index 613ec6879..0e5fc75b0 100755
--- a/src/conf_mode/service_console-server.py
+++ b/src/conf_mode/service_console-server.py
@@ -27,8 +27,11 @@ from vyos import ConfigError
config_file = r'/run/conserver/conserver.cf'
-def get_config():
- conf = Config()
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['service', 'console-server']
# Retrieve CLI representation as dictionary
diff --git a/src/conf_mode/service_ids_fastnetmon.py b/src/conf_mode/service_ids_fastnetmon.py
index d46f9578e..27d0ee60c 100755
--- a/src/conf_mode/service_ids_fastnetmon.py
+++ b/src/conf_mode/service_ids_fastnetmon.py
@@ -28,8 +28,11 @@ airbag.enable()
config_file = r'/etc/fastnetmon.conf'
networks_list = r'/etc/networks_list'
-def get_config():
- conf = Config()
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['service', 'ids', 'ddos-protection']
fastnetmon = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
return fastnetmon
diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py
index 553cc2e97..96cf932d1 100755
--- a/src/conf_mode/service_ipoe-server.py
+++ b/src/conf_mode/service_ipoe-server.py
@@ -55,8 +55,11 @@ default_config_data = {
'thread_cnt': get_half_cpus()
}
-def get_config():
- conf = Config()
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
base_path = ['service', 'ipoe-server']
if not conf.exists(base_path):
return None
diff --git a/src/conf_mode/service_mdns-repeater.py b/src/conf_mode/service_mdns-repeater.py
index 1a6b2c328..729518c96 100755
--- a/src/conf_mode/service_mdns-repeater.py
+++ b/src/conf_mode/service_mdns-repeater.py
@@ -28,8 +28,11 @@ airbag.enable()
config_file = r'/etc/default/mdns-repeater'
-def get_config():
- conf = Config()
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['service', 'mdns', 'repeater']
mdns = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
return mdns
diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py
index 39d34a7e2..45d3806d5 100755
--- a/src/conf_mode/service_pppoe-server.py
+++ b/src/conf_mode/service_pppoe-server.py
@@ -85,8 +85,11 @@ default_config_data = {
'thread_cnt': get_half_cpus()
}
-def get_config():
- conf = Config()
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
base_path = ['service', 'pppoe-server']
if not conf.exists(base_path):
return None
diff --git a/src/conf_mode/service_router-advert.py b/src/conf_mode/service_router-advert.py
index 4e1c432ab..687d7068f 100755
--- a/src/conf_mode/service_router-advert.py
+++ b/src/conf_mode/service_router-advert.py
@@ -29,8 +29,11 @@ airbag.enable()
config_file = r'/run/radvd/radvd.conf'
-def get_config():
- conf = Config()
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['service', 'router-advert']
rtradv = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py
index 7b262565a..a19fa72d8 100755
--- a/src/conf_mode/ssh.py
+++ b/src/conf_mode/ssh.py
@@ -31,8 +31,11 @@ airbag.enable()
config_file = r'/run/ssh/sshd_config'
systemd_override = r'/etc/systemd/system/ssh.service.d/override.conf'
-def get_config():
- conf = Config()
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['service', 'ssh']
if not conf.exists(base):
return None
diff --git a/src/conf_mode/system-ip.py b/src/conf_mode/system-ip.py
index 85f1e3771..64c9e6d05 100755
--- a/src/conf_mode/system-ip.py
+++ b/src/conf_mode/system-ip.py
@@ -35,9 +35,12 @@ default_config_data = {
def sysctl(name, value):
call('sysctl -wq {}={}'.format(name, value))
-def get_config():
+def get_config(config=None):
ip_opt = deepcopy(default_config_data)
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
conf.set_level('system ip')
if conf.exists(''):
if conf.exists('arp table-size'):
diff --git a/src/conf_mode/system-ipv6.py b/src/conf_mode/system-ipv6.py
index 3417c609d..f70ec2631 100755
--- a/src/conf_mode/system-ipv6.py
+++ b/src/conf_mode/system-ipv6.py
@@ -41,9 +41,12 @@ default_config_data = {
def sysctl(name, value):
call('sysctl -wq {}={}'.format(name, value))
-def get_config():
+def get_config(config=None):
ip_opt = deepcopy(default_config_data)
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
conf.set_level('system ipv6')
if conf.exists(''):
ip_opt['disable_addr_assignment'] = conf.exists('disable')
diff --git a/src/conf_mode/system-login-banner.py b/src/conf_mode/system-login-banner.py
index 5c0adc921..569010735 100755
--- a/src/conf_mode/system-login-banner.py
+++ b/src/conf_mode/system-login-banner.py
@@ -41,9 +41,12 @@ default_config_data = {
'motd': motd
}
-def get_config():
+def get_config(config=None):
banner = default_config_data
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
base_level = ['system', 'login', 'banner']
if not conf.exists(base_level):
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index b1dd583b5..2aca199f9 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.py
@@ -56,9 +56,12 @@ def get_local_users():
return local_users
-def get_config():
+def get_config(config=None):
login = default_config_data
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
base_level = ['system', 'login']
# We do not need to check if the nodes exist or not and bail out early
diff --git a/src/conf_mode/system-options.py b/src/conf_mode/system-options.py
index 0aacd19d8..6ac35a4ab 100755
--- a/src/conf_mode/system-options.py
+++ b/src/conf_mode/system-options.py
@@ -31,8 +31,11 @@ curlrc_config = r'/etc/curlrc'
ssh_config = r'/etc/ssh/ssh_config'
systemd_action_file = '/lib/systemd/system/ctrl-alt-del.target'
-def get_config():
- conf = Config()
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['system', 'options']
options = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
return options
diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py
index cfc1ca55f..d29109c41 100755
--- a/src/conf_mode/system-syslog.py
+++ b/src/conf_mode/system-syslog.py
@@ -27,8 +27,11 @@ from vyos.template import render
from vyos import airbag
airbag.enable()
-def get_config():
- c = Config()
+def get_config(config=None):
+ if config:
+ c = config
+ else:
+ c = Config()
if not c.exists('system syslog'):
return None
c.set_level('system syslog')
diff --git a/src/conf_mode/system-timezone.py b/src/conf_mode/system-timezone.py
index 0f4513122..4d9f017a6 100755
--- a/src/conf_mode/system-timezone.py
+++ b/src/conf_mode/system-timezone.py
@@ -29,9 +29,12 @@ default_config_data = {
'name': 'UTC'
}
-def get_config():
+def get_config(config=None):
tz = deepcopy(default_config_data)
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
if conf.exists('system time-zone'):
tz['name'] = conf.return_value('system time-zone')
diff --git a/src/conf_mode/system-wifi-regdom.py b/src/conf_mode/system-wifi-regdom.py
index 30ea89098..874f93923 100755
--- a/src/conf_mode/system-wifi-regdom.py
+++ b/src/conf_mode/system-wifi-regdom.py
@@ -34,9 +34,12 @@ default_config_data = {
'deleted' : False
}
-def get_config():
+def get_config(config=None):
regdom = deepcopy(default_config_data)
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['system', 'wifi-regulatory-domain']
# Check if interface has been removed
diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py
index 6f83335f3..b17818797 100755
--- a/src/conf_mode/system_console.py
+++ b/src/conf_mode/system_console.py
@@ -26,8 +26,11 @@ airbag.enable()
by_bus_dir = '/dev/serial/by-bus'
-def get_config():
- conf = Config()
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['system', 'console']
# retrieve configuration at once
diff --git a/src/conf_mode/system_lcd.py b/src/conf_mode/system_lcd.py
index 31a09252d..a540d1b9e 100755
--- a/src/conf_mode/system_lcd.py
+++ b/src/conf_mode/system_lcd.py
@@ -29,8 +29,11 @@ airbag.enable()
lcdd_conf = '/run/LCDd/LCDd.conf'
lcdproc_conf = '/run/lcdproc/lcdproc.conf'
-def get_config():
- conf = Config()
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['system', 'lcd']
lcd = conf.get_config_dict(base, key_mangling=('-', '_'),
get_first_key=True)
diff --git a/src/conf_mode/task_scheduler.py b/src/conf_mode/task_scheduler.py
index 51d8684cb..129be5d3c 100755
--- a/src/conf_mode/task_scheduler.py
+++ b/src/conf_mode/task_scheduler.py
@@ -53,8 +53,11 @@ def make_command(executable, arguments):
else:
return("sg vyattacfg \"{0}\"".format(executable))
-def get_config():
- conf = Config()
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
conf.set_level("system task-scheduler task")
task_names = conf.list_nodes("")
tasks = []
diff --git a/src/conf_mode/tftp_server.py b/src/conf_mode/tftp_server.py
index d31851bef..ad5ee9c33 100755
--- a/src/conf_mode/tftp_server.py
+++ b/src/conf_mode/tftp_server.py
@@ -40,9 +40,12 @@ default_config_data = {
'listen': []
}
-def get_config():
+def get_config(config=None):
tftpd = deepcopy(default_config_data)
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['service', 'tftp-server']
if not conf.exists(base):
return None
diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py
index 26ad1af84..13831dcd8 100755
--- a/src/conf_mode/vpn_l2tp.py
+++ b/src/conf_mode/vpn_l2tp.py
@@ -70,8 +70,11 @@ default_config_data = {
'thread_cnt': get_half_cpus()
}
-def get_config():
- conf = Config()
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
base_path = ['vpn', 'l2tp', 'remote-access']
if not conf.exists(base_path):
return None
diff --git a/src/conf_mode/vpn_pptp.py b/src/conf_mode/vpn_pptp.py
index 32cbadd74..9f3b40534 100755
--- a/src/conf_mode/vpn_pptp.py
+++ b/src/conf_mode/vpn_pptp.py
@@ -56,8 +56,11 @@ default_pptp = {
'thread_cnt': get_half_cpus()
}
-def get_config():
- conf = Config()
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
base_path = ['vpn', 'pptp', 'remote-access']
if not conf.exists(base_path):
return None
diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py
index ddb499bf4..7fc370f99 100755
--- a/src/conf_mode/vpn_sstp.py
+++ b/src/conf_mode/vpn_sstp.py
@@ -65,10 +65,13 @@ default_config_data = {
'thread_cnt' : get_half_cpus()
}
-def get_config():
+def get_config(config=None):
sstp = deepcopy(default_config_data)
base_path = ['vpn', 'sstp']
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
if not conf.exists(base_path):
return None
diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py
index 56ca813ff..2f4da0240 100755
--- a/src/conf_mode/vrf.py
+++ b/src/conf_mode/vrf.py
@@ -76,8 +76,11 @@ def vrf_routing(c, match):
return matched
-def get_config():
- conf = Config()
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
vrf_config = deepcopy(default_config_data)
cfg_base = ['vrf']
diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py
index 292eb0c78..f1ceb261b 100755
--- a/src/conf_mode/vrrp.py
+++ b/src/conf_mode/vrrp.py
@@ -32,11 +32,14 @@ from vyos.ifconfig.vrrp import VRRP
from vyos import airbag
airbag.enable()
-def get_config():
+def get_config(config=None):
vrrp_groups = []
sync_groups = []
- config = vyos.config.Config()
+ if config:
+ config = config
+ else:
+ config = vyos.config.Config()
# Get the VRRP groups
for group_name in config.list_nodes("high-availability vrrp group"):
diff --git a/src/conf_mode/vyos_cert.py b/src/conf_mode/vyos_cert.py
index fb4644d5a..dc7c64684 100755
--- a/src/conf_mode/vyos_cert.py
+++ b/src/conf_mode/vyos_cert.py
@@ -103,10 +103,13 @@ def generate_self_signed(cert_data):
if san_config:
san_config.close()
-def get_config():
+def get_config(config=None):
vyos_cert = vyos.defaults.vyos_cert_data
- conf = Config()
+ if config:
+ conf = config
+ else:
+ conf = Config()
if not conf.exists('service https certificates system-generated-certificate'):
return None
else:
diff --git a/src/services/vyos-configd b/src/services/vyos-configd
new file mode 100755
index 000000000..75f84d3df
--- /dev/null
+++ b/src/services/vyos-configd
@@ -0,0 +1,224 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 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/>.
+#
+#
+
+import os
+import sys
+import grp
+import re
+import json
+import logging
+import signal
+import importlib.util
+import zmq
+
+from vyos.defaults import directories
+from vyos.configsource import ConfigSourceString
+from vyos.config import Config
+from vyos import ConfigError
+
+CFG_GROUP = 'vyattacfg'
+
+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"
+
+# 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'
+
+active_string = ''
+session_string = ''
+
+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 run_script(script, config) -> int:
+ config.set_level([])
+ try:
+ c = script.get_config(config)
+ script.verify(c)
+ script.generate(c)
+ script.apply(c)
+ except ConfigError as e:
+ logger.critical(e)
+ return R_ERROR_COMMIT
+ except Exception:
+ return R_ERROR_DAEMON
+
+ return R_SUCCESS
+
+def initialization(socket):
+ # Reset config strings:
+ active_string = ''
+ session_string = ''
+ # zmq synchronous for ipc from single client:
+ active_string = socket.recv().decode()
+ resp = "active"
+ socket.send(resp.encode())
+ session_string = socket.recv().decode()
+ resp = "session"
+ socket.send(resp.encode())
+
+ configsource = ConfigSourceString(running_config_text=active_string,
+ session_config_text=session_string)
+
+ config = Config(config_source=configsource)
+
+ return config
+
+def process_node_data(config, data) -> int:
+ if not config:
+ logger.critical(f"Empty config")
+ return R_ERROR_DAEMON
+
+ script_name = None
+
+ res = re.match(r'^.+\/([^/].+).py(VYOS_TAGNODE_VALUE=.+)?', data)
+ if res.group(1):
+ script_name = res.group(1)
+ if res.group(2):
+ env = res.group(2).split('=')
+ os.environ[env[0]] = env[1]
+
+ if not script_name:
+ logger.critical(f"Missing script_name")
+ return R_ERROR_DAEMON
+
+ if script_name in exclude_set:
+ return R_PASS
+
+ result = run_script(conf_mode_scripts[script_name], config)
+
+ return result
+
+def remove_if_file(f: str):
+ try:
+ os.remove(f)
+ except FileNotFoundError:
+ pass
+ except OSError:
+ raise
+
+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['SUDO_USER'] = 'vyos'
+ os.environ['SUDO_GID'] = str(cfg_group.gr_gid)
+
+ def sig_handler(signum, frame):
+ 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 = process_node_data(config, message["data"])
+ response = res.to_bytes(1, byteorder=sys.byteorder)
+ logger.debug(f"Sending response {res}")
+ socket.send(response)
+ else:
+ logger.critical(f"Unexpected message: {message}")
diff --git a/src/shim/Makefile b/src/shim/Makefile
new file mode 100644
index 000000000..c8487e3c2
--- /dev/null
+++ b/src/shim/Makefile
@@ -0,0 +1,20 @@
+DEBUG = 0
+
+CC := gcc
+CFLAGS := -I./mkjson -L./mkjson/lib -DDEBUG=${DEBUG}
+LIBS := -lmkjson -lzmq
+
+.PHONY: vyshim
+vyshim: vyshim.c libmkjson
+ $(CC) $(CFLAGS) -o $@ $< $(LIBS)
+
+.PHONY: libmkjson
+libmkjson:
+ $(MAKE) -C mkjson
+
+all: vyshim
+
+.PHONY: clean
+clean:
+ $(MAKE) -C mkjson clean
+ rm -f vyshim
diff --git a/src/shim/mkjson/LICENSE b/src/shim/mkjson/LICENSE
new file mode 100644
index 000000000..8c4284c91
--- /dev/null
+++ b/src/shim/mkjson/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 Jacek Wieczorek
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/src/shim/mkjson/makefile b/src/shim/mkjson/makefile
new file mode 100644
index 000000000..ba75399d2
--- /dev/null
+++ b/src/shim/mkjson/makefile
@@ -0,0 +1,30 @@
+CFLAGS = -Wall -Os -I.
+CC = gcc
+AR = ar
+
+#USE_ASPRINTF make flag can be used in order to encourage asprintf use inside the library
+ifeq ($(USE_ASPRINTF),1)
+CFLAGS += -D_GNU_SOURCE
+endif
+
+#Builds object and a static library file
+all: clean force
+ $(CC) $(CFLAGS) -c mkjson.c -o obj/mkjson.o
+ $(AR) -cvq lib/libmkjson.a obj/mkjson.o
+ $(AR) -t lib/libmkjson.a
+
+#Normal cleanup
+clean:
+ -rm -rf obj
+ -rm -rf lib
+
+#Environment init
+force:
+ -mkdir obj
+ -mkdir lib
+
+#Build the example snippet
+example: all
+ gcc -o example examples/example.c -I. -Llib -lmkjson
+
+
diff --git a/src/shim/mkjson/mkjson.c b/src/shim/mkjson/mkjson.c
new file mode 100644
index 000000000..1172664fb
--- /dev/null
+++ b/src/shim/mkjson/mkjson.c
@@ -0,0 +1,307 @@
+/* mkjson.c - a part of mkjson library
+ *
+ * Copyright (C) 2018 Jacek Wieczorek
+ *
+ * This software may be modified and distributed under the terms
+ * of the MIT license. See the LICENSE file for details.
+ */
+
+#include <mkjson.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <string.h>
+
+// Works like asprintf, but it's always there
+// I don't want the name to collide with anything
+static int allsprintf( char **strp, const char *fmt, ... )
+{
+ int len;
+ va_list ap;
+ va_start( ap, fmt );
+
+ #ifdef _GNU_SOURCE
+ // Just hand everything to vasprintf, if it's available
+ len = vasprintf( strp, fmt, ap );
+ #else
+ // Or do it the manual way
+ char *buf;
+ len = vsnprintf( NULL, 0, fmt, ap );
+ if ( len >= 0 )
+ {
+ buf = malloc( ++len );
+ if ( buf != NULL )
+ {
+ // Hopefully, that's the right way to do it
+ va_end( ap );
+ va_start( ap, fmt );
+
+ // Write and return the data
+ len = vsnprintf( buf, len, fmt, ap );
+ if ( len >= 0 )
+ {
+ *strp = buf;
+ }
+ else
+ {
+ free( buf );
+ }
+ }
+ }
+ #endif
+
+ va_end( ap );
+ return len;
+}
+
+// Return JSON string built from va_arg arguments
+// If no longer needed, should be passed to free() by user
+char *mkjson( enum mkjson_container_type otype, int count, ... )
+{
+ int i, len, goodchunks = 0, failure = 0;
+ char *json, *prefix, **chunks, ign;
+
+ // Value - type and data
+ enum mkjson_value_type vtype;
+ const char *key;
+ long long int intval;
+ long double dblval;
+ const char *strval;
+
+ // Since v0.9 count cannot be a negative value and datatype is indicated by a separate argument
+ // Since I'm not sure whether it's right to put assertions in libraries, the next line is commented out
+ // assert( count >= 0 && "After v0.9 negative count is prohibited; please use otype argument instead" );
+ if ( count < 0 || ( otype != MKJSON_OBJ && otype != MKJSON_ARR ) ) return NULL;
+
+ // Allocate chunk pointer array - on standard platforms each one should be NULL
+ chunks = calloc( count, sizeof( char* ) );
+ if ( chunks == NULL ) return NULL;
+
+ // This should rather be at the point of no return
+ va_list ap;
+ va_start( ap, count );
+
+ // Create chunks
+ for ( i = 0; i < count && !failure; i++ )
+ {
+ // Get value type
+ vtype = va_arg( ap, enum mkjson_value_type );
+
+ // Get key
+ if ( otype == MKJSON_OBJ )
+ {
+ key = va_arg( ap, char* );
+ if ( key == NULL )
+ {
+ failure = 1;
+ break;
+ }
+ }
+ else key = "";
+
+ // Generate prefix
+ if ( allsprintf( &prefix, "%s%s%s",
+ otype == MKJSON_OBJ ? "\"" : "", // Quote before key
+ key, // Key
+ otype == MKJSON_OBJ ? "\": " : "" ) == -1 ) // Quote and colon after key
+ {
+ failure = 1;
+ break;
+ }
+
+ // Depending on value type
+ ign = 0;
+ switch ( vtype )
+ {
+ // Ignore string / JSON data
+ case MKJSON_IGN_STRING:
+ case MKJSON_IGN_JSON:
+ (void) va_arg( ap, const char* );
+ ign = 1;
+ break;
+
+ // Ignore string / JSON data and pass the pointer to free
+ case MKJSON_IGN_STRING_FREE:
+ case MKJSON_IGN_JSON_FREE:
+ free( va_arg( ap, char* ) );
+ ign = 1;
+ break;
+
+ // Ignore int / long long int
+ case MKJSON_IGN_INT:
+ case MKJSON_IGN_LLINT:
+ if ( vtype == MKJSON_IGN_INT )
+ (void) va_arg( ap, int );
+ else
+ (void) va_arg( ap, long long int );
+ ign = 1;
+ break;
+
+ // Ignore double / long double
+ case MKJSON_IGN_DOUBLE:
+ case MKJSON_IGN_LDOUBLE:
+ if ( vtype == MKJSON_IGN_DOUBLE )
+ (void) va_arg( ap, double );
+ else
+ (void) va_arg( ap, long double );
+ ign = 1;
+ break;
+
+ // Ignore boolean
+ case MKJSON_IGN_BOOL:
+ (void) va_arg( ap, int );
+ ign = 1;
+ break;
+
+ // Ignore null value
+ case MKJSON_IGN_NULL:
+ ign = 1;
+ break;
+
+ // A null-terminated string
+ case MKJSON_STRING:
+ case MKJSON_STRING_FREE:
+ strval = va_arg( ap, const char* );
+
+ // If the pointer points to NULL, the string will be replaced with JSON null value
+ if ( strval == NULL )
+ {
+ if ( allsprintf( chunks + i, "%snull", prefix ) == -1 )
+ chunks[i] = NULL;
+ }
+ else
+ {
+ if ( allsprintf( chunks + i, "%s\"%s\"", prefix, strval ) == -1 )
+ chunks[i] = NULL;
+ }
+
+ // Optional free
+ if ( vtype == MKJSON_STRING_FREE )
+ free( (char*) strval );
+ break;
+
+ // Embed JSON data
+ case MKJSON_JSON:
+ case MKJSON_JSON_FREE:
+ strval = va_arg( ap, const char* );
+
+ // If the pointer points to NULL, the JSON data is replaced with null value
+ if ( allsprintf( chunks + i, "%s%s", prefix, strval == NULL ? "null" : strval ) == -1 )
+ chunks[i] = NULL;
+
+ // Optional free
+ if ( vtype == MKJSON_JSON_FREE )
+ free( (char*) strval );
+ break;
+
+ // int / long long int
+ case MKJSON_INT:
+ case MKJSON_LLINT:
+ if ( vtype == MKJSON_INT )
+ intval = va_arg( ap, int );
+ else
+ intval = va_arg( ap, long long int );
+
+ if ( allsprintf( chunks + i, "%s%Ld", prefix, intval ) == -1 ) chunks[i] = NULL;
+ break;
+
+ // double / long double
+ case MKJSON_DOUBLE:
+ case MKJSON_LDOUBLE:
+ if ( vtype == MKJSON_DOUBLE )
+ dblval = va_arg( ap, double );
+ else
+ dblval = va_arg( ap, long double );
+
+ if ( allsprintf( chunks + i, "%s%Lf", prefix, dblval ) == -1 ) chunks[i] = NULL;
+ break;
+
+ // double / long double
+ case MKJSON_SCI_DOUBLE:
+ case MKJSON_SCI_LDOUBLE:
+ if ( vtype == MKJSON_SCI_DOUBLE )
+ dblval = va_arg( ap, double );
+ else
+ dblval = va_arg( ap, long double );
+
+ if ( allsprintf( chunks + i, "%s%Le", prefix, dblval ) == -1 ) chunks[i] = NULL;
+ break;
+
+ // Boolean
+ case MKJSON_BOOL:
+ intval = va_arg( ap, int );
+ if ( allsprintf( chunks + i, "%s%s", prefix, intval ? "true" : "false" ) == -1 ) chunks[i] = NULL;
+ break;
+
+ // JSON null
+ case MKJSON_NULL:
+ if ( allsprintf( chunks + i, "%snull", prefix ) == -1 ) chunks[i] = NULL;
+ break;
+
+ // Bad type specifier
+ default:
+ chunks[i] = NULL;
+ break;
+ }
+
+ // Free prefix memory
+ free( prefix );
+
+ // NULL chunk without ignore flag indicates failure
+ if ( !ign && chunks[i] == NULL ) failure = 1;
+
+ // NULL chunk now indicates ignore flag
+ if ( ign ) chunks[i] = NULL;
+ else goodchunks++;
+ }
+
+ // We won't use ap anymore
+ va_end( ap );
+
+ // If everything is fine, merge chunks and create full JSON table
+ if ( !failure )
+ {
+ // Get total length (this is without NUL byte)
+ len = 0;
+ for ( i = 0; i < count; i++ )
+ if ( chunks[i] != NULL )
+ len += strlen( chunks[i] );
+
+ // Total length = Chunks length + 2 brackets + separators
+ if ( goodchunks == 0 ) goodchunks = 1;
+ len = len + 2 + ( goodchunks - 1 ) * 2;
+
+ // Allocate memory for the whole thing
+ json = calloc( len + 1, sizeof( char ) );
+ if ( json != NULL )
+ {
+ // Merge chunks (and do not overwrite the first bracket)
+ for ( i = 0; i < count; i++ )
+ {
+ // Add separators:
+ // - not on the begining
+ // - always after valid chunk
+ // - between two valid chunks
+ // - between valid and ignored chunk if the latter isn't the last one
+ if ( i != 0 && chunks[i - 1] != NULL && ( chunks[i] != NULL || ( chunks[i] == NULL && i != count - 1 ) ) )
+ strcat( json + 1, ", ");
+
+ if ( chunks[i] != NULL )
+ strcat( json + 1, chunks[i] );
+ }
+
+ // Add proper brackets
+ json[0] = otype == MKJSON_OBJ ? '{' : '[';
+ json[len - 1] = otype == MKJSON_OBJ ? '}' : ']';
+ }
+ }
+ else json = NULL;
+
+ // Free chunks
+ for ( i = 0; i < count; i++ )
+ free( chunks[i] );
+ free( chunks );
+
+ return json;
+}
+
diff --git a/src/shim/mkjson/mkjson.h b/src/shim/mkjson/mkjson.h
new file mode 100644
index 000000000..38cc07b26
--- /dev/null
+++ b/src/shim/mkjson/mkjson.h
@@ -0,0 +1,50 @@
+/* mkjson.h - a part of mkjson library
+ *
+ * Copyright (C) 2018 Jacek Wieczorek
+ *
+ * This software may be modified and distributed under the terms
+ * of the MIT license. See the LICENSE file for details.
+ */
+
+#ifndef MKJSON_H
+#define MKJSON_H
+
+// JSON container types
+enum mkjson_container_type
+{
+ MKJSON_ARR = 0, // An array
+ MKJSON_OBJ = 1 // An object (hash or whatever you call it)
+};
+
+// JSON data types
+enum mkjson_value_type
+{
+ MKJSON_STRING = (int)('s'), // const char* - String data
+ MKJSON_STRING_FREE = (int)('f'), // char* - String data, but pointer is freed
+ MKJSON_JSON = (int)('r'), // const char* - JSON data (like string, but no quotes)
+ MKJSON_JSON_FREE = (int)('j'), // char* - JSON data, but pointer is freed
+ MKJSON_INT = (int)('i'), // int - An integer
+ MKJSON_LLINT = (int)('I'), // long long int - A long integer
+ MKJSON_DOUBLE = (int)('d'), // double - A double
+ MKJSON_LDOUBLE = (int)('D'), // long double - A long double
+ MKJSON_SCI_DOUBLE = (int)('e'), // double - A double with scientific notation
+ MKJSON_SCI_LDOUBLE = (int)('E'), // long double - A long double with scientific notation
+ MKJSON_BOOL = (int)('b'), // int - A boolean value
+ MKJSON_NULL = (int)('n'), // -- - JSON null value
+
+ // These cause one argument of certain type to be ignored
+ MKJSON_IGN_STRING = (-MKJSON_STRING),
+ MKJSON_IGN_STRING_FREE = (-MKJSON_STRING_FREE),
+ MKJSON_IGN_JSON = (-MKJSON_JSON),
+ MKJSON_IGN_JSON_FREE = (-MKJSON_JSON_FREE),
+ MKJSON_IGN_INT = (-MKJSON_INT),
+ MKJSON_IGN_LLINT = (-MKJSON_LLINT),
+ MKJSON_IGN_DOUBLE = (-MKJSON_DOUBLE),
+ MKJSON_IGN_LDOUBLE = (-MKJSON_LDOUBLE),
+ MKJSON_IGN_BOOL = (-MKJSON_BOOL),
+ MKJSON_IGN_NULL = (-MKJSON_NULL)
+};
+
+extern char *mkjson( enum mkjson_container_type otype, int count, ... );
+
+#endif
diff --git a/src/shim/vyshim.c b/src/shim/vyshim.c
new file mode 100644
index 000000000..8b6feab99
--- /dev/null
+++ b/src/shim/vyshim.c
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2020 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/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <string.h>
+#include <sys/time.h>
+#include <time.h>
+#include <stdint.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <zmq.h>
+#include "mkjson.h"
+
+/*
+ *
+ *
+ */
+
+#if DEBUG
+#define DEBUG_ON 1
+#else
+#define DEBUG_ON 0
+#endif
+#define debug_print(fmt, ...) \
+ do { if (DEBUG_ON) fprintf(stderr, fmt, ##__VA_ARGS__); } while (0)
+#define debug_call(f) \
+ do { if (DEBUG_ON) f; } while (0)
+
+#define SOCKET_PATH "ipc:///run/vyos-configd.sock"
+
+#define GET_ACTIVE "cli-shell-api --show-active-only --show-show-defaults --show-ignore-edit showConfig"
+#define GET_SESSION "cli-shell-api --show-working-only --show-show-defaults --show-ignore-edit showConfig"
+
+#define COMMIT_MARKER "/var/tmp/initial_in_commit"
+
+enum {
+ SUCCESS = 1 << 0,
+ ERROR_COMMIT = 1 << 1,
+ ERROR_DAEMON = 1 << 2,
+ PASS = 1 << 3
+};
+
+volatile int init_alarm = 0;
+volatile int timeout = 0;
+
+int initialization(void *);
+int pass_through(char **, int);
+void timer_handler(int);
+
+double get_posix_clock_time(void);
+
+int main(int argc, char* argv[])
+{
+ // string for node data: conf_mode script and tagnode, if applicable
+ char string_node_data[256];
+ string_node_data[0] = '\0';
+
+ void *context = zmq_ctx_new();
+ void *requester = zmq_socket(context, ZMQ_REQ);
+
+ int init_timeout = 0;
+
+ debug_print("Connecting to vyos-configd ...\n");
+ zmq_connect(requester, SOCKET_PATH);
+
+ if (access(COMMIT_MARKER, F_OK) != -1) {
+ init_timeout = initialization(requester);
+ if (!init_timeout) remove(COMMIT_MARKER);
+ }
+
+ int end = argc > 3 ? 2 : argc - 1;
+
+ // if initial communication failed, pass through execution of script
+ if (init_timeout) {
+ int ret = pass_through(argv, end);
+ return ret;
+ }
+
+ for (int i = end; i > 0 ; i--) {
+ strncat(&string_node_data[0], argv[i], 127);
+ }
+
+ char error_code[1];
+ debug_print("Sending node data ...\n");
+ char *string_node_data_msg = mkjson(MKJSON_OBJ, 2,
+ MKJSON_STRING, "type", "node",
+ MKJSON_STRING, "data", &string_node_data[0]);
+
+ zmq_send(requester, string_node_data_msg, strlen(string_node_data_msg), 0);
+ zmq_recv(requester, error_code, 1, 0);
+ debug_print("Received node data receipt\n");
+
+ int err = (int)error_code[0];
+
+ free(string_node_data_msg);
+
+ zmq_close(requester);
+ zmq_ctx_destroy(context);
+
+ if (err & PASS) {
+ debug_print("Received PASS\n");
+ int ret = pass_through(argv, end);
+ return ret;
+ }
+
+ if (err & ERROR_DAEMON) {
+ debug_print("Received ERROR_DAEMON\n");
+ int ret = pass_through(argv, end);
+ return ret;
+ }
+
+ if (err & ERROR_COMMIT) {
+ debug_print("Received ERROR_COMMIT\n");
+ return -1;
+ }
+
+ return 0;
+}
+
+int initialization(void* Requester)
+{
+ char *active_str = NULL;
+ size_t active_len = 0;
+
+ char *session_str = NULL;
+ size_t session_len = 0;
+
+ char *empty_string = "\n";
+
+ char buffer[16];
+
+ struct sigaction sa;
+ struct itimerval timer, none_timer;
+
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_handler = &timer_handler;
+ sigaction(SIGALRM, &sa, NULL);
+
+ timer.it_value.tv_sec = 0;
+ timer.it_value.tv_usec = 10000;
+ timer.it_interval.tv_sec = timer.it_interval.tv_usec = 0;
+ none_timer.it_value.tv_sec = none_timer.it_value.tv_usec = 0;
+ none_timer.it_interval.tv_sec = none_timer.it_interval.tv_usec = 0;
+
+ double prev_time_value, time_value;
+ double time_diff;
+
+ debug_print("Sending init announcement\n");
+ char *init_announce = mkjson(MKJSON_OBJ, 1,
+ MKJSON_STRING, "type", "init");
+
+ // check for timeout on initial contact
+ while (!init_alarm) {
+ debug_call(prev_time_value = get_posix_clock_time());
+
+ setitimer(ITIMER_REAL, &timer, NULL);
+
+ zmq_send(Requester, init_announce, strlen(init_announce), 0);
+ zmq_recv(Requester, buffer, 16, 0);
+
+ setitimer(ITIMER_REAL, &none_timer, &timer);
+
+ debug_call(time_value = get_posix_clock_time());
+
+ debug_print("Received init receipt\n");
+ debug_call(time_diff = time_value - prev_time_value);
+ debug_print("time elapse %f\n", time_diff);
+
+ break;
+ }
+
+ free(init_announce);
+
+ if (timeout) return -1;
+
+ FILE *fp_a = popen(GET_ACTIVE, "r");
+ getdelim(&active_str, &active_len, '\0', fp_a);
+ int ret = pclose(fp_a);
+
+ if (!ret) {
+ debug_print("Sending active config\n");
+ zmq_send(Requester, active_str, active_len - 1, 0);
+ zmq_recv(Requester, buffer, 16, 0);
+ debug_print("Received active receipt\n");
+ } else {
+ debug_print("Sending empty active config\n");
+ zmq_send(Requester, empty_string, 0, 0);
+ zmq_recv(Requester, buffer, 16, 0);
+ debug_print("Received active receipt\n");
+ }
+
+ free(active_str);
+
+ FILE *fp_s = popen(GET_SESSION, "r");
+ getdelim(&session_str, &session_len, '\0', fp_s);
+ pclose(fp_s);
+
+ debug_print("Sending session config\n");
+ zmq_send(Requester, session_str, session_len - 1, 0);
+ zmq_recv(Requester, buffer, 16, 0);
+ debug_print("Received session receipt\n");
+
+ free(session_str);
+
+ return 0;
+}
+
+int pass_through(char **argv, int end)
+{
+ char *newargv[] = { NULL, NULL };
+ pid_t child_pid;
+
+ newargv[0] = argv[end];
+ if (end > 1) {
+ putenv(argv[end - 1]);
+ }
+
+ debug_print("pass-through invoked\n");
+
+ if ((child_pid=fork()) < 0) {
+ debug_print("fork() failed\n");
+ return -1;
+ } else if (child_pid == 0) {
+ if (-1 == execv(argv[end], newargv)) {
+ debug_print("pass_through execve failed %s: %s\n",
+ argv[end], strerror(errno));
+ return -1;
+ }
+ } else if (child_pid > 0) {
+ int status;
+ pid_t wait_pid = waitpid(child_pid, &status, 0);
+ if (wait_pid < 0) {
+ debug_print("waitpid() failed\n");
+ return -1;
+ } else if (wait_pid == child_pid) {
+ if (WIFEXITED(status)) {
+ debug_print("child exited with code %d\n",
+ WEXITSTATUS(status));
+ return WEXITSTATUS(status);
+ }
+ }
+ }
+
+ return 0;
+}
+
+void timer_handler(int signum)
+{
+ debug_print("timer_handler invoked\n");
+ timeout = 1;
+ init_alarm = 1;
+
+ return;
+}
+
+#ifdef _POSIX_MONOTONIC_CLOCK
+double get_posix_clock_time(void)
+{
+ struct timespec ts;
+
+ if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0) {
+ return (double) (ts.tv_sec + ts.tv_nsec / 1000000000.0);
+ } else {
+ return 0;
+ }
+}
+#else
+double get_posix_clock_time(void)
+{return (double)0;}
+#endif
diff --git a/src/systemd/vyos-configd.service b/src/systemd/vyos-configd.service
new file mode 100644
index 000000000..274ccc787
--- /dev/null
+++ b/src/systemd/vyos-configd.service
@@ -0,0 +1,27 @@
+[Unit]
+Description=VyOS configuration daemon
+
+# Without this option, lots of default dependencies are added,
+# among them network.target, which creates a dependency cycle
+DefaultDependencies=no
+
+# Seemingly sensible way to say "as early as the system is ready"
+# All vyos-configd needs is read/write mounted root
+After=systemd-remount-fs.service
+Before=vyos-router.service
+
+[Service]
+ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-configd
+Type=idle
+
+SyslogIdentifier=vyos-configd
+SyslogFacility=daemon
+
+Restart=on-failure
+
+# Does't work in Jessie but leave it here
+User=root
+Group=vyattacfg
+
+[Install]
+WantedBy=vyos.target