summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOleksandr Kuchmystyi <o.kuchmystyi@vyos.io>2026-03-24 16:26:33 +0300
committerOleksandr Kuchmystyi <o.kuchmystyi@vyos.io>2026-04-01 11:30:15 +0300
commit54fc0e1199625c8b6223e5d034bf895cfbf57c77 (patch)
tree0dc3ba44371d966fde332bba7d60357d38c71cfc
parent3823523c01e07ca010d035db7311b3c7f0e4e511 (diff)
downloadvyos-1x-54fc0e1199625c8b6223e5d034bf895cfbf57c77.tar.gz
vyos-1x-54fc0e1199625c8b6223e5d034bf895cfbf57c77.zip
config-sync: T7784: Add command to diff configuration with secondary node
Add a new operational command to compare configuration between nodes participating in config synchronization. New command: - `show configuration secondary sync [commands] [running|candidate|saved] [config-node-path]`. This allows operators to view configuration differences across secondary peer before applying or syncing changes. Supports: - displaying using raw diff and 'commands' format; - optional section filtering (subtree comparison); - selectable config source (running, candidate, saved).
-rw-r--r--data/op-mode-standardized.json1
-rw-r--r--op-mode-definitions/include/show-config-sync-section-commands.xml.i449
-rw-r--r--op-mode-definitions/include/show-config-sync-section.xml.i449
-rw-r--r--op-mode-definitions/show-config-sync.xml.in98
-rw-r--r--python/vyos/config_mgmt.py75
-rw-r--r--python/vyos/http_api_client.py148
-rw-r--r--python/vyos/utils/list.py20
-rw-r--r--smoketest/scripts/cli/test_service_config-sync.py174
-rw-r--r--src/op_mode/config_sync.py193
9 files changed, 1607 insertions, 0 deletions
diff --git a/data/op-mode-standardized.json b/data/op-mode-standardized.json
index d1f23bfb0..3dff070a1 100644
--- a/data/op-mode-standardized.json
+++ b/data/op-mode-standardized.json
@@ -5,6 +5,7 @@
"bridge.py",
"cgnat.py",
"config_mgmt.py",
+"config_sync.py",
"conntrack.py",
"container.py",
"cpu.py",
diff --git a/op-mode-definitions/include/show-config-sync-section-commands.xml.i b/op-mode-definitions/include/show-config-sync-section-commands.xml.i
new file mode 100644
index 000000000..98ca435b7
--- /dev/null
+++ b/op-mode-definitions/include/show-config-sync-section-commands.xml.i
@@ -0,0 +1,449 @@
+<!-- included start from show-config-sync-section-commands.xml.i -->
+<leafNode name="firewall">
+ <properties>
+ <help>Firewall</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="firewall" --source="$6"</command>
+</leafNode>
+<node name="interfaces">
+ <properties>
+ <help>Interfaces</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="interfaces" --source="$6"</command>
+ <children>
+ <leafNode name="bonding">
+ <properties>
+ <help>Bonding interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="interfaces bonding" --source="$6"</command>
+ </leafNode>
+ <leafNode name="bridge">
+ <properties>
+ <help>Bridge interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="interfaces bridge" --source="$6"</command>
+ </leafNode>
+ <leafNode name="dummy">
+ <properties>
+ <help>Dummy interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="interfaces dummy" --source="$6"</command>
+ </leafNode>
+ <leafNode name="ethernet">
+ <properties>
+ <help>Ethernet interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="interfaces ethernet" --source="$6"</command>
+ </leafNode>
+ <leafNode name="geneve">
+ <properties>
+ <help>GENEVE interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="interfaces geneve" --source="$6"</command>
+ </leafNode>
+ <leafNode name="input">
+ <properties>
+ <help>Input interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="interfaces input" --source="$6"</command>
+ </leafNode>
+ <leafNode name="l2tpv3">
+ <properties>
+ <help>L2TPv3 interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="interfaces l2tpv3" --source="$6"</command>
+ </leafNode>
+ <leafNode name="loopback">
+ <properties>
+ <help>Loopback interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="interfaces loopback" --source="$6"</command>
+ </leafNode>
+ <leafNode name="macsec">
+ <properties>
+ <help>MACsec interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="interfaces macsec" --source="$6"</command>
+ </leafNode>
+ <leafNode name="openvpn">
+ <properties>
+ <help>OpenVPN interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="interfaces openvpn" --source="$6"</command>
+ </leafNode>
+ <leafNode name="pppoe">
+ <properties>
+ <help>PPPoE interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="interfaces pppoe" --source="$6"</command>
+ </leafNode>
+ <leafNode name="pseudo-ethernet">
+ <properties>
+ <help>Pseudo-Ethernet interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="interfaces pseudo-ethernet" --source="$6"</command>
+ </leafNode>
+ <leafNode name="sstpc">
+ <properties>
+ <help>SSTP client interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="interfaces sstpc" --source="$6"</command>
+ </leafNode>
+ <leafNode name="tunnel">
+ <properties>
+ <help>Tunnel interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="interfaces tunnel" --source="$6"</command>
+ </leafNode>
+ <leafNode name="virtual-ethernet">
+ <properties>
+ <help>Virtual Ethernet interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="interfaces virtual-ethernet" --source="$6"</command>
+ </leafNode>
+ <leafNode name="vti">
+ <properties>
+ <help>Virtual tunnel interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="interfaces vti" --source="$6"</command>
+ </leafNode>
+ <leafNode name="vxlan">
+ <properties>
+ <help>VXLAN interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="interfaces vxlan" --source="$6"</command>
+ </leafNode>
+ <leafNode name="wireguard">
+ <properties>
+ <help>Wireguard interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="interfaces wireguard" --source="$6"</command>
+ </leafNode>
+ <leafNode name="wireless">
+ <properties>
+ <help>Wireless interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="interfaces wireless" --source="$6"</command>
+ </leafNode>
+ <leafNode name="wwan">
+ <properties>
+ <help>WWAN interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="interfaces wwan" --source="$6"</command>
+ </leafNode>
+ </children>
+</node>
+<leafNode name="nat">
+ <properties>
+ <help>NAT</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="nat" --source="$6"</command>
+</leafNode>
+<leafNode name="nat66">
+ <properties>
+ <help>NAT66</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="nat66" --source="$6"</command>
+</leafNode>
+<leafNode name="pki">
+ <properties>
+ <help>Public key infrastructure (PKI)</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="pki" --source="$6"</command>
+</leafNode>
+<leafNode name="policy">
+ <properties>
+ <help>Routing policy</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="policy" --source="$6"</command>
+</leafNode>
+<node name="protocols">
+ <properties>
+ <help>Routing protocols</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="protocols" --source="$6"</command>
+ <children>
+ <leafNode name="babel">
+ <properties>
+ <help>Babel Routing Protocol</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="protocols babel" --source="$6"</command>
+ </leafNode>
+ <leafNode name="bfd">
+ <properties>
+ <help>Bidirectional Forwarding Detection (BFD)</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="protocols bfd" --source="$6"</command>
+ </leafNode>
+ <leafNode name="bgp">
+ <properties>
+ <help>Border Gateway Protocol (BGP)</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="protocols bgp" --source="$6"</command>
+ </leafNode>
+ <leafNode name="failover">
+ <properties>
+ <help>Failover route</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="protocols failover" --source="$6"</command>
+ </leafNode>
+ <leafNode name="igmp-proxy">
+ <properties>
+ <help>Internet Group Management Protocol (IGMP) proxy</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="protocols igmp-proxy" --source="$6"</command>
+ </leafNode>
+ <leafNode name="isis">
+ <properties>
+ <help>Intermediate System to Intermediate System (IS-IS)</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="protocols isis" --source="$6"</command>
+ </leafNode>
+ <leafNode name="mpls">
+ <properties>
+ <help>Multiprotocol Label Switching (MPLS)</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="protocols mpls" --source="$6"</command>
+ </leafNode>
+ <leafNode name="nhrp">
+ <properties>
+ <help>Next Hop Resolution Protocol (NHRP) parameters</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="protocols nhrp" --source="$6"</command>
+ </leafNode>
+ <leafNode name="ospf">
+ <properties>
+ <help>Open Shortest Path First (OSPF)</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="protocols ospf" --source="$6"</command>
+ </leafNode>
+ <leafNode name="ospfv3">
+ <properties>
+ <help>Open Shortest Path First (OSPF) for IPv6</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="protocols ospfv3" --source="$6"</command>
+ </leafNode>
+ <leafNode name="pim">
+ <properties>
+ <help>Protocol Independent Multicast (PIM) and IGMP</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="protocols pim" --source="$6"</command>
+ </leafNode>
+ <leafNode name="pim6">
+ <properties>
+ <help>Protocol Independent Multicast for IPv6 (PIMv6) and MLD</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="protocols pim6" --source="$6"</command>
+ </leafNode>
+ <leafNode name="rip">
+ <properties>
+ <help>Routing Information Protocol (RIP) parameters</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="protocols rip" --source="$6"</command>
+ </leafNode>
+ <leafNode name="ripng">
+ <properties>
+ <help>Routing Information Protocol (RIPng) parameters</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="protocols ripng" --source="$6"</command>
+ </leafNode>
+ <leafNode name="rpki">
+ <properties>
+ <help>Resource Public Key Infrastructure (RPKI)</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="protocols rpki" --source="$6"</command>
+ </leafNode>
+ <leafNode name="segment-routing">
+ <properties>
+ <help>Segment-Routing (SR) parameters</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="protocols segment-routing" --source="$6"</command>
+ </leafNode>
+ <leafNode name="static">
+ <properties>
+ <help>Static Routing</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="protocols static" --source="$6"</command>
+ </leafNode>
+ </children>
+</node>
+<node name="qos">
+ <properties>
+ <help>Quality of Service (QoS)</help>
+ </properties>
+ <children>
+ <leafNode name="interface">
+ <properties>
+ <help>Interface to apply QoS policy</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="qos interface" --source="$6"</command>
+ </leafNode>
+ <leafNode name="policy">
+ <properties>
+ <help>Service Policy definitions</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="qos policy" --source="$6"</command>
+ </leafNode>
+ </children>
+</node>
+<node name="service">
+ <properties>
+ <help>System services</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="service" --source="$6"</command>
+ <children>
+ <leafNode name="console-server">
+ <properties>
+ <help>Serial Console Server</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="service console-server" --source="$6"</command>
+ </leafNode>
+ <leafNode name="dhcp-relay">
+ <properties>
+ <help>Host Configuration Protocol (DHCP) relay agent</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="service dhcp-relay" --source="$6"</command>
+ </leafNode>
+ <leafNode name="dhcp-server">
+ <properties>
+ <help>Dynamic Host Configuration Protocol (DHCP) for DHCP server</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="service dhcp-server" --source="$6"</command>
+ </leafNode>
+ <leafNode name="dhcpv6-relay">
+ <properties>
+ <help>DHCPv6 Relay Agent parameters</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="service dhcpv6-relay" --source="$6"</command>
+ </leafNode>
+ <leafNode name="dhcpv6-server">
+ <properties>
+ <help>DHCP for IPv6 (DHCPv6) server</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="service dhcpv6-server" --source="$6"</command>
+ </leafNode>
+ <leafNode name="dns">
+ <properties>
+ <help>Domain Name System (DNS) related services</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="service dns" --source="$6"</command>
+ </leafNode>
+ <leafNode name="lldp">
+ <properties>
+ <help>LLDP settings</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="service lldp" --source="$6"</command>
+ </leafNode>
+ <leafNode name="mdns">
+ <properties>
+ <help>Multicast DNS (mDNS) parameters</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="service mdns" --source="$6"</command>
+ </leafNode>
+ <leafNode name="monitoring">
+ <properties>
+ <help>Monitoring services</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="service monitoring" --source="$6"</command>
+ </leafNode>
+ <leafNode name="ndp-proxy">
+ <properties>
+ <help>Neighbor Discovery Protocol (NDP) Proxy</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="service ndp-proxy" --source="$6"</command>
+ </leafNode>
+ <leafNode name="ntp">
+ <properties>
+ <help>Network Time Protocol (NTP) configuration</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="service ntp" --source="$6"</command>
+ </leafNode>
+ <leafNode name="snmp">
+ <properties>
+ <help>Simple Network Management Protocol (SNMP)</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="service snmp" --source="$6"</command>
+ </leafNode>
+ <leafNode name="tftp-server">
+ <properties>
+ <help>Trivial File Transfer Protocol (TFTP) server</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="service tftp-server" --source="$6"</command>
+ </leafNode>
+ <leafNode name="webproxy">
+ <properties>
+ <help>Webproxy service settings</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="service webproxy" --source="$6"</command>
+ </leafNode>
+ </children>
+</node>
+<node name="system">
+ <properties>
+ <help>System parameters</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="system" --source="$6"</command>
+ <children>
+ <leafNode name="conntrack">
+ <properties>
+ <help>Connection Tracking</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="system conntrack" --source="$6"</command>
+ </leafNode>
+ <leafNode name="flow-accounting">
+ <properties>
+ <help>Flow accounting</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="system flow-accounting" --source="$6"</command>
+ </leafNode>
+ <leafNode name="login">
+ <properties>
+ <help>System User Login</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="system login" --source="$6"</command>
+ </leafNode>
+ <leafNode name="option">
+ <properties>
+ <help>System Options</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="system option" --source="$6"</command>
+ </leafNode>
+ <leafNode name="sflow">
+ <properties>
+ <help>sFlow</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="system sflow" --source="$6"</command>
+ </leafNode>
+ <leafNode name="static-host-mapping">
+ <properties>
+ <help>Map host names to addresses</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="system static-host-mapping" --source="$6"</command>
+ </leafNode>
+ <leafNode name="sysctl">
+ <properties>
+ <help>Configure kernel parameters at runtime</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="system sysctl" --source="$6"</command>
+ </leafNode>
+ <leafNode name="time-zone">
+ <properties>
+ <help>Local time zone</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="system time-zone" --source="$6"</command>
+ </leafNode>
+ </children>
+</node>
+<leafNode name="vpn">
+ <properties>
+ <help>Virtual Private Network (VPN)</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="vpn" --source="$6"</command>
+</leafNode>
+<leafNode name="vrf">
+ <properties>
+ <help>Virtual Routing and Forwarding</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --section="vrf" --source="$6"</command>
+</leafNode>
+<!-- included end --> \ No newline at end of file
diff --git a/op-mode-definitions/include/show-config-sync-section.xml.i b/op-mode-definitions/include/show-config-sync-section.xml.i
new file mode 100644
index 000000000..456f9e2f6
--- /dev/null
+++ b/op-mode-definitions/include/show-config-sync-section.xml.i
@@ -0,0 +1,449 @@
+<!-- included start from show-config-sync-section.xml.i -->
+<leafNode name="firewall">
+ <properties>
+ <help>Firewall</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="firewall" --source="$5"</command>
+</leafNode>
+<node name="interfaces">
+ <properties>
+ <help>Interfaces</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="interfaces" --source="$5"</command>
+ <children>
+ <leafNode name="bonding">
+ <properties>
+ <help>Bonding interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="interfaces bonding" --source="$5"</command>
+ </leafNode>
+ <leafNode name="bridge">
+ <properties>
+ <help>Bridge interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="interfaces bridge" --source="$5"</command>
+ </leafNode>
+ <leafNode name="dummy">
+ <properties>
+ <help>Dummy interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="interfaces dummy" --source="$5"</command>
+ </leafNode>
+ <leafNode name="ethernet">
+ <properties>
+ <help>Ethernet interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="interfaces ethernet" --source="$5"</command>
+ </leafNode>
+ <leafNode name="geneve">
+ <properties>
+ <help>GENEVE interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="interfaces geneve" --source="$5"</command>
+ </leafNode>
+ <leafNode name="input">
+ <properties>
+ <help>Input interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="interfaces input" --source="$5"</command>
+ </leafNode>
+ <leafNode name="l2tpv3">
+ <properties>
+ <help>L2TPv3 interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="interfaces l2tpv3" --source="$5"</command>
+ </leafNode>
+ <leafNode name="loopback">
+ <properties>
+ <help>Loopback interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="interfaces loopback" --source="$5"</command>
+ </leafNode>
+ <leafNode name="macsec">
+ <properties>
+ <help>MACsec interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="interfaces macsec" --source="$5"</command>
+ </leafNode>
+ <leafNode name="openvpn">
+ <properties>
+ <help>OpenVPN interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="interfaces openvpn" --source="$5"</command>
+ </leafNode>
+ <leafNode name="pppoe">
+ <properties>
+ <help>PPPoE interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="interfaces pppoe" --source="$5"</command>
+ </leafNode>
+ <leafNode name="pseudo-ethernet">
+ <properties>
+ <help>Pseudo-Ethernet interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="interfaces pseudo-ethernet" --source="$5"</command>
+ </leafNode>
+ <leafNode name="sstpc">
+ <properties>
+ <help>SSTP client interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="interfaces sstpc" --source="$5"</command>
+ </leafNode>
+ <leafNode name="tunnel">
+ <properties>
+ <help>Tunnel interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="interfaces tunnel" --source="$5"</command>
+ </leafNode>
+ <leafNode name="virtual-ethernet">
+ <properties>
+ <help>Virtual Ethernet interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="interfaces virtual-ethernet" --source="$5"</command>
+ </leafNode>
+ <leafNode name="vti">
+ <properties>
+ <help>Virtual tunnel interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="interfaces vti" --source="$5"</command>
+ </leafNode>
+ <leafNode name="vxlan">
+ <properties>
+ <help>VXLAN interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="interfaces vxlan" --source="$5"</command>
+ </leafNode>
+ <leafNode name="wireguard">
+ <properties>
+ <help>Wireguard interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="interfaces wireguard" --source="$5"</command>
+ </leafNode>
+ <leafNode name="wireless">
+ <properties>
+ <help>Wireless interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="interfaces wireless" --source="$5"</command>
+ </leafNode>
+ <leafNode name="wwan">
+ <properties>
+ <help>WWAN interface</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="interfaces wwan" --source="$5"</command>
+ </leafNode>
+ </children>
+</node>
+<leafNode name="nat">
+ <properties>
+ <help>NAT</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="nat" --source="$5"</command>
+</leafNode>
+<leafNode name="nat66">
+ <properties>
+ <help>NAT66</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="nat66" --source="$5"</command>
+</leafNode>
+<leafNode name="pki">
+ <properties>
+ <help>Public key infrastructure (PKI)</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="pki" --source="$5"</command>
+</leafNode>
+<leafNode name="policy">
+ <properties>
+ <help>Routing policy</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="policy" --source="$5"</command>
+</leafNode>
+<node name="protocols">
+ <properties>
+ <help>Routing protocols</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="protocols" --source="$5"</command>
+ <children>
+ <leafNode name="babel">
+ <properties>
+ <help>Babel Routing Protocol</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="protocols babel" --source="$5"</command>
+ </leafNode>
+ <leafNode name="bfd">
+ <properties>
+ <help>Bidirectional Forwarding Detection (BFD)</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="protocols bfd" --source="$5"</command>
+ </leafNode>
+ <leafNode name="bgp">
+ <properties>
+ <help>Border Gateway Protocol (BGP)</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="protocols bgp" --source="$5"</command>
+ </leafNode>
+ <leafNode name="failover">
+ <properties>
+ <help>Failover route</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="protocols failover" --source="$5"</command>
+ </leafNode>
+ <leafNode name="igmp-proxy">
+ <properties>
+ <help>Internet Group Management Protocol (IGMP) proxy</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="protocols igmp-proxy" --source="$5"</command>
+ </leafNode>
+ <leafNode name="isis">
+ <properties>
+ <help>Intermediate System to Intermediate System (IS-IS)</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="protocols isis" --source="$5"</command>
+ </leafNode>
+ <leafNode name="mpls">
+ <properties>
+ <help>Multiprotocol Label Switching (MPLS)</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="protocols mpls" --source="$5"</command>
+ </leafNode>
+ <leafNode name="nhrp">
+ <properties>
+ <help>Next Hop Resolution Protocol (NHRP) parameters</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="protocols nhrp" --source="$5"</command>
+ </leafNode>
+ <leafNode name="ospf">
+ <properties>
+ <help>Open Shortest Path First (OSPF)</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="protocols ospf" --source="$5"</command>
+ </leafNode>
+ <leafNode name="ospfv3">
+ <properties>
+ <help>Open Shortest Path First (OSPF) for IPv6</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="protocols ospfv3" --source="$5"</command>
+ </leafNode>
+ <leafNode name="pim">
+ <properties>
+ <help>Protocol Independent Multicast (PIM) and IGMP</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="protocols pim" --source="$5"</command>
+ </leafNode>
+ <leafNode name="pim6">
+ <properties>
+ <help>Protocol Independent Multicast for IPv6 (PIMv6) and MLD</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="protocols pim6" --source="$5"</command>
+ </leafNode>
+ <leafNode name="rip">
+ <properties>
+ <help>Routing Information Protocol (RIP) parameters</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="protocols rip" --source="$5"</command>
+ </leafNode>
+ <leafNode name="ripng">
+ <properties>
+ <help>Routing Information Protocol (RIPng) parameters</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="protocols ripng" --source="$5"</command>
+ </leafNode>
+ <leafNode name="rpki">
+ <properties>
+ <help>Resource Public Key Infrastructure (RPKI)</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="protocols rpki" --source="$5"</command>
+ </leafNode>
+ <leafNode name="segment-routing">
+ <properties>
+ <help>Segment-Routing (SR) parameters</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="protocols segment-routing" --source="$5"</command>
+ </leafNode>
+ <leafNode name="static">
+ <properties>
+ <help>Static Routing</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="protocols static" --source="$5"</command>
+ </leafNode>
+ </children>
+</node>
+<node name="qos">
+ <properties>
+ <help>Quality of Service (QoS)</help>
+ </properties>
+ <children>
+ <leafNode name="interface">
+ <properties>
+ <help>Interface to apply QoS policy</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="qos interface" --source="$5"</command>
+ </leafNode>
+ <leafNode name="policy">
+ <properties>
+ <help>Service Policy definitions</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="qos policy" --source="$5"</command>
+ </leafNode>
+ </children>
+</node>
+<node name="service">
+ <properties>
+ <help>System services</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="service" --source="$5"</command>
+ <children>
+ <leafNode name="console-server">
+ <properties>
+ <help>Serial Console Server</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="service console-server" --source="$5"</command>
+ </leafNode>
+ <leafNode name="dhcp-relay">
+ <properties>
+ <help>Host Configuration Protocol (DHCP) relay agent</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="service dhcp-relay" --source="$5"</command>
+ </leafNode>
+ <leafNode name="dhcp-server">
+ <properties>
+ <help>Dynamic Host Configuration Protocol (DHCP) for DHCP server</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="service dhcp-server" --source="$5"</command>
+ </leafNode>
+ <leafNode name="dhcpv6-relay">
+ <properties>
+ <help>DHCPv6 Relay Agent parameters</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="service dhcpv6-relay" --source="$5"</command>
+ </leafNode>
+ <leafNode name="dhcpv6-server">
+ <properties>
+ <help>DHCP for IPv6 (DHCPv6) server</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="service dhcpv6-server" --source="$5"</command>
+ </leafNode>
+ <leafNode name="dns">
+ <properties>
+ <help>Domain Name System (DNS) related services</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="service dns" --source="$5"</command>
+ </leafNode>
+ <leafNode name="lldp">
+ <properties>
+ <help>LLDP settings</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="service lldp" --source="$5"</command>
+ </leafNode>
+ <leafNode name="mdns">
+ <properties>
+ <help>Multicast DNS (mDNS) parameters</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="service mdns" --source="$5"</command>
+ </leafNode>
+ <leafNode name="monitoring">
+ <properties>
+ <help>Monitoring services</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="service monitoring" --source="$5"</command>
+ </leafNode>
+ <leafNode name="ndp-proxy">
+ <properties>
+ <help>Neighbor Discovery Protocol (NDP) Proxy</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="service ndp-proxy" --source="$5"</command>
+ </leafNode>
+ <leafNode name="ntp">
+ <properties>
+ <help>Network Time Protocol (NTP) configuration</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="service ntp" --source="$5"</command>
+ </leafNode>
+ <leafNode name="snmp">
+ <properties>
+ <help>Simple Network Management Protocol (SNMP)</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="service snmp" --source="$5"</command>
+ </leafNode>
+ <leafNode name="tftp-server">
+ <properties>
+ <help>Trivial File Transfer Protocol (TFTP) server</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="service tftp-server" --source="$5"</command>
+ </leafNode>
+ <leafNode name="webproxy">
+ <properties>
+ <help>Webproxy service settings</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="service webproxy" --source="$5"</command>
+ </leafNode>
+ </children>
+</node>
+<node name="system">
+ <properties>
+ <help>System parameters</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="system" --source="$5"</command>
+ <children>
+ <leafNode name="conntrack">
+ <properties>
+ <help>Connection Tracking</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="system conntrack" --source="$5"</command>
+ </leafNode>
+ <leafNode name="flow-accounting">
+ <properties>
+ <help>Flow accounting</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="system flow-accounting" --source="$5"</command>
+ </leafNode>
+ <leafNode name="login">
+ <properties>
+ <help>System User Login</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="system login" --source="$5"</command>
+ </leafNode>
+ <leafNode name="option">
+ <properties>
+ <help>System Options</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="system option" --source="$5"</command>
+ </leafNode>
+ <leafNode name="sflow">
+ <properties>
+ <help>sFlow</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="system sflow" --source="$5"</command>
+ </leafNode>
+ <leafNode name="static-host-mapping">
+ <properties>
+ <help>Map host names to addresses</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="system static-host-mapping" --source="$5"</command>
+ </leafNode>
+ <leafNode name="sysctl">
+ <properties>
+ <help>Configure kernel parameters at runtime</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="system sysctl" --source="$5"</command>
+ </leafNode>
+ <leafNode name="time-zone">
+ <properties>
+ <help>Local time zone</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="system time-zone" --source="$5"</command>
+ </leafNode>
+ </children>
+</node>
+<leafNode name="vpn">
+ <properties>
+ <help>Virtual Private Network (VPN)</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="vpn" --source="$5"</command>
+</leafNode>
+<leafNode name="vrf">
+ <properties>
+ <help>Virtual Routing and Forwarding</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --section="vrf" --source="$5"</command>
+</leafNode>
+<!-- included end --> \ No newline at end of file
diff --git a/op-mode-definitions/show-config-sync.xml.in b/op-mode-definitions/show-config-sync.xml.in
new file mode 100644
index 000000000..7c39fa64e
--- /dev/null
+++ b/op-mode-definitions/show-config-sync.xml.in
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="configuration">
+ <children>
+ <node name="secondary">
+ <properties>
+ <help>Show configuration synchronization information (config-sync service)</help>
+ </properties>
+ <children>
+ <node name="sync">
+ <properties>
+ <help>Show differences between local configuration and remote VyOS instance</help>
+ </properties>
+ <!-- No source, no section, no commands -->
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff</command>
+ <children>
+ <!-- Optional source -->
+ <node name="running">
+ <properties>
+ <help>Compare with local running configuration (default)</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --source="running"</command>
+ <!-- 'running' source with section -->
+ <children>
+ #include <include/show-config-sync-section.xml.i>
+ </children>
+ </node>
+ <node name="candidate">
+ <properties>
+ <help>Compare with local candidate configuration</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --source="candidate"</command>
+ <!-- 'candidate' source with section -->
+ <children>
+ #include <include/show-config-sync-section.xml.i>
+ </children>
+ </node>
+ <node name="saved">
+ <properties>
+ <help>Compare with local saved configuration</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --source="saved"</command>
+ <!-- 'saved' source with section -->
+ <children>
+ #include <include/show-config-sync-section.xml.i>
+ </children>
+ </node>
+ <!-- The same thing, but using the command form -->
+ <node name="commands">
+ <properties>
+ <help>Show the difference as commands</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands</command>
+ <children>
+ <!-- Optional source -->
+ <node name="running">
+ <properties>
+ <help>Compare with local running configuration (default)</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --source="running"</command>
+ <!-- 'running' source with section -->
+ <children>
+ #include <include/show-config-sync-section-commands.xml.i>
+ </children>
+ </node>
+ <node name="candidate">
+ <properties>
+ <help>Compare with local candidate configuration</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --source="candidate"</command>
+ <!-- 'candidate' source with section -->
+ <children>
+ #include <include/show-config-sync-section-commands.xml.i>
+ </children>
+ </node>
+ <node name="saved">
+ <properties>
+ <help>Compare with local saved configuration</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/config_sync.py show_sync_diff --commands --source="saved"</command>
+ <!-- 'saved' source with section -->
+ <children>
+ #include <include/show-config-sync-section-commands.xml.i>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/python/vyos/config_mgmt.py b/python/vyos/config_mgmt.py
index e1e38b26f..f7d8883c4 100644
--- a/python/vyos/config_mgmt.py
+++ b/python/vyos/config_mgmt.py
@@ -38,6 +38,7 @@ from vyos.configtree import ConfigTreeError
from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
from vyos.configtree import diff_compare
+from vyos.configtree import DiffTree
from vyos.load_config import load
from vyos.load_config import LoadConfigError
from vyos.defaults import directories
@@ -446,6 +447,80 @@ Proceed ?"""
return self.compare(commands=cmnds, rev1=r1, rev2=r2)
+ def _format_remote_diff(self, diff_tree: DiffTree, path: list, commands: bool):
+ add_tree = diff_tree.add
+ del_tree = diff_tree.delete
+ command_prefix = ' '.join(path)
+
+ result_lines = []
+ if commands:
+ # Process the deleted elements into command format and filter based on prefix (path)
+ for line in del_tree.to_commands(op='delete').splitlines():
+ if line.startswith(f'delete {command_prefix}'):
+ result_lines.append(line)
+
+ # Process the added elements into command format and filter based on prefix (path)
+ for line in add_tree.to_commands(op='set').splitlines():
+ if line.startswith(f'set {command_prefix}'):
+ result_lines.append(line)
+ else:
+ with_node = len(path) > 1
+ # Retrieve subtrees for the specified path from both added and deleted trees
+ del_tree = del_tree.get_subtree(path, with_node=with_node)
+ add_tree = add_tree.get_subtree(path, with_node=with_node)
+
+ # Convert the subtrees to string lines for further processing
+ del_tree_lines = str(del_tree).splitlines()
+ add_tree_lines = str(add_tree).splitlines()
+
+ # Format the lines with a prefix ('-', '+') and filter out empty lines
+ del_lines = [f'- {l}' for l in del_tree_lines if l.strip()]
+ add_lines = [f'+ {l}' for l in add_tree_lines if l.strip()]
+
+ if del_lines or add_lines:
+ # Adjust command prefix if a node is present in the path
+ command_prefix = ' '.join(path[:-1]) if with_node else command_prefix
+ if command_prefix:
+ result_lines.append(f'[{command_prefix}]')
+
+ # Combine both deleted and added lines and process them
+ result_lines.extend(del_lines + add_lines)
+
+ # Join the result lines into a single string, excluding empty lines
+ return '\n'.join((line for line in result_lines if line.strip()))
+
+ def remote_compare(
+ self,
+ source: str,
+ remote_tree: ConfigTree,
+ path: Optional[list] = None,
+ commands: bool = False,
+ ) -> str:
+ """
+ Compares a local configuration tree with a remote
+ configuration tree based on the specified source ('running', 'candidate', 'saved').
+ """
+ path = path or []
+
+ # Determine the correct local configuration tree based on the 'source' parameter
+ if source == 'running':
+ local_tree = self.active_config
+ elif source == 'candidate':
+ local_tree = self.working_config
+ elif source == 'saved':
+ local_tree = self._get_saved_config_tree()
+ else:
+ raise ConfigMgmtError(
+ 'Invalid source, must be one of: running, candidate, saved'
+ )
+
+ try:
+ diff_tree = DiffTree(remote_tree, local_tree)
+ except ConfigTreeError as e:
+ raise ConfigMgmtError(e) from e
+
+ return self._format_remote_diff(diff_tree, path, commands)
+
# Initialization and post-commit hooks for conf-mode
#
def initialize_revision(self):
diff --git a/python/vyos/http_api_client.py b/python/vyos/http_api_client.py
new file mode 100644
index 000000000..ae16cfa54
--- /dev/null
+++ b/python/vyos/http_api_client.py
@@ -0,0 +1,148 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import json
+import urllib3
+import requests
+from typing import Optional
+from dataclasses import dataclass
+
+from vyos.version import get_version
+from vyos.template import bracketize_ipv6
+
+
+class ApiError(Exception):
+ """Generic VyOS HTTP API client error"""
+
+
+class ApiAuthError(ApiError):
+ """Authentication/authorization error"""
+
+
+class ApiTransportError(ApiError):
+ """Network/transport error (timeouts, connection errors, etc.)"""
+
+
+class ApiResponseError(ApiError):
+ """Server responded, but payload is invalid or indicates an error"""
+
+ def __init__(self, message, response=None):
+ super().__init__(message)
+ self.response = response
+
+
+@dataclass(frozen=True)
+class ApiClientConfig:
+ host: str
+ key: str
+ port: int = 443
+ timeout: Optional[int] = None
+ verify_tls: bool = False
+
+
+class ApiClient:
+ """Small helper for talking to VyOS HTTP API using requests.
+
+ Design goals:
+ - minimal surface area (thin wrapper around requests)
+ - consistent error handling + typed exceptions
+ - safe defaults for VyOS typical self-signed HTTPS usage (verify_tls=False)
+ """
+
+ _DEFAULT_HEADERS = {
+ 'Content-Type': 'application/json',
+ 'User-Agent': f'VyOS/{get_version()}',
+ }
+
+ def __init__(self, config: ApiClientConfig):
+ assert isinstance(config, ApiClientConfig)
+
+ self._cfg = config
+ self._host = bracketize_ipv6(config.host)
+
+ self._session = requests.Session()
+ self._session.headers.update(self._DEFAULT_HEADERS)
+
+ if not config.verify_tls:
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+
+ @property
+ def base_url(self) -> str:
+ return f'https://{self._host}:{self._cfg.port}'
+
+ def post(
+ self,
+ endpoint: str,
+ payload: dict,
+ *,
+ params: Optional[dict] = None,
+ raise_on_error: bool = True,
+ ) -> dict:
+ """POST JSON to API and return decoded JSON response.
+
+ Args:
+ endpoint: URL path, e.g. '/configure-section' or 'configure'
+ payload: dict that will be JSON-encoded and sent as request body
+ params: Optional query parameters
+ raise_on_error: If True, raise when response envelope contains bad status code
+
+ Raises:
+ ApiTransportError: network problems/timeouts
+ ApiAuthError: 401/403 responses
+ ApiResponseError: non-2xx responses or invalid JSON
+ """
+ if not endpoint.startswith('/'):
+ endpoint = f'/{endpoint}'
+
+ # Most VyOS endpoints in this repo expect 'key' inside body.
+ body = dict(payload)
+ body.setdefault('key', self._cfg.key)
+
+ url = f'{self.base_url}{endpoint}'
+
+ try:
+ resp = self._session.post(
+ url,
+ data=json.dumps(body),
+ params=params,
+ timeout=self._cfg.timeout,
+ verify=self._cfg.verify_tls,
+ )
+ except requests.exceptions.Timeout as e:
+ raise ApiTransportError(f'Request timed out: {e}') from e
+ except requests.exceptions.RequestException as e:
+ raise ApiTransportError(f'Request failed: {e}') from e
+
+ if not resp.ok:
+ text = resp.text.strip()
+ err_msg = f'HTTP {resp.status_code} from {endpoint}: {text}'
+
+ if resp.status_code in (401, 403):
+ raise ApiAuthError(err_msg)
+ elif resp.status_code >= 500:
+ raise ApiResponseError(err_msg, response=resp)
+
+ if raise_on_error:
+ raise ApiResponseError(err_msg, response=resp)
+
+ try:
+ return resp.json()
+ except json.JSONDecodeError as e:
+ raise ApiResponseError(
+ f'Invalid JSON response from {endpoint}: {e}',
+ response=resp,
+ ) from e
diff --git a/python/vyos/utils/list.py b/python/vyos/utils/list.py
index ea4bcd3ba..01a399026 100644
--- a/python/vyos/utils/list.py
+++ b/python/vyos/utils/list.py
@@ -41,3 +41,23 @@ def list_strip(lst: list, sub: list, right: bool = False) -> list:
sub = []
return lst
+
+
+def list_contains_sublist(lst: list, sub: list) -> bool:
+ """
+ Check if any sublist in lst contains any element from list sub.
+
+ Parameters:
+ lst (list of list): A list of sublists to be searched.
+ sub (list): A list of elements to search for.
+
+ Returns:
+ bool: True if any element from sub is found in any sublist of lst, otherwise False.
+ """
+
+ for sublist in lst:
+ if len(sub) == len(sublist):
+ # Ensure all elements the same and position in right position
+ if all(a == b for a, b in zip(sub, sublist, strict=True)):
+ return True
+ return False
diff --git a/smoketest/scripts/cli/test_service_config-sync.py b/smoketest/scripts/cli/test_service_config-sync.py
new file mode 100644
index 000000000..8e22800c0
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_config-sync.py
@@ -0,0 +1,174 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# 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 time
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+HTTPS_PATH = ['service', 'https']
+SYNC_PATH = ['service', 'config-sync']
+
+ADDRESS = '127.0.0.1'
+KEY = 'id_key'
+
+
+class TestConfigSyncWithHTTPS(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.cli_delete(cls, HTTPS_PATH)
+ cls.cli_delete(cls, SYNC_PATH)
+
+ def tearDown(self):
+ self.cli_delete(HTTPS_PATH)
+ self.cli_delete(SYNC_PATH)
+
+ self.cli_delete(['interfaces', 'dummy'])
+ self.cli_delete(['system', 'time-zone'])
+
+ self.cli_commit()
+ super().tearDown()
+
+ def _configure_r1_config_sync(self):
+ """
+ Simulates R1: config-sync client
+ """
+ self.cli_set(SYNC_PATH + ['mode', 'load'])
+ self.cli_set(SYNC_PATH + ['secondary', 'address', ADDRESS])
+ self.cli_set(SYNC_PATH + ['secondary', 'key', KEY])
+ self.cli_set(SYNC_PATH + ['secondary', 'port', '443'])
+
+ self.cli_set(SYNC_PATH + ['section', 'interfaces', 'dummy'])
+ self.cli_set(SYNC_PATH + ['section', 'system', 'time-zone'])
+
+ self.cli_commit()
+
+ # wait to init config-sync service
+ time.sleep(1)
+
+ def _configure_r2_https_api(self):
+ """
+ Simulates R2: HTTPS API endpoint
+ """
+ self.cli_set(HTTPS_PATH + ['api', 'rest'])
+ self.cli_set(HTTPS_PATH + ['api', 'keys', 'id', 'KEY', 'key', KEY])
+ self.cli_set(HTTPS_PATH + ['listen-address', '0.0.0.0'])
+ self.cli_commit()
+
+ def test_basic(self):
+ """
+ Validate: basic config-sync configuration (R1 side)
+ """
+
+ self._configure_r2_https_api()
+ self._configure_r1_config_sync()
+
+ config = self.op_mode(['show', 'configuration', 'commands'])
+
+ self.assertIn("set service config-sync mode 'load'", config)
+ self.assertIn(f"set service config-sync secondary address '{ADDRESS}'", config)
+
+ def test_show_diff_candidate_interfaces(self):
+ """
+ Validate: show configuration secondary sync commands candidate interfaces dummy
+ """
+
+ self._configure_r2_https_api()
+ self._configure_r1_config_sync()
+
+ # committed config
+ self.cli_set(['interfaces', 'dummy', 'dum0', 'address', '192.0.2.1/32'])
+ self.cli_commit()
+
+ # candidate change
+ self.cli_set(['interfaces', 'dummy', 'dum0', 'address', '192.0.2.2/32'])
+
+ output = self.op_mode(
+ [
+ 'show',
+ 'configuration',
+ 'secondary',
+ 'sync',
+ 'commands',
+ 'candidate',
+ 'interfaces',
+ 'dummy',
+ ]
+ )
+
+ self.assertIsInstance(output, str)
+ self.assertIn("set interfaces dummy dum0 address '192.0.2.2/32'", output)
+
+ def test_show_diff_saved_system(self):
+ """
+ Validate: show configuration secondary sync saved system time-zone
+ """
+
+ self._configure_r2_https_api()
+ self._configure_r1_config_sync()
+
+ # committed config
+ self.cli_set(['system', 'time-zone', 'UTC'])
+ self.cli_commit()
+
+ output = self.op_mode(
+ [
+ 'show',
+ 'configuration',
+ 'secondary',
+ 'sync',
+ 'saved',
+ 'system',
+ 'time-zone',
+ ]
+ )
+
+ self.assertIsInstance(output, str)
+ self.assertIn('[system]\n- time-zone', output)
+
+ output = self.op_mode(['show', 'configuration', 'secondary', 'sync', 'saved'])
+
+ self.assertIsInstance(output, str)
+ self.assertIn('[system]\n- time-zone', output)
+
+ def test_show_diff_empty(self):
+ """
+ No candidate changes -> empty diff
+ """
+
+ self._configure_r2_https_api()
+ self._configure_r1_config_sync()
+
+ output = self.op_mode(['show', 'configuration', 'secondary', 'sync'])
+ self.assertTrue(output.strip() == '' or 'No changes' in output, repr(output))
+
+ output = self.op_mode(
+ [
+ 'show',
+ 'configuration',
+ 'secondary',
+ 'sync',
+ 'running',
+ 'interfaces',
+ 'dummy',
+ ]
+ )
+ self.assertTrue(output.strip() == '' or 'No changes' in output)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=5)
diff --git a/src/op_mode/config_sync.py b/src/op_mode/config_sync.py
new file mode 100644
index 000000000..d6eff7cd2
--- /dev/null
+++ b/src/op_mode/config_sync.py
@@ -0,0 +1,193 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# 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 sys
+import typing
+from pathlib import Path
+
+from vyos import opmode
+from vyos import http_api_client as http
+from vyos.utils.file import read_json
+from vyos.utils.dict import dict_to_paths
+from vyos.utils.list import list_contains_sublist
+from vyos.configtree import ConfigTree
+from vyos.configtree import ConfigTreeError
+from vyos.config_mgmt import ConfigMgmt
+from vyos.config_mgmt import ConfigMgmtError
+
+CONFIG_FILE = Path('/run/config_sync_conf.conf')
+
+
+def _normalize_section(section: typing.Optional[str]) -> list:
+ """Convert optional CLI section argument to config tree path list"""
+
+ if not section:
+ return []
+
+ # Section can be passed as a single string token ('interfaces ethernet')
+ return list(section.split())
+
+
+def _read_json_config() -> dict:
+ """Read config-sync service runtime JSON file"""
+
+ if not CONFIG_FILE.exists():
+ raise opmode.UnconfiguredObject('Config-sync service is not configured')
+
+ return read_json(CONFIG_FILE, defaultonfailure={})
+
+
+def _load_config_sync_sections() -> list:
+ """Load sections from config-sync service runtime JSON file"""
+
+ cfg = _read_json_config()
+ sections = cfg.get('section', {})
+
+ return list(dict_to_paths(sections)) if sections else []
+
+
+def _load_config_sync_settings() -> dict:
+ """Load remote API settings from config-sync service runtime JSON file"""
+
+ cfg = _read_json_config()
+ secondary = cfg.get('secondary', {})
+ address = secondary.get('address')
+ key = secondary.get('key')
+ port = int(secondary.get('port', 443))
+ timeout = int(secondary.get('timeout')) if secondary.get('timeout') else None
+
+ if not address or not key:
+ raise opmode.UnconfiguredObject(
+ 'Config-sync is not fully configured: missing secondary address/key'
+ )
+
+ return dict(host=address, key=key, port=port, timeout=timeout)
+
+
+class ConfigSyncDiffManager:
+ def __init__(self):
+ api_settings = _load_config_sync_settings()
+ self._client = http.ApiClient(http.ApiClientConfig(**api_settings))
+
+ self._config_mgmt = ConfigMgmt()
+
+ def _get_remote_config_tree(self, section_path: list = None) -> ConfigTree:
+ """
+ Retrieve remote config (or subtree) as ConfigTree via HTTPS API.
+
+ Note: Endpoint name is expected to be available on remote VyOS instance.
+ """
+
+ payload = {
+ 'configFormat': 'raw',
+ 'op': 'showConfig',
+ 'path': section_path or [],
+ }
+
+ try:
+ resp_data = self._client.post('retrieve', payload, raise_on_error=False)
+ except http.ApiError as e:
+ raise opmode.InternalError(f'Remote API failed: {e}') from e
+
+ error = (resp_data.get('error') or resp_data.get('detail') or '').strip()
+ if error:
+ ignored_errors = ('configuration under specified path is empty',)
+ if error.lower() not in ignored_errors:
+ raise opmode.InternalError(
+ f'Remote API responded with an error: {error}'
+ )
+
+ config_raw = resp_data.get('data') or ''
+ try:
+ return ConfigTree(config_raw)
+ except ConfigTreeError as e:
+ raise opmode.InternalError(f'Unable to build remote ConfigTree: {e}') from e
+
+ def get_sync_diff(
+ self,
+ source: str,
+ sections: list,
+ commands: typing.Optional[bool] = False,
+ ) -> str:
+ """Returns differences between local config and remote config for a given sections"""
+
+ results = []
+ remote_tree = self._get_remote_config_tree()
+ for section_path in sections:
+ try:
+ result = self._config_mgmt.remote_compare(
+ source,
+ remote_tree,
+ path=section_path,
+ commands=commands,
+ )
+ except ConfigMgmtError as e:
+ raise opmode.InternalError(str(e)) from e
+
+ result = result.strip()
+ if result:
+ results.append(result)
+
+ return '\n'.join(results)
+
+
+def show_sync_diff(
+ raw: bool,
+ source: typing.Optional[str],
+ section: typing.Optional[str],
+ commands: typing.Optional[bool],
+) -> str:
+ """Show differences between local config and remote config for a given section.
+
+ Args:
+ raw: unused (op-mode convention); output is always text.
+ source: local source config: running/candidate/saved.
+ section: optional top-level section to diff (e.g. "nat", "system time-zone").
+ commands: flag which indicates format of output.
+
+ Returns:
+ Diff output (string). Empty diff is rendered as "no changes".
+ """
+ _ = raw # op-mode framework passes it; keep signature consistent
+
+ source = source or 'running'
+ selected_section = _normalize_section(section)
+
+ configured_sections = _load_config_sync_sections()
+ if selected_section:
+ if not list_contains_sublist(configured_sections, selected_section):
+ raise opmode.UnconfiguredObject(
+ f"Config-sync is not configured for '{section}' section. "
+ f"Use 'set service config-sync section {section}' for this."
+ )
+ sections = [selected_section]
+ else:
+ sections = configured_sections
+
+ manager = ConfigSyncDiffManager()
+ output = manager.get_sync_diff(source, sections, commands=commands)
+
+ return output if output else 'No changes between local and remote configuration'
+
+
+if __name__ == '__main__':
+ try:
+ res = opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, opmode.Error) as e:
+ print(e)
+ sys.exit(1)