summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--Makefile2
-rw-r--r--data/templates/conserver/dropbear@.service.j22
-rw-r--r--data/templates/frr/bgpd.frr.j23
-rw-r--r--debian/control6
-rw-r--r--debian/vyos-1x.postinst14
-rw-r--r--interface-definitions/include/bgp/afi-route-map-export-import.xml.i34
-rw-r--r--interface-definitions/include/bgp/afi-route-map-export.xml.i18
-rw-r--r--interface-definitions/include/bgp/afi-route-map-import.xml.i18
-rw-r--r--interface-definitions/include/bgp/afi-route-map-vpn.xml.i3
-rw-r--r--interface-definitions/include/bgp/afi-route-map-vrf.xml.i17
-rw-r--r--interface-definitions/include/bgp/afi-route-map.xml.i3
-rw-r--r--interface-definitions/include/bgp/protocol-common-config.xml.i1
-rw-r--r--python/setup.py38
-rw-r--r--python/vyos/config_mgmt.py10
-rw-r--r--python/vyos/configsource.py10
-rw-r--r--python/vyos/configtree.py76
-rw-r--r--python/vyos/defaults.py5
-rw-r--r--python/vyos/proto/__init__.py0
-rw-r--r--python/vyos/utils/auth.py64
-rw-r--r--smoketest/scripts/cli/base_vyostest_shim.py6
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_bgp.py36
-rwxr-xr-xsmoketest/scripts/cli/test_system_login.py20
-rwxr-xr-xsrc/conf_mode/protocols_bgp.py11
-rwxr-xr-xsrc/conf_mode/service_console-server.py8
-rwxr-xr-xsrc/conf_mode/system_login.py21
-rwxr-xr-xsrc/helpers/show_commit_data.py56
-rwxr-xr-xsrc/helpers/test_commit.py49
-rwxr-xr-xsrc/helpers/vyos-certbot-renew-pki.sh2
-rwxr-xr-xsrc/op_mode/image_installer.py19
-rwxr-xr-xsrc/services/vyos-commitd453
-rw-r--r--src/systemd/vyos-commitd.service27
-rw-r--r--src/tests/test_config_diff.py11
33 files changed, 984 insertions, 62 deletions
diff --git a/.gitignore b/.gitignore
index d1bfc91d7..27ed8000f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -151,6 +151,9 @@ data/reftree.cache
# autogenerated vyos-configd JSON definition
data/configd-include.json
+# autogenerated vyos-commitd protobuf files
+python/vyos/proto/*pb2.py
+
# We do not use pip
Pipfile
Pipfile.lock
diff --git a/Makefile b/Makefile
index 3ec5ed73f..72b3d2784 100644
--- a/Makefile
+++ b/Makefile
@@ -27,7 +27,7 @@ libvyosconfig:
rm -rf /tmp/libvyosconfig && \
git clone https://github.com/vyos/libvyosconfig.git /tmp/libvyosconfig || exit 1
cd /tmp/libvyosconfig && \
- git checkout 677d1e2bf8109b9fd4da60e20376f992b747e384 || exit 1
+ git checkout 5f15d8095efd11756a867e552a3f8fe6c77e57cc || exit 1
eval $$(opam env --root=/opt/opam --set-root) && ./build.sh
fi
diff --git a/data/templates/conserver/dropbear@.service.j2 b/data/templates/conserver/dropbear@.service.j2
index e355dab43..c6c31f98f 100644
--- a/data/templates/conserver/dropbear@.service.j2
+++ b/data/templates/conserver/dropbear@.service.j2
@@ -1,4 +1,4 @@
[Service]
ExecStart=
-ExecStart=/usr/sbin/dropbear -w -j -k -r /etc/dropbear/dropbear_rsa_host_key -b /etc/issue.net -c "/usr/bin/console {{ device }}" -P /run/conserver/dropbear.%I.pid -p %I
+ExecStart=/usr/sbin/dropbear -w -j -k -r /etc/dropbear/dropbear_rsa_host_key -r /etc/dropbear/dropbear_ecdsa_host_key -b /etc/issue.net -c "/usr/bin/console {{ device }}" -P /run/conserver/dropbear.%I.pid -p %I
PIDFile=/run/conserver/dropbear.%I.pid
diff --git a/data/templates/frr/bgpd.frr.j2 b/data/templates/frr/bgpd.frr.j2
index 3b462b4a9..b89f15be1 100644
--- a/data/templates/frr/bgpd.frr.j2
+++ b/data/templates/frr/bgpd.frr.j2
@@ -357,6 +357,9 @@ router bgp {{ system_as }} {{ 'vrf ' ~ vrf if vrf is vyos_defined }}
import vrf {{ vrf }}
{% endfor %}
{% endif %}
+{% if afi_config.route_map.vrf.import is vyos_defined %}
+ import vrf route-map {{ afi_config.route_map.vrf.import }}
+{% endif %}
{% if afi_config.label.vpn.export is vyos_defined %}
label vpn export {{ afi_config.label.vpn.export }}
{% endif %}
diff --git a/debian/control b/debian/control
index efc008af2..20b1a228c 100644
--- a/debian/control
+++ b/debian/control
@@ -15,6 +15,8 @@ Build-Depends:
# For generating command definitions
python3-lxml,
python3-xmltodict,
+# For generating serialization functions
+ protobuf-compiler,
# For running tests
python3-coverage,
python3-hurry.filesize,
@@ -70,6 +72,7 @@ Depends:
python3-netifaces,
python3-paramiko,
python3-passlib,
+ python3-protobuf,
python3-pyroute2,
python3-psutil,
python3-pyhumps,
@@ -77,6 +80,7 @@ Depends:
python3-pyudev,
python3-six,
python3-tabulate,
+ python3-tomli,
python3-voluptuous,
python3-xmltodict,
python3-zmq,
@@ -123,6 +127,8 @@ Depends:
# Live filesystem tools
squashfs-tools,
fuse-overlayfs,
+# Tools for checking password strength
+ python3-cracklib,
## End installer
auditd,
iputils-arping,
diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst
index fde58651a..ba97f37f6 100644
--- a/debian/vyos-1x.postinst
+++ b/debian/vyos-1x.postinst
@@ -195,6 +195,10 @@ if [ ! -x $PRECONFIG_SCRIPT ]; then
EOF
fi
+# cracklib-runtime default database location
+CRACKLIB_DIR=/var/cache/cracklib
+CRACKLIB_DB=cracklib_dict
+
# create /opt/vyatta/etc/config/scripts/vyos-postconfig-bootup.script
POSTCONFIG_SCRIPT=/opt/vyatta/etc/config/scripts/vyos-postconfig-bootup.script
if [ ! -x $POSTCONFIG_SCRIPT ]; then
@@ -206,7 +210,15 @@ if [ ! -x $POSTCONFIG_SCRIPT ]; then
# This script is executed at boot time after VyOS configuration is fully applied.
# Any modifications required to work around unfixed bugs
# or use services not available through the VyOS CLI system can be placed here.
-
+#
+# T6353 - Just in case, check if cracklib was installed properly
+# If the database file is missing, re-install the runtime package
+#
+if [ ! -f "${CRACKLIB_DIR}/${CRACKLIB_DB}.pwd" ]; then
+ mkdir -p $CRACKLIB_DIR
+ /usr/sbin/create-cracklib-dict -o $CRACKLIB_DIR/$CRACKLIB_DB \
+ /usr/share/dict/cracklib-small
+fi
EOF
fi
diff --git a/interface-definitions/include/bgp/afi-route-map-export-import.xml.i b/interface-definitions/include/bgp/afi-route-map-export-import.xml.i
deleted file mode 100644
index 388991241..000000000
--- a/interface-definitions/include/bgp/afi-route-map-export-import.xml.i
+++ /dev/null
@@ -1,34 +0,0 @@
-<!-- include start from bgp/afi-route-map.xml.i -->
-<leafNode name="export">
- <properties>
- <help>Route-map to filter outgoing route updates</help>
- <completionHelp>
- <path>policy route-map</path>
- </completionHelp>
- <valueHelp>
- <format>txt</format>
- <description>Route map name</description>
- </valueHelp>
- <constraint>
- #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
- </constraint>
- <constraintErrorMessage>Name of route-map can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage>
- </properties>
-</leafNode>
-<leafNode name="import">
- <properties>
- <help>Route-map to filter incoming route updates</help>
- <completionHelp>
- <path>policy route-map</path>
- </completionHelp>
- <valueHelp>
- <format>txt</format>
- <description>Route map name</description>
- </valueHelp>
- <constraint>
- #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
- </constraint>
- <constraintErrorMessage>Name of route-map can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage>
- </properties>
-</leafNode>
-<!-- include end -->
diff --git a/interface-definitions/include/bgp/afi-route-map-export.xml.i b/interface-definitions/include/bgp/afi-route-map-export.xml.i
new file mode 100644
index 000000000..94d77caf2
--- /dev/null
+++ b/interface-definitions/include/bgp/afi-route-map-export.xml.i
@@ -0,0 +1,18 @@
+<!-- include start from bgp/afi-route-map-export.xml.i -->
+<leafNode name="export">
+ <properties>
+ <help>Route-map to filter outgoing route updates</help>
+ <completionHelp>
+ <path>policy route-map</path>
+ </completionHelp>
+ <valueHelp>
+ <format>txt</format>
+ <description>Route map name</description>
+ </valueHelp>
+ <constraint>
+ #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
+ </constraint>
+ <constraintErrorMessage>Route map names can only contain alphanumeric characters, hyphens, and underscores</constraintErrorMessage>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/bgp/afi-route-map-import.xml.i b/interface-definitions/include/bgp/afi-route-map-import.xml.i
new file mode 100644
index 000000000..a1b154fcd
--- /dev/null
+++ b/interface-definitions/include/bgp/afi-route-map-import.xml.i
@@ -0,0 +1,18 @@
+<!-- include start from bgp/afi-route-map-import.xml.i -->
+<leafNode name="import">
+ <properties>
+ <help>Route-map to filter incoming route updates</help>
+ <completionHelp>
+ <path>policy route-map</path>
+ </completionHelp>
+ <valueHelp>
+ <format>txt</format>
+ <description>Route map name</description>
+ </valueHelp>
+ <constraint>
+ #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
+ </constraint>
+ <constraintErrorMessage>Name of route-map can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/bgp/afi-route-map-vpn.xml.i b/interface-definitions/include/bgp/afi-route-map-vpn.xml.i
index e6be113c5..ac7b55af6 100644
--- a/interface-definitions/include/bgp/afi-route-map-vpn.xml.i
+++ b/interface-definitions/include/bgp/afi-route-map-vpn.xml.i
@@ -9,7 +9,8 @@
<help>Between current address-family and VPN</help>
</properties>
<children>
- #include <include/bgp/afi-route-map-export-import.xml.i>
+ #include <include/bgp/afi-route-map-export.xml.i>
+ #include <include/bgp/afi-route-map-import.xml.i>
</children>
</node>
</children>
diff --git a/interface-definitions/include/bgp/afi-route-map-vrf.xml.i b/interface-definitions/include/bgp/afi-route-map-vrf.xml.i
new file mode 100644
index 000000000..5c1783bda
--- /dev/null
+++ b/interface-definitions/include/bgp/afi-route-map-vrf.xml.i
@@ -0,0 +1,17 @@
+<!-- include start from bgp/afi-route-map-vrf.xml.i -->
+<node name="route-map">
+ <properties>
+ <help>Route-map to filter route updates to/from this peer</help>
+ </properties>
+ <children>
+ <node name="vrf">
+ <properties>
+ <help>Between current address-family and VRF</help>
+ </properties>
+ <children>
+ #include <include/bgp/afi-route-map-import.xml.i>
+ </children>
+ </node>
+ </children>
+</node>
+<!-- include end -->
diff --git a/interface-definitions/include/bgp/afi-route-map.xml.i b/interface-definitions/include/bgp/afi-route-map.xml.i
index 0b6178176..f8e1d7033 100644
--- a/interface-definitions/include/bgp/afi-route-map.xml.i
+++ b/interface-definitions/include/bgp/afi-route-map.xml.i
@@ -4,7 +4,8 @@
<help>Route-map to filter route updates to/from this peer</help>
</properties>
<children>
- #include <include/bgp/afi-route-map-export-import.xml.i>
+ #include <include/bgp/afi-route-map-export.xml.i>
+ #include <include/bgp/afi-route-map-import.xml.i>
</children>
</node>
<!-- include end -->
diff --git a/interface-definitions/include/bgp/protocol-common-config.xml.i b/interface-definitions/include/bgp/protocol-common-config.xml.i
index 21514e762..31c8cafea 100644
--- a/interface-definitions/include/bgp/protocol-common-config.xml.i
+++ b/interface-definitions/include/bgp/protocol-common-config.xml.i
@@ -119,6 +119,7 @@
</tagNode>
#include <include/bgp/afi-rd.xml.i>
#include <include/bgp/afi-route-map-vpn.xml.i>
+ #include <include/bgp/afi-route-map-vrf.xml.i>
#include <include/bgp/afi-route-target-vpn.xml.i>
#include <include/bgp/afi-nexthop-vpn-export.xml.i>
<node name="redistribute">
diff --git a/python/setup.py b/python/setup.py
index 2d614e724..96dc211f7 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -1,5 +1,11 @@
import os
+import sys
+import subprocess
from setuptools import setup
+from setuptools.command.build_py import build_py
+
+sys.path.append('./vyos')
+from defaults import directories
def packages(directory):
return [
@@ -8,6 +14,35 @@ def packages(directory):
if os.path.isfile(os.path.join(_[0], '__init__.py'))
]
+
+class GenerateProto(build_py):
+ ver = os.environ.get('OCAML_VERSION')
+ if ver:
+ proto_path = f'/opt/opam/{ver}/share/vyconf'
+ else:
+ proto_path = directories['proto_path']
+
+ def run(self):
+ # find all .proto files in vyconf proto_path
+ proto_files = []
+ for _, _, files in os.walk(self.proto_path):
+ for file in files:
+ if file.endswith('.proto'):
+ proto_files.append(file)
+
+ # compile each .proto file to Python
+ for proto_file in proto_files:
+ subprocess.check_call(
+ [
+ 'protoc',
+ '--python_out=vyos/proto',
+ f'--proto_path={self.proto_path}/',
+ proto_file,
+ ]
+ )
+
+ build_py.run(self)
+
setup(
name = "vyos",
version = "1.3.0",
@@ -29,4 +64,7 @@ setup(
"config-mgmt = vyos.config_mgmt:run",
],
},
+ cmdclass={
+ 'build_py': GenerateProto,
+ },
)
diff --git a/python/vyos/config_mgmt.py b/python/vyos/config_mgmt.py
index 1c2b70fdf..dd8910afb 100644
--- a/python/vyos/config_mgmt.py
+++ b/python/vyos/config_mgmt.py
@@ -287,7 +287,7 @@ Proceed ?"""
# commits under commit-confirm are not added to revision list unless
# confirmed, hence a soft revert is to revision 0
- revert_ct = self._get_config_tree_revision(0)
+ revert_ct = self.get_config_tree_revision(0)
message = '[commit-confirm] Reverting to previous config now'
os.system('wall -n ' + message)
@@ -351,7 +351,7 @@ Proceed ?"""
)
return msg, 1
- rollback_ct = self._get_config_tree_revision(rev)
+ rollback_ct = self.get_config_tree_revision(rev)
try:
load(rollback_ct, switch='explicit')
print('Rollback diff has been applied.')
@@ -382,7 +382,7 @@ Proceed ?"""
if rev1 is not None:
if not self._check_revision_number(rev1):
return f'Invalid revision number {rev1}', 1
- ct1 = self._get_config_tree_revision(rev1)
+ ct1 = self.get_config_tree_revision(rev1)
ct2 = self.working_config
msg = f'No changes between working and revision {rev1} configurations.\n'
if rev2 is not None:
@@ -390,7 +390,7 @@ Proceed ?"""
return f'Invalid revision number {rev2}', 1
# compare older to newer
ct2 = ct1
- ct1 = self._get_config_tree_revision(rev2)
+ ct1 = self.get_config_tree_revision(rev2)
msg = f'No changes between revisions {rev2} and {rev1} configurations.\n'
out = ''
@@ -575,7 +575,7 @@ Proceed ?"""
r = f.read().decode()
return r
- def _get_config_tree_revision(self, rev: int):
+ def get_config_tree_revision(self, rev: int):
c = self._get_file_revision(rev)
return ConfigTree(c)
diff --git a/python/vyos/configsource.py b/python/vyos/configsource.py
index 59e5ac8a1..65cef5333 100644
--- a/python/vyos/configsource.py
+++ b/python/vyos/configsource.py
@@ -319,3 +319,13 @@ class ConfigSourceString(ConfigSource):
self._session_config = ConfigTree(session_config_text) if session_config_text else None
except ValueError:
raise ConfigSourceError(f"Init error in {type(self)}")
+
+class ConfigSourceCache(ConfigSource):
+ def __init__(self, running_config_cache=None, session_config_cache=None):
+ super().__init__()
+
+ try:
+ self._running_config = ConfigTree(internal=running_config_cache) if running_config_cache else None
+ self._session_config = ConfigTree(internal=session_config_cache) if session_config_cache else None
+ except ValueError:
+ raise ConfigSourceError(f"Init error in {type(self)}")
diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py
index 4ad0620a5..83954327c 100644
--- a/python/vyos/configtree.py
+++ b/python/vyos/configtree.py
@@ -66,9 +66,14 @@ class ConfigTreeError(Exception):
class ConfigTree(object):
- def __init__(self, config_string=None, address=None, libpath=LIBPATH):
- if config_string is None and address is None:
- raise TypeError("ConfigTree() requires one of 'config_string' or 'address'")
+ def __init__(
+ self, config_string=None, address=None, internal=None, libpath=LIBPATH
+ ):
+ if config_string is None and address is None and internal is None:
+ raise TypeError(
+ "ConfigTree() requires one of 'config_string', 'address', or 'internal'"
+ )
+
self.__config = None
self.__lib = cdll.LoadLibrary(libpath)
@@ -89,6 +94,13 @@ class ConfigTree(object):
self.__to_commands.argtypes = [c_void_p, c_char_p]
self.__to_commands.restype = c_char_p
+ self.__read_internal = self.__lib.read_internal
+ self.__read_internal.argtypes = [c_char_p]
+ self.__read_internal.restype = c_void_p
+
+ self.__write_internal = self.__lib.write_internal
+ self.__write_internal.argtypes = [c_void_p, c_char_p]
+
self.__to_json = self.__lib.to_json
self.__to_json.argtypes = [c_void_p]
self.__to_json.restype = c_char_p
@@ -168,7 +180,21 @@ class ConfigTree(object):
self.__destroy = self.__lib.destroy
self.__destroy.argtypes = [c_void_p]
- if address is None:
+ self.__equal = self.__lib.equal
+ self.__equal.argtypes = [c_void_p, c_void_p]
+ self.__equal.restype = c_bool
+
+ if address is not None:
+ self.__config = address
+ self.__version = ''
+ elif internal is not None:
+ config = self.__read_internal(internal.encode())
+ if config is None:
+ msg = self.__get_error().decode()
+ raise ValueError('Failed to read internal rep: {0}'.format(msg))
+ else:
+ self.__config = config
+ elif config_string is not None:
config_section, version_section = extract_version(config_string)
config_section = escape_backslash(config_section)
config = self.__from_string(config_section.encode())
@@ -179,8 +205,9 @@ class ConfigTree(object):
self.__config = config
self.__version = version_section
else:
- self.__config = address
- self.__version = ''
+ raise TypeError(
+ "ConfigTree() requires one of 'config_string', 'address', or 'internal'"
+ )
self.__migration = os.environ.get('VYOS_MIGRATION')
if self.__migration:
@@ -190,6 +217,11 @@ class ConfigTree(object):
if self.__config is not None:
self.__destroy(self.__config)
+ def __eq__(self, other):
+ if isinstance(other, ConfigTree):
+ return self.__equal(self._get_config(), other._get_config())
+ return False
+
def __str__(self):
return self.to_string()
@@ -199,6 +231,9 @@ class ConfigTree(object):
def get_version_string(self):
return self.__version
+ def write_cache(self, file_name):
+ self.__write_internal(self._get_config(), file_name)
+
def to_string(self, ordered_values=False, no_version=False):
config_string = self.__to_string(self.__config, ordered_values).decode()
config_string = unescape_backslash(config_string)
@@ -488,6 +523,35 @@ def mask_inclusive(left, right, libpath=LIBPATH):
return tree
+def show_commit_data(active_tree, proposed_tree, libpath=LIBPATH):
+ if not (
+ isinstance(active_tree, ConfigTree) and isinstance(proposed_tree, ConfigTree)
+ ):
+ raise TypeError('Arguments must be instances of ConfigTree')
+
+ __lib = cdll.LoadLibrary(libpath)
+ __show_commit_data = __lib.show_commit_data
+ __show_commit_data.argtypes = [c_void_p, c_void_p]
+ __show_commit_data.restype = c_char_p
+
+ res = __show_commit_data(active_tree._get_config(), proposed_tree._get_config())
+
+ return res.decode()
+
+
+def test_commit(active_tree, proposed_tree, libpath=LIBPATH):
+ if not (
+ isinstance(active_tree, ConfigTree) and isinstance(proposed_tree, ConfigTree)
+ ):
+ raise TypeError('Arguments must be instances of ConfigTree')
+
+ __lib = cdll.LoadLibrary(libpath)
+ __test_commit = __lib.test_commit
+ __test_commit.argtypes = [c_void_p, c_void_p]
+
+ __test_commit(active_tree._get_config(), proposed_tree._get_config())
+
+
def reference_tree_to_json(from_dir, to_file, internal_cache='', libpath=LIBPATH):
try:
__lib = cdll.LoadLibrary(libpath)
diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py
index 86194cd55..2b08ff68e 100644
--- a/python/vyos/defaults.py
+++ b/python/vyos/defaults.py
@@ -38,7 +38,8 @@ directories = {
'vyos_configdir' : '/opt/vyatta/config',
'completion_dir' : f'{base_dir}/completion',
'ca_certificates' : '/usr/local/share/ca-certificates/vyos',
- 'ppp_nexthop_dir' : '/run/ppp_nexthop'
+ 'ppp_nexthop_dir' : '/run/ppp_nexthop',
+ 'proto_path' : '/usr/share/vyos/vyconf'
}
systemd_services = {
@@ -69,3 +70,5 @@ rt_symbolic_names = {
rt_global_vrf = rt_symbolic_names['main']
rt_global_table = rt_symbolic_names['main']
+
+vyconfd_conf = '/etc/vyos/vyconfd.conf'
diff --git a/python/vyos/proto/__init__.py b/python/vyos/proto/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/python/vyos/proto/__init__.py
diff --git a/python/vyos/utils/auth.py b/python/vyos/utils/auth.py
index a0b3e1cae..a27d8a28a 100644
--- a/python/vyos/utils/auth.py
+++ b/python/vyos/utils/auth.py
@@ -13,10 +13,74 @@
# You should have received a copy of the GNU Lesser General Public License along with this library;
# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+import cracklib
+import math
import re
+import string
+from enum import StrEnum
+from decimal import Decimal
from vyos.utils.process import cmd
+
+DEFAULT_PASSWORD = 'vyos'
+LOW_ENTROPY_MSG = 'should be at least 8 characters long;'
+WEAK_PASSWORD_MSG= 'The password complexity is too low - @MSG@'
+
+
+class EPasswdStrength(StrEnum):
+ WEAK = 'Weak'
+ DECENT = 'Decent'
+ STRONG = 'Strong'
+
+
+def calculate_entropy(charset: str, passwd: str) -> float:
+ """
+ Calculate the entropy of a password based on the set of characters used
+ Uses E = log2(R**L) formula, where
+ - R is the range (length) of the character set
+ - L is the length of password
+ """
+ return math.log(math.pow(len(charset), len(passwd)), 2)
+
+def evaluate_strength(passwd: str) -> dict[str, str]:
+ """ Evaluates password strength and returns a check result dict """
+ charset = (cracklib.ASCII_UPPERCASE + cracklib.ASCII_LOWERCASE +
+ string.punctuation + string.digits)
+
+ result = {
+ 'strength': '',
+ 'error': '',
+ }
+
+ try:
+ cracklib.FascistCheck(passwd)
+ except ValueError as e:
+ # The password is vulnerable to dictionary attack no matter the entropy
+ if 'is' in str(e):
+ msg = str(e).replace('is', 'should not be')
+ else:
+ msg = f'should not be {e}'
+ result.update(strength=EPasswdStrength.WEAK)
+ result.update(error=WEAK_PASSWORD_MSG.replace('@MSG@', msg))
+ else:
+ # Now check the password's entropy
+ # Cast to Decimal for more precise rounding
+ entropy = Decimal.from_float(calculate_entropy(charset, passwd))
+
+ match round(entropy):
+ case e if e in range(0, 59):
+ result.update(strength=EPasswdStrength.WEAK)
+ result.update(
+ error=WEAK_PASSWORD_MSG.replace('@MSG@', LOW_ENTROPY_MSG)
+ )
+ case e if e in range(60, 119):
+ result.update(strength=EPasswdStrength.DECENT)
+ case e if e >= 120:
+ result.update(strength=EPasswdStrength.STRONG)
+
+ return result
+
def make_password_hash(password):
""" Makes a password hash for /etc/shadow using mkpasswd """
diff --git a/smoketest/scripts/cli/base_vyostest_shim.py b/smoketest/scripts/cli/base_vyostest_shim.py
index edf940efd..f0674f187 100644
--- a/smoketest/scripts/cli/base_vyostest_shim.py
+++ b/smoketest/scripts/cli/base_vyostest_shim.py
@@ -94,14 +94,18 @@ class VyOSUnitTestSHIM:
def cli_commit(self):
if self.debug:
print('commit')
- self._session.commit()
# During a commit there is a process opening commit_lock, and run()
# returns 0
while run(f'sudo lsof -nP {commit_lock}') == 0:
sleep(0.250)
+ # Return the output of commit
+ # Necessary for testing Warning cases
+ out = self._session.commit()
# Wait for CStore completion for fast non-interactive commits
sleep(self._commit_guard_time)
+ return out
+
def op_mode(self, path : list) -> None:
"""
Execute OP-mode command and return stdout
diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py
index d8d5415b5..8403dcc37 100755
--- a/smoketest/scripts/cli/test_protocols_bgp.py
+++ b/smoketest/scripts/cli/test_protocols_bgp.py
@@ -1540,6 +1540,42 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
self.assertIn(f'neighbor OVERLAY remote-as {int(ASN) + 1}', conf)
self.assertIn(f'neighbor OVERLAY local-as {int(ASN) + 1}', conf)
+ def test_bgp_30_import_vrf_routemap(self):
+ router_id = '127.0.0.3'
+ table = '1000'
+ vrf = 'red'
+ vrf_base = ['vrf', 'name', vrf]
+ self.cli_set(vrf_base + ['table', table])
+ self.cli_set(vrf_base + ['protocols', 'bgp', 'system-as', ASN])
+ self.cli_set(
+ vrf_base + ['protocols', 'bgp', 'parameters', 'router-id',
+ router_id])
+
+ self.cli_set(
+ base_path + ['address-family', 'ipv4-unicast', 'import',
+ 'vrf', vrf])
+ self.cli_set(
+ base_path + ['address-family', 'ipv4-unicast', 'route-map',
+ 'vrf', 'import', route_map_in])
+
+ self.cli_commit()
+
+ # Verify FRR bgpd configuration
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}',
+ endsection='^exit')
+ self.assertIn(f'router bgp {ASN}', frrconfig)
+ self.assertIn(f' address-family ipv4 unicast', frrconfig)
+
+ self.assertIn(f' import vrf {vrf}', frrconfig)
+ self.assertIn(f' import vrf route-map {route_map_in}', frrconfig)
+
+ # Verify FRR bgpd configuration
+ frr_vrf_config = self.getFRRconfig(
+ f'router bgp {ASN} vrf {vrf}', endsection='^exit')
+ self.assertIn(f'router bgp {ASN} vrf {vrf}', frr_vrf_config)
+ self.assertIn(f' bgp router-id {router_id}', frr_vrf_config)
+
+
def test_bgp_99_bmp(self):
target_name = 'instance-bmp'
target_address = '127.0.0.1'
diff --git a/smoketest/scripts/cli/test_system_login.py b/smoketest/scripts/cli/test_system_login.py
index d79f5521c..ed72f378e 100755
--- a/smoketest/scripts/cli/test_system_login.py
+++ b/smoketest/scripts/cli/test_system_login.py
@@ -25,7 +25,9 @@ import shutil
from base_vyostest_shim import VyOSUnitTestSHIM
+from contextlib import redirect_stdout
from gzip import GzipFile
+from io import StringIO, TextIOWrapper
from subprocess import Popen
from subprocess import PIPE
from pwd import getpwall
@@ -42,6 +44,7 @@ from vyos.xml_ref import default_value
base_path = ['system', 'login']
users = ['vyos1', 'vyos-roxx123', 'VyOS-123_super.Nice']
+weak_passwd_user = ['test_user', 'passWord1']
ssh_test_command = '/opt/vyatta/bin/vyatta-op-cmd-wrapper show version'
@@ -194,18 +197,20 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
def test_system_login_user(self):
for user in users:
name = f'VyOS Roxx {user}'
+ passwd = f'{user}-pSWd-t3st'
home_dir = f'/tmp/smoketest/{user}'
- self.cli_set(base_path + ['user', user, 'authentication', 'plaintext-password', user])
+ self.cli_set(base_path + ['user', user, 'authentication', 'plaintext-password', passwd])
self.cli_set(base_path + ['user', user, 'full-name', name])
self.cli_set(base_path + ['user', user, 'home-directory', home_dir])
self.cli_commit()
for user in users:
+ passwd = f'{user}-pSWd-t3st'
tmp = ['su','-', user]
proc = Popen(tmp, stdin=PIPE, stdout=PIPE, stderr=PIPE)
- tmp = f'{user}\nuname -a'
+ tmp = f'{passwd}\nuname -a'
proc.stdin.write(tmp.encode())
proc.stdin.flush()
(stdout, stderr) = proc.communicate()
@@ -229,6 +234,17 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
tmp = cmd(f'sudo passwd -S {locked_user}')
self.assertIn(f'{locked_user} P ', tmp)
+ def test_system_login_weak_password_warning(self):
+ self.cli_set(base_path + [
+ 'user', weak_passwd_user[0], 'authentication',
+ 'plaintext-password', weak_passwd_user[1]
+ ])
+
+ out = self.cli_commit().strip()
+
+ self.assertIn('WARNING: The password complexity is too low', out)
+ self.cli_delete(base_path + ['user', weak_passwd_user[0]])
+
def test_system_login_otp(self):
otp_user = 'otp-test_user'
otp_password = 'SuperTestPassword'
diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py
index c4af717af..53e83c3b4 100755
--- a/src/conf_mode/protocols_bgp.py
+++ b/src/conf_mode/protocols_bgp.py
@@ -523,12 +523,21 @@ def verify(config_dict):
raise ConfigError(
'Please unconfigure import vrf commands before using vpn commands in dependent VRFs!')
+ if (dict_search('route_map.vrf.import', afi_config) is not None
+ or dict_search('import.vrf', afi_config) is not None):
# FRR error: please unconfigure vpn to vrf commands before
# using import vrf commands
- if 'vpn' in afi_config['import'] or dict_search('export.vpn', afi_config) != None:
+ if ('vpn' in afi_config['import']
+ or dict_search('export.vpn', afi_config) is not None):
raise ConfigError('Please unconfigure VPN to VRF commands before '\
'using "import vrf" commands!')
+ if (dict_search('route_map.vpn.import', afi_config) is not None
+ or dict_search('route_map.vpn.export', afi_config) is not None) :
+ raise ConfigError('Please unconfigure route-map VPN to VRF commands before '\
+ 'using "import vrf" commands!')
+
+
# Verify that the export/import route-maps do exist
for export_import in ['export', 'import']:
tmp = dict_search(f'route_map.vpn.{export_import}', afi_config)
diff --git a/src/conf_mode/service_console-server.py b/src/conf_mode/service_console-server.py
index b112add3f..b83c6dfb1 100755
--- a/src/conf_mode/service_console-server.py
+++ b/src/conf_mode/service_console-server.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2021 VyOS maintainers and contributors
+# Copyright (C) 2018-2025 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
@@ -98,6 +98,12 @@ def generate(proxy):
return None
def apply(proxy):
+ if not os.path.exists('/etc/dropbear/dropbear_rsa_host_key'):
+ call('dropbearkey -t rsa -s 4096 -f /etc/dropbear/dropbear_rsa_host_key')
+
+ if not os.path.exists('/etc/dropbear/dropbear_ecdsa_host_key'):
+ call('dropbearkey -t ecdsa -f /etc/dropbear/dropbear_ecdsa_host_key')
+
call('systemctl daemon-reload')
call('systemctl stop dropbear@*.service conserver-server.service')
diff --git a/src/conf_mode/system_login.py b/src/conf_mode/system_login.py
index d3a969d9b..1e6061ecf 100755
--- a/src/conf_mode/system_login.py
+++ b/src/conf_mode/system_login.py
@@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
+import warnings
from passlib.hosts import linux_context
from psutil import users
@@ -24,11 +25,17 @@ from pwd import getpwuid
from sys import exit
from time import sleep
+from vyos.base import Warning
from vyos.config import Config
from vyos.configverify import verify_vrf
from vyos.template import render
from vyos.template import is_ipv4
-from vyos.utils.auth import get_current_user
+from vyos.utils.auth import (
+ DEFAULT_PASSWORD,
+ EPasswdStrength,
+ evaluate_strength,
+ get_current_user
+)
from vyos.utils.configfs import delete_cli_node
from vyos.utils.configfs import add_cli_node
from vyos.utils.dict import dict_search
@@ -146,6 +153,18 @@ def verify(login):
if s_user.pw_name == user and s_user.pw_uid < MIN_USER_UID:
raise ConfigError(f'User "{user}" can not be created, conflict with local system account!')
+ # T6353: Check password for complexity using cracklib.
+ # A user password should be sufficiently complex
+ plaintext_password = dict_search(
+ path='authentication.plaintext_password',
+ dict_object=user_config
+ ) or None
+
+ if plaintext_password is not None:
+ result = evaluate_strength(plaintext_password)
+ if result['strength'] == EPasswdStrength.WEAK:
+ Warning(result['error'])
+
for pubkey, pubkey_options in (dict_search('authentication.public_keys', user_config) or {}).items():
if 'type' not in pubkey_options:
raise ConfigError(f'Missing type for public-key "{pubkey}"!')
diff --git a/src/helpers/show_commit_data.py b/src/helpers/show_commit_data.py
new file mode 100755
index 000000000..d507ed9a4
--- /dev/null
+++ b/src/helpers/show_commit_data.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2025 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/>.
+#
+#
+# This script is used to show the commit data of the configuration
+
+import sys
+from pathlib import Path
+from argparse import ArgumentParser
+
+from vyos.config_mgmt import ConfigMgmt
+from vyos.configtree import ConfigTree
+from vyos.configtree import show_commit_data
+
+cm = ConfigMgmt()
+
+parser = ArgumentParser(
+ description='Show commit priority queue; no options compares the last two commits'
+)
+parser.add_argument('--active-config', help='Path to the active configuration file')
+parser.add_argument('--proposed-config', help='Path to the proposed configuration file')
+args = parser.parse_args()
+
+active_arg = args.active_config
+proposed_arg = args.proposed_config
+
+if active_arg and not proposed_arg:
+ print('--proposed-config is required when --active-config is specified')
+ sys.exit(1)
+
+if not active_arg and not proposed_arg:
+ active = cm.get_config_tree_revision(1)
+ proposed = cm.get_config_tree_revision(0)
+else:
+ if active_arg:
+ active = ConfigTree(Path(active_arg).read_text())
+ else:
+ active = cm.get_config_tree_revision(0)
+
+ proposed = ConfigTree(Path(proposed_arg).read_text())
+
+ret = show_commit_data(active, proposed)
+print(ret)
diff --git a/src/helpers/test_commit.py b/src/helpers/test_commit.py
new file mode 100755
index 000000000..00a413687
--- /dev/null
+++ b/src/helpers/test_commit.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2025 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/>.
+#
+#
+# This script is used to test execution of the commit algorithm by vyos-commitd
+
+from pathlib import Path
+from argparse import ArgumentParser
+from datetime import datetime
+
+from vyos.configtree import ConfigTree
+from vyos.configtree import test_commit
+
+
+parser = ArgumentParser(
+ description='Execute commit priority queue'
+)
+parser.add_argument(
+ '--active-config', help='Path to the active configuration file', required=True
+)
+parser.add_argument(
+ '--proposed-config', help='Path to the proposed configuration file', required=True
+)
+args = parser.parse_args()
+
+active_arg = args.active_config
+proposed_arg = args.proposed_config
+
+active = ConfigTree(Path(active_arg).read_text())
+proposed = ConfigTree(Path(proposed_arg).read_text())
+
+
+time_begin_commit = datetime.now()
+test_commit(active, proposed)
+time_end_commit = datetime.now()
+print(f'commit time: {time_end_commit - time_begin_commit}')
diff --git a/src/helpers/vyos-certbot-renew-pki.sh b/src/helpers/vyos-certbot-renew-pki.sh
index d0b663f7b..1c273d2fa 100755
--- a/src/helpers/vyos-certbot-renew-pki.sh
+++ b/src/helpers/vyos-certbot-renew-pki.sh
@@ -1,3 +1,3 @@
-#!/bin/sh
+#!/bin/vbash
source /opt/vyatta/etc/functions/script-template
/usr/libexec/vyos/conf_mode/pki.py certbot_renew
diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py
index 609b0b347..c6e9c7f6f 100755
--- a/src/op_mode/image_installer.py
+++ b/src/op_mode/image_installer.py
@@ -32,10 +32,16 @@ from errno import ENOSPC
from psutil import disk_partitions
+from vyos.base import Warning
from vyos.configtree import ConfigTree
from vyos.remote import download
from vyos.system import disk, grub, image, compat, raid, SYSTEM_CFG_VER
from vyos.template import render
+from vyos.utils.auth import (
+ DEFAULT_PASSWORD,
+ EPasswdStrength,
+ evaluate_strength
+)
from vyos.utils.io import ask_input, ask_yes_no, select_entry
from vyos.utils.file import chmod_2775
from vyos.utils.process import cmd, run, rc_cmd
@@ -83,6 +89,9 @@ MSG_WARN_ROOT_SIZE_TOOBIG: str = 'The size is too big. Try again.'
MSG_WARN_ROOT_SIZE_TOOSMALL: str = 'The size is too small. Try again'
MSG_WARN_IMAGE_NAME_WRONG: str = 'The suggested name is unsupported!\n'\
'It must be between 1 and 64 characters long and contains only the next characters: .+-_ a-z A-Z 0-9'
+
+MSG_WARN_CHANGE_PASSWORD: str = 'Default password used. Consider changing ' \
+ 'it on next login.'
MSG_WARN_PASSWORD_CONFIRM: str = 'The entered values did not match. Try again'
'Installing a different image flavor may cause functionality degradation or break your system.\n' \
'Do you want to continue with installation?'
@@ -778,10 +787,20 @@ def install_image() -> None:
while True:
user_password: str = ask_input(MSG_INPUT_PASSWORD, no_echo=True,
non_empty=True)
+
+ if user_password == DEFAULT_PASSWORD:
+ Warning(MSG_WARN_CHANGE_PASSWORD)
+ else:
+ result = evaluate_strength(user_password)
+ if result['strength'] == EPasswdStrength.WEAK:
+ Warning(result['error'])
+
confirm: str = ask_input(MSG_INPUT_PASSWORD_CONFIRM, no_echo=True,
non_empty=True)
+
if user_password == confirm:
break
+
print(MSG_WARN_PASSWORD_CONFIRM)
# ask for default console
diff --git a/src/services/vyos-commitd b/src/services/vyos-commitd
new file mode 100755
index 000000000..8dbd39058
--- /dev/null
+++ b/src/services/vyos-commitd
@@ -0,0 +1,453 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2025 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 json
+import signal
+import socket
+import typing
+import logging
+import traceback
+import importlib.util
+import io
+from contextlib import redirect_stdout
+from dataclasses import dataclass
+from dataclasses import fields
+from dataclasses import field
+from dataclasses import asdict
+from pathlib import Path
+
+import tomli
+
+from google.protobuf.json_format import MessageToDict
+from google.protobuf.json_format import ParseDict
+
+from vyos.defaults import directories
+from vyos.utils.boot import boot_configuration_complete
+from vyos.configsource import ConfigSourceCache
+from vyos.configsource import ConfigSourceError
+from vyos.config import Config
+from vyos.frrender import FRRender
+from vyos.frrender import get_frrender_dict
+from vyos import ConfigError
+
+from vyos.proto import vycall_pb2
+
+
+@dataclass
+class Status:
+ success: bool = False
+ out: str = ''
+
+
+@dataclass
+class Call:
+ script_name: str = ''
+ tag_value: str = None
+ arg_value: str = None
+ reply: Status = None
+
+ def set_reply(self, success: bool, out: str):
+ self.reply = Status(success=success, out=out)
+
+
+@dataclass
+class Session:
+ # pylint: disable=too-many-instance-attributes
+
+ session_id: str = ''
+ named_active: str = None
+ named_proposed: str = None
+ dry_run: bool = False
+ atomic: bool = False
+ background: bool = False
+ config: Config = None
+ init: Status = None
+ calls: list[Call] = field(default_factory=list)
+
+ def set_init(self, success: bool, out: str):
+ self.init = Status(success=success, out=out)
+
+
+@dataclass
+class ServerConf:
+ commitd_socket: str = ''
+ session_dir: str = ''
+ running_cache: str = ''
+ session_cache: str = ''
+
+
+server_conf = None
+SOCKET_PATH = None
+conf_mode_scripts = None
+frr = None
+
+CFG_GROUP = 'vyattacfg'
+
+script_stdout_log = '/tmp/vyos-commitd-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)
+
+
+vyos_conf_scripts_dir = directories['conf_mode']
+commitd_include_file = os.path.join(directories['data'], 'configd-include.json')
+
+
+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)
+
+
+def load_conf_mode_scripts():
+ with open(commitd_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()
+
+ # this is redundant, as all scripts are currently in the include file;
+ # leave it as an inexpensive check for future changes
+ 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)
+
+ scripts = dict(zip(imports, modules))
+
+ return scripts
+
+
+def get_session_out(session: Session) -> str:
+ out = ''
+ if session.init and session.init.out:
+ out = f'{out} + init: {session.init.out} + \n'
+ for call in session.calls:
+ reply = call.reply
+ if reply and reply.out:
+ out = f'{out} + {call.script_name}: {reply.out} + \n'
+ return out
+
+
+def write_stdout_log(file_name, session):
+ if boot_configuration_complete():
+ return
+ with open(file_name, 'a') as f:
+ f.write(get_session_out(session))
+
+
+def msg_to_commit_data(msg: vycall_pb2.Commit) -> Session:
+ # pylint: disable=no-member
+
+ d = MessageToDict(msg, preserving_proto_field_name=True)
+
+ # wrap in dataclasses
+ session = Session(**d)
+ session.init = Status(**session.init) if session.init else None
+ session.calls = list(map(lambda x: Call(**x), session.calls))
+ for call in session.calls:
+ call.reply = Status(**call.reply) if call.reply else None
+
+ return session
+
+
+def commit_data_to_msg(obj: Session) -> vycall_pb2.Commit:
+ # pylint: disable=no-member
+
+ # avoid asdict attempt of deepcopy on Config obj
+ obj.config = None
+
+ msg = vycall_pb2.Commit()
+ msg = ParseDict(asdict(obj), msg, ignore_unknown_fields=True)
+
+ return msg
+
+
+def initialization(session: Session) -> Session:
+ running_cache = os.path.join(server_conf.session_dir, server_conf.running_cache)
+ session_cache = os.path.join(server_conf.session_dir, server_conf.session_cache)
+ try:
+ configsource = ConfigSourceCache(
+ running_config_cache=running_cache,
+ session_config_cache=session_cache,
+ )
+ except ConfigSourceError as e:
+ fail_msg = f'Failed to read config caches: {e}'
+ logger.critical(fail_msg)
+ session.set_init(False, fail_msg)
+ return session
+
+ session.set_init(True, '')
+
+ config = Config(config_source=configsource)
+
+ dependent_func: dict[str, list[typing.Callable]] = {}
+ setattr(config, 'dependent_func', dependent_func)
+
+ scripts_called = []
+ setattr(config, 'scripts_called', scripts_called)
+
+ dry_run = False
+ setattr(config, 'dry_run', dry_run)
+
+ session.config = config
+
+ return session
+
+
+def run_script(script_name: str, config: Config, args: list) -> tuple[bool, 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 False, str(e)
+ except Exception:
+ tb = traceback.format_exc()
+ logger.error(tb)
+ return False, tb
+
+ return True, ''
+
+
+def process_call_data(call: Call, config: Config, last: bool = False) -> None:
+ # pylint: disable=too-many-locals
+
+ script_name = key_name_from_file_name(call.script_name)
+
+ if script_name not in conf_mode_scripts:
+ fail_msg = f'No such script: {call.script_name}'
+ logger.critical(fail_msg)
+ call.set_reply(False, fail_msg)
+ return
+
+ config.dependency_list.clear()
+
+ tag_value = call.tag_value if call.tag_value is not None else ''
+ os.environ['VYOS_TAGNODE_VALUE'] = tag_value
+
+ args = call.arg_value.split() if call.arg_value else []
+ args.insert(0, f'{script_name}.py')
+
+ 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)
+
+ with redirect_stdout(io.StringIO()) as o:
+ success, err_out = run_script(script_name, config, args)
+ amb_out = o.getvalue()
+ o.close()
+
+ out = amb_out + err_out
+
+ call.set_reply(success, out)
+
+ logger.info(f'[{script_name}] {out}')
+
+ if last:
+ scripts_called = getattr(config, 'scripts_called', [])
+ logger.debug(f'scripts_called: {scripts_called}')
+
+ if last and success:
+ tmp = get_frrender_dict(config)
+ if frr.generate(tmp):
+ # only apply a new FRR configuration if anything changed
+ # in comparison to the previous applied configuration
+ frr.apply()
+
+
+def process_session_data(session: Session) -> Session:
+ if session.init is None or not session.init.success:
+ return session
+
+ config = session.config
+ len_calls = len(session.calls)
+ for index, call in enumerate(session.calls):
+ process_call_data(call, config, last=len_calls == index + 1)
+
+ return session
+
+
+def read_message(msg: bytes) -> Session:
+ """Read message into Session instance"""
+
+ message = vycall_pb2.Commit() # pylint: disable=no-member
+ message.ParseFromString(msg)
+ session = msg_to_commit_data(message)
+
+ session = initialization(session)
+ session = process_session_data(session)
+
+ write_stdout_log(script_stdout_log, session)
+
+ return session
+
+
+def write_reply(session: Session) -> bytearray:
+ """Serialize modified object to bytearray, prepending data length
+ header"""
+
+ reply = commit_data_to_msg(session)
+ encoded_data = reply.SerializeToString()
+ byte_size = reply.ByteSize()
+ length_bytes = byte_size.to_bytes(4)
+ arr = bytearray(length_bytes)
+ arr.extend(encoded_data)
+
+ return arr
+
+
+def load_server_conf() -> ServerConf:
+ # pylint: disable=import-outside-toplevel
+ # pylint: disable=broad-exception-caught
+ from vyos.defaults import vyconfd_conf
+
+ try:
+ with open(vyconfd_conf, 'rb') as f:
+ vyconfd_conf_d = tomli.load(f)
+
+ except Exception as e:
+ logger.critical(f'Failed to open the vyconfd.conf file {vyconfd_conf}: {e}')
+ sys.exit(1)
+
+ app = vyconfd_conf_d.get('appliance', {})
+
+ conf_data = {
+ k: v for k, v in app.items() if k in [_.name for _ in fields(ServerConf)]
+ }
+
+ conf = ServerConf(**conf_data)
+
+ return conf
+
+
+def remove_if_exists(f: str):
+ try:
+ os.unlink(f)
+ except FileNotFoundError:
+ pass
+
+
+def sig_handler(_signum, _frame):
+ logger.info('stopping server')
+ raise KeyboardInterrupt
+
+
+def run_server():
+ # pylint: disable=global-statement
+
+ global server_conf
+ global SOCKET_PATH
+ global conf_mode_scripts
+ global frr
+
+ signal.signal(signal.SIGTERM, sig_handler)
+ signal.signal(signal.SIGINT, sig_handler)
+
+ logger.info('starting server')
+
+ server_conf = load_server_conf()
+ SOCKET_PATH = server_conf.commitd_socket
+ conf_mode_scripts = load_conf_mode_scripts()
+
+ cfg_group = grp.getgrnam(CFG_GROUP)
+ os.setgid(cfg_group.gr_gid)
+
+ server_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+
+ remove_if_exists(SOCKET_PATH)
+ server_socket.bind(SOCKET_PATH)
+ Path(SOCKET_PATH).chmod(0o775)
+
+ # We only need one long-lived instance of FRRender
+ frr = FRRender()
+
+ server_socket.listen(2)
+ while True:
+ try:
+ conn, _ = server_socket.accept()
+ logger.debug('connection accepted')
+ while True:
+ # receive size of data
+ data_length = conn.recv(4)
+ if not data_length:
+ logger.debug('no data')
+ # if no data break
+ break
+
+ length = int.from_bytes(data_length)
+ # receive data
+ data = conn.recv(length)
+
+ session = read_message(data)
+ reply = write_reply(session)
+ conn.sendall(reply)
+
+ conn.close()
+ logger.debug('connection closed')
+
+ except KeyboardInterrupt:
+ break
+
+ server_socket.close()
+ sys.exit(0)
+
+
+if __name__ == '__main__':
+ run_server()
diff --git a/src/systemd/vyos-commitd.service b/src/systemd/vyos-commitd.service
new file mode 100644
index 000000000..5b083f500
--- /dev/null
+++ b/src/systemd/vyos-commitd.service
@@ -0,0 +1,27 @@
+[Unit]
+Description=VyOS commit 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-commitd
+Type=idle
+
+SyslogIdentifier=vyos-commitd
+SyslogFacility=daemon
+
+Restart=on-failure
+
+# Does't work in Jessie but leave it here
+User=root
+Group=vyattacfg
+
+[Install]
+WantedBy=vyos.target
diff --git a/src/tests/test_config_diff.py b/src/tests/test_config_diff.py
index 39e17613a..4017fff4d 100644
--- a/src/tests/test_config_diff.py
+++ b/src/tests/test_config_diff.py
@@ -31,11 +31,11 @@ class TestConfigDiff(TestCase):
def test_unit(self):
diff = vyos.configtree.DiffTree(self.config_left, self.config_null)
sub = diff.sub
- self.assertEqual(sub.to_string(), self.config_left.to_string())
+ self.assertEqual(sub, self.config_left)
diff = vyos.configtree.DiffTree(self.config_null, self.config_left)
add = diff.add
- self.assertEqual(add.to_string(), self.config_left.to_string())
+ self.assertEqual(add, self.config_left)
def test_symmetry(self):
lr_diff = vyos.configtree.DiffTree(self.config_left,
@@ -45,10 +45,10 @@ class TestConfigDiff(TestCase):
sub = lr_diff.sub
add = rl_diff.add
- self.assertEqual(sub.to_string(), add.to_string())
+ self.assertEqual(sub, add)
add = lr_diff.add
sub = rl_diff.sub
- self.assertEqual(add.to_string(), sub.to_string())
+ self.assertEqual(add, sub)
def test_identity(self):
lr_diff = vyos.configtree.DiffTree(self.config_left,
@@ -61,6 +61,9 @@ class TestConfigDiff(TestCase):
r_union = vyos.configtree.union(add, inter)
l_union = vyos.configtree.union(sub, inter)
+ # here we must compare string representations instead of using
+ # dunder equal, as we assert equivalence of the values list, which
+ # is optionally ordered at render
self.assertEqual(r_union.to_string(),
self.config_right.to_string(ordered_values=True))
self.assertEqual(l_union.to_string(),