Skip to content

Commit 78a3ba7

Browse files
authoredMar 18, 2025··
Merge pull request #4398 from jestabro/commitd
T7121: Set up communication vyconfd to vyos-commitd
2 parents 62ebdb8 + d8a6295 commit 78a3ba7

14 files changed

+727
-17
lines changed
 

‎.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,9 @@ data/reftree.cache
151151
# autogenerated vyos-configd JSON definition
152152
data/configd-include.json
153153

154+
# autogenerated vyos-commitd protobuf files
155+
python/vyos/proto/*pb2.py
156+
154157
# We do not use pip
155158
Pipfile
156159
Pipfile.lock

‎Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ libvyosconfig:
2727
rm -rf /tmp/libvyosconfig && \
2828
git clone https://github.com/vyos/libvyosconfig.git /tmp/libvyosconfig || exit 1
2929
cd /tmp/libvyosconfig && \
30-
git checkout 677d1e2bf8109b9fd4da60e20376f992b747e384 || exit 1
30+
git checkout 5f15d8095efd11756a867e552a3f8fe6c77e57cc || exit 1
3131
eval $$(opam env --root=/opt/opam --set-root) && ./build.sh
3232
fi
3333

‎debian/control

+4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ Build-Depends:
1515
# For generating command definitions
1616
python3-lxml,
1717
python3-xmltodict,
18+
# For generating serialization functions
19+
protobuf-compiler,
1820
# For running tests
1921
python3-coverage,
2022
python3-hurry.filesize,
@@ -70,13 +72,15 @@ Depends:
7072
python3-netifaces,
7173
python3-paramiko,
7274
python3-passlib,
75+
python3-protobuf,
7376
python3-pyroute2,
7477
python3-psutil,
7578
python3-pyhumps,
7679
python3-pystache,
7780
python3-pyudev,
7881
python3-six,
7982
python3-tabulate,
83+
python3-tomli,
8084
python3-voluptuous,
8185
python3-xmltodict,
8286
python3-zmq,

‎python/setup.py

+38
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import os
2+
import sys
3+
import subprocess
24
from setuptools import setup
5+
from setuptools.command.build_py import build_py
6+
7+
sys.path.append('./vyos')
8+
from defaults import directories
39

410
def packages(directory):
511
return [
@@ -8,6 +14,35 @@ def packages(directory):
814
if os.path.isfile(os.path.join(_[0], '__init__.py'))
915
]
1016

17+
18+
class GenerateProto(build_py):
19+
ver = os.environ.get('OCAML_VERSION')
20+
if ver:
21+
proto_path = f'/opt/opam/{ver}/share/vyconf'
22+
else:
23+
proto_path = directories['proto_path']
24+
25+
def run(self):
26+
# find all .proto files in vyconf proto_path
27+
proto_files = []
28+
for _, _, files in os.walk(self.proto_path):
29+
for file in files:
30+
if file.endswith('.proto'):
31+
proto_files.append(file)
32+
33+
# compile each .proto file to Python
34+
for proto_file in proto_files:
35+
subprocess.check_call(
36+
[
37+
'protoc',
38+
'--python_out=vyos/proto',
39+
f'--proto_path={self.proto_path}/',
40+
proto_file,
41+
]
42+
)
43+
44+
build_py.run(self)
45+
1146
setup(
1247
name = "vyos",
1348
version = "1.3.0",
@@ -29,4 +64,7 @@ def packages(directory):
2964
"config-mgmt = vyos.config_mgmt:run",
3065
],
3166
},
67+
cmdclass={
68+
'build_py': GenerateProto,
69+
},
3270
)

‎python/vyos/config_mgmt.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ def revert_soft(self) -> Tuple[str, int]:
287287

288288
# commits under commit-confirm are not added to revision list unless
289289
# confirmed, hence a soft revert is to revision 0
290-
revert_ct = self._get_config_tree_revision(0)
290+
revert_ct = self.get_config_tree_revision(0)
291291

292292
message = '[commit-confirm] Reverting to previous config now'
293293
os.system('wall -n ' + message)
@@ -351,7 +351,7 @@ def rollback_soft(self, rev: int):
351351
)
352352
return msg, 1
353353

354-
rollback_ct = self._get_config_tree_revision(rev)
354+
rollback_ct = self.get_config_tree_revision(rev)
355355
try:
356356
load(rollback_ct, switch='explicit')
357357
print('Rollback diff has been applied.')
@@ -382,15 +382,15 @@ def compare(
382382
if rev1 is not None:
383383
if not self._check_revision_number(rev1):
384384
return f'Invalid revision number {rev1}', 1
385-
ct1 = self._get_config_tree_revision(rev1)
385+
ct1 = self.get_config_tree_revision(rev1)
386386
ct2 = self.working_config
387387
msg = f'No changes between working and revision {rev1} configurations.\n'
388388
if rev2 is not None:
389389
if not self._check_revision_number(rev2):
390390
return f'Invalid revision number {rev2}', 1
391391
# compare older to newer
392392
ct2 = ct1
393-
ct1 = self._get_config_tree_revision(rev2)
393+
ct1 = self.get_config_tree_revision(rev2)
394394
msg = f'No changes between revisions {rev2} and {rev1} configurations.\n'
395395

396396
out = ''
@@ -575,7 +575,7 @@ def _get_file_revision(self, rev: int):
575575
r = f.read().decode()
576576
return r
577577

578-
def _get_config_tree_revision(self, rev: int):
578+
def get_config_tree_revision(self, rev: int):
579579
c = self._get_file_revision(rev)
580580
return ConfigTree(c)
581581

‎python/vyos/configsource.py

+10
Original file line numberDiff line numberDiff line change
@@ -319,3 +319,13 @@ def __init__(self, running_config_text=None, session_config_text=None):
319319
self._session_config = ConfigTree(session_config_text) if session_config_text else None
320320
except ValueError:
321321
raise ConfigSourceError(f"Init error in {type(self)}")
322+
323+
class ConfigSourceCache(ConfigSource):
324+
def __init__(self, running_config_cache=None, session_config_cache=None):
325+
super().__init__()
326+
327+
try:
328+
self._running_config = ConfigTree(internal=running_config_cache) if running_config_cache else None
329+
self._session_config = ConfigTree(internal=session_config_cache) if session_config_cache else None
330+
except ValueError:
331+
raise ConfigSourceError(f"Init error in {type(self)}")

‎python/vyos/configtree.py

+70-6
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,14 @@ class ConfigTreeError(Exception):
6666

6767

6868
class ConfigTree(object):
69-
def __init__(self, config_string=None, address=None, libpath=LIBPATH):
70-
if config_string is None and address is None:
71-
raise TypeError("ConfigTree() requires one of 'config_string' or 'address'")
69+
def __init__(
70+
self, config_string=None, address=None, internal=None, libpath=LIBPATH
71+
):
72+
if config_string is None and address is None and internal is None:
73+
raise TypeError(
74+
"ConfigTree() requires one of 'config_string', 'address', or 'internal'"
75+
)
76+
7277
self.__config = None
7378
self.__lib = cdll.LoadLibrary(libpath)
7479

@@ -89,6 +94,13 @@ def __init__(self, config_string=None, address=None, libpath=LIBPATH):
8994
self.__to_commands.argtypes = [c_void_p, c_char_p]
9095
self.__to_commands.restype = c_char_p
9196

97+
self.__read_internal = self.__lib.read_internal
98+
self.__read_internal.argtypes = [c_char_p]
99+
self.__read_internal.restype = c_void_p
100+
101+
self.__write_internal = self.__lib.write_internal
102+
self.__write_internal.argtypes = [c_void_p, c_char_p]
103+
92104
self.__to_json = self.__lib.to_json
93105
self.__to_json.argtypes = [c_void_p]
94106
self.__to_json.restype = c_char_p
@@ -168,7 +180,21 @@ def __init__(self, config_string=None, address=None, libpath=LIBPATH):
168180
self.__destroy = self.__lib.destroy
169181
self.__destroy.argtypes = [c_void_p]
170182

171-
if address is None:
183+
self.__equal = self.__lib.equal
184+
self.__equal.argtypes = [c_void_p, c_void_p]
185+
self.__equal.restype = c_bool
186+
187+
if address is not None:
188+
self.__config = address
189+
self.__version = ''
190+
elif internal is not None:
191+
config = self.__read_internal(internal.encode())
192+
if config is None:
193+
msg = self.__get_error().decode()
194+
raise ValueError('Failed to read internal rep: {0}'.format(msg))
195+
else:
196+
self.__config = config
197+
elif config_string is not None:
172198
config_section, version_section = extract_version(config_string)
173199
config_section = escape_backslash(config_section)
174200
config = self.__from_string(config_section.encode())
@@ -179,8 +205,9 @@ def __init__(self, config_string=None, address=None, libpath=LIBPATH):
179205
self.__config = config
180206
self.__version = version_section
181207
else:
182-
self.__config = address
183-
self.__version = ''
208+
raise TypeError(
209+
"ConfigTree() requires one of 'config_string', 'address', or 'internal'"
210+
)
184211

185212
self.__migration = os.environ.get('VYOS_MIGRATION')
186213
if self.__migration:
@@ -190,6 +217,11 @@ def __del__(self):
190217
if self.__config is not None:
191218
self.__destroy(self.__config)
192219

220+
def __eq__(self, other):
221+
if isinstance(other, ConfigTree):
222+
return self.__equal(self._get_config(), other._get_config())
223+
return False
224+
193225
def __str__(self):
194226
return self.to_string()
195227

@@ -199,6 +231,9 @@ def _get_config(self):
199231
def get_version_string(self):
200232
return self.__version
201233

234+
def write_cache(self, file_name):
235+
self.__write_internal(self._get_config(), file_name)
236+
202237
def to_string(self, ordered_values=False, no_version=False):
203238
config_string = self.__to_string(self.__config, ordered_values).decode()
204239
config_string = unescape_backslash(config_string)
@@ -488,6 +523,35 @@ def mask_inclusive(left, right, libpath=LIBPATH):
488523
return tree
489524

490525

526+
def show_commit_data(active_tree, proposed_tree, libpath=LIBPATH):
527+
if not (
528+
isinstance(active_tree, ConfigTree) and isinstance(proposed_tree, ConfigTree)
529+
):
530+
raise TypeError('Arguments must be instances of ConfigTree')
531+
532+
__lib = cdll.LoadLibrary(libpath)
533+
__show_commit_data = __lib.show_commit_data
534+
__show_commit_data.argtypes = [c_void_p, c_void_p]
535+
__show_commit_data.restype = c_char_p
536+
537+
res = __show_commit_data(active_tree._get_config(), proposed_tree._get_config())
538+
539+
return res.decode()
540+
541+
542+
def test_commit(active_tree, proposed_tree, libpath=LIBPATH):
543+
if not (
544+
isinstance(active_tree, ConfigTree) and isinstance(proposed_tree, ConfigTree)
545+
):
546+
raise TypeError('Arguments must be instances of ConfigTree')
547+
548+
__lib = cdll.LoadLibrary(libpath)
549+
__test_commit = __lib.test_commit
550+
__test_commit.argtypes = [c_void_p, c_void_p]
551+
552+
__test_commit(active_tree._get_config(), proposed_tree._get_config())
553+
554+
491555
def reference_tree_to_json(from_dir, to_file, internal_cache='', libpath=LIBPATH):
492556
try:
493557
__lib = cdll.LoadLibrary(libpath)

‎python/vyos/defaults.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
'vyos_configdir' : '/opt/vyatta/config',
3939
'completion_dir' : f'{base_dir}/completion',
4040
'ca_certificates' : '/usr/local/share/ca-certificates/vyos',
41-
'ppp_nexthop_dir' : '/run/ppp_nexthop'
41+
'ppp_nexthop_dir' : '/run/ppp_nexthop',
42+
'proto_path' : '/usr/share/vyos/vyconf'
4243
}
4344

4445
systemd_services = {
@@ -69,3 +70,5 @@
6970

7071
rt_global_vrf = rt_symbolic_names['main']
7172
rt_global_table = rt_symbolic_names['main']
73+
74+
vyconfd_conf = '/etc/vyos/vyconfd.conf'

‎python/vyos/proto/__init__.py

Whitespace-only changes.

‎src/helpers/show_commit_data.py

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright (C) 2025 VyOS maintainers and contributors
4+
#
5+
# This program is free software; you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License version 2 or later as
7+
# published by the Free Software Foundation.
8+
#
9+
# This program is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
# GNU General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU General Public License
15+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
#
17+
#
18+
# This script is used to show the commit data of the configuration
19+
20+
import sys
21+
from pathlib import Path
22+
from argparse import ArgumentParser
23+
24+
from vyos.config_mgmt import ConfigMgmt
25+
from vyos.configtree import ConfigTree
26+
from vyos.configtree import show_commit_data
27+
28+
cm = ConfigMgmt()
29+
30+
parser = ArgumentParser(
31+
description='Show commit priority queue; no options compares the last two commits'
32+
)
33+
parser.add_argument('--active-config', help='Path to the active configuration file')
34+
parser.add_argument('--proposed-config', help='Path to the proposed configuration file')
35+
args = parser.parse_args()
36+
37+
active_arg = args.active_config
38+
proposed_arg = args.proposed_config
39+
40+
if active_arg and not proposed_arg:
41+
print('--proposed-config is required when --active-config is specified')
42+
sys.exit(1)
43+
44+
if not active_arg and not proposed_arg:
45+
active = cm.get_config_tree_revision(1)
46+
proposed = cm.get_config_tree_revision(0)
47+
else:
48+
if active_arg:
49+
active = ConfigTree(Path(active_arg).read_text())
50+
else:
51+
active = cm.get_config_tree_revision(0)
52+
53+
proposed = ConfigTree(Path(proposed_arg).read_text())
54+
55+
ret = show_commit_data(active, proposed)
56+
print(ret)

‎src/helpers/test_commit.py

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright (C) 2025 VyOS maintainers and contributors
4+
#
5+
# This program is free software; you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License version 2 or later as
7+
# published by the Free Software Foundation.
8+
#
9+
# This program is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
# GNU General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU General Public License
15+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
#
17+
#
18+
# This script is used to test execution of the commit algorithm by vyos-commitd
19+
20+
from pathlib import Path
21+
from argparse import ArgumentParser
22+
from datetime import datetime
23+
24+
from vyos.configtree import ConfigTree
25+
from vyos.configtree import test_commit
26+
27+
28+
parser = ArgumentParser(
29+
description='Execute commit priority queue'
30+
)
31+
parser.add_argument(
32+
'--active-config', help='Path to the active configuration file', required=True
33+
)
34+
parser.add_argument(
35+
'--proposed-config', help='Path to the proposed configuration file', required=True
36+
)
37+
args = parser.parse_args()
38+
39+
active_arg = args.active_config
40+
proposed_arg = args.proposed_config
41+
42+
active = ConfigTree(Path(active_arg).read_text())
43+
proposed = ConfigTree(Path(proposed_arg).read_text())
44+
45+
46+
time_begin_commit = datetime.now()
47+
test_commit(active, proposed)
48+
time_end_commit = datetime.now()
49+
print(f'commit time: {time_end_commit - time_begin_commit}')

‎src/services/vyos-commitd

+453
Large diffs are not rendered by default.

‎src/systemd/vyos-commitd.service

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[Unit]
2+
Description=VyOS commit daemon
3+
4+
# Without this option, lots of default dependencies are added,
5+
# among them network.target, which creates a dependency cycle
6+
DefaultDependencies=no
7+
8+
# Seemingly sensible way to say "as early as the system is ready"
9+
# All vyos-configd needs is read/write mounted root
10+
After=systemd-remount-fs.service
11+
Before=vyos-router.service
12+
13+
[Service]
14+
ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-commitd
15+
Type=idle
16+
17+
SyslogIdentifier=vyos-commitd
18+
SyslogFacility=daemon
19+
20+
Restart=on-failure
21+
22+
# Does't work in Jessie but leave it here
23+
User=root
24+
Group=vyattacfg
25+
26+
[Install]
27+
WantedBy=vyos.target

‎src/tests/test_config_diff.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ def setUp(self):
3131
def test_unit(self):
3232
diff = vyos.configtree.DiffTree(self.config_left, self.config_null)
3333
sub = diff.sub
34-
self.assertEqual(sub.to_string(), self.config_left.to_string())
34+
self.assertEqual(sub, self.config_left)
3535

3636
diff = vyos.configtree.DiffTree(self.config_null, self.config_left)
3737
add = diff.add
38-
self.assertEqual(add.to_string(), self.config_left.to_string())
38+
self.assertEqual(add, self.config_left)
3939

4040
def test_symmetry(self):
4141
lr_diff = vyos.configtree.DiffTree(self.config_left,
@@ -45,10 +45,10 @@ def test_symmetry(self):
4545

4646
sub = lr_diff.sub
4747
add = rl_diff.add
48-
self.assertEqual(sub.to_string(), add.to_string())
48+
self.assertEqual(sub, add)
4949
add = lr_diff.add
5050
sub = rl_diff.sub
51-
self.assertEqual(add.to_string(), sub.to_string())
51+
self.assertEqual(add, sub)
5252

5353
def test_identity(self):
5454
lr_diff = vyos.configtree.DiffTree(self.config_left,
@@ -61,6 +61,9 @@ def test_identity(self):
6161
r_union = vyos.configtree.union(add, inter)
6262
l_union = vyos.configtree.union(sub, inter)
6363

64+
# here we must compare string representations instead of using
65+
# dunder equal, as we assert equivalence of the values list, which
66+
# is optionally ordered at render
6467
self.assertEqual(r_union.to_string(),
6568
self.config_right.to_string(ordered_values=True))
6669
self.assertEqual(l_union.to_string(),

0 commit comments

Comments
 (0)
Please sign in to comment.