diff options
| author | Oleksandr Kuchmystyi <o.kuchmystyi@vyos.io> | 2026-03-24 16:26:33 +0300 |
|---|---|---|
| committer | Oleksandr Kuchmystyi <o.kuchmystyi@vyos.io> | 2026-04-01 11:30:15 +0300 |
| commit | 54fc0e1199625c8b6223e5d034bf895cfbf57c77 (patch) | |
| tree | 0dc3ba44371d966fde332bba7d60357d38c71cfc | |
| parent | 3823523c01e07ca010d035db7311b3c7f0e4e511 (diff) | |
| download | vyos-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.json | 1 | ||||
| -rw-r--r-- | op-mode-definitions/include/show-config-sync-section-commands.xml.i | 449 | ||||
| -rw-r--r-- | op-mode-definitions/include/show-config-sync-section.xml.i | 449 | ||||
| -rw-r--r-- | op-mode-definitions/show-config-sync.xml.in | 98 | ||||
| -rw-r--r-- | python/vyos/config_mgmt.py | 75 | ||||
| -rw-r--r-- | python/vyos/http_api_client.py | 148 | ||||
| -rw-r--r-- | python/vyos/utils/list.py | 20 | ||||
| -rw-r--r-- | smoketest/scripts/cli/test_service_config-sync.py | 174 | ||||
| -rw-r--r-- | src/op_mode/config_sync.py | 193 |
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) |
