diff --git a/Makefile b/Makefile index 432de7547..44a6e35ed 100644 --- a/Makefile +++ b/Makefile @@ -1,125 +1,125 @@ TMPL_DIR := templates-cfg OP_TMPL_DIR := templates-op BUILD_DIR := build DATA_DIR := data SHIM_DIR := src/shim LIBS := -lzmq CFLAGS := BUILD_ARCH := $(shell dpkg-architecture -q DEB_BUILD_ARCH) J2LINT := $(shell command -v j2lint 2> /dev/null) -PYLINT_FILES := $(shell git ls-files *.py) +PYLINT_FILES := $(shell git ls-files *.py src/migration-scripts) config_xml_src = $(wildcard interface-definitions/*.xml.in) config_xml_obj = $(config_xml_src:.xml.in=.xml) op_xml_src = $(wildcard op-mode-definitions/*.xml.in) op_xml_obj = $(op_xml_src:.xml.in=.xml) %.xml: %.xml.in @echo Generating $(BUILD_DIR)/$@ from $< mkdir -p $(BUILD_DIR)/$(dir $@) $(CURDIR)/scripts/transclude-template $< > $(BUILD_DIR)/$@ .PHONY: interface_definitions .ONESHELL: interface_definitions: $(config_xml_obj) mkdir -p $(TMPL_DIR) $(CURDIR)/scripts/override-default $(BUILD_DIR)/interface-definitions find $(BUILD_DIR)/interface-definitions -type f -name "*.xml" | xargs -I {} $(CURDIR)/scripts/build-command-templates {} $(CURDIR)/schema/interface_definition.rng $(TMPL_DIR) || exit 1 $(CURDIR)/python/vyos/xml_ref/generate_cache.py --xml-dir $(BUILD_DIR)/interface-definitions || exit 1 # XXX: delete top level node.def's that now live in other packages # IPSec VPN EAP-RADIUS does not support source-address rm -rf $(TMPL_DIR)/vpn/ipsec/remote-access/radius/source-address # T2472 - EIGRP support rm -rf $(TMPL_DIR)/protocols/eigrp # T2773 - EIGRP support for VRF rm -rf $(TMPL_DIR)/vrf/name/node.tag/protocols/eigrp # XXX: test if there are empty node.def files - this is not allowed as these # could mask help strings or mandatory priority statements find $(TMPL_DIR) -name node.def -type f -empty -exec false {} + || sh -c 'echo "There are empty node.def files! Check your interface definitions." && exit 1' ifeq ($(BUILD_ARCH),arm64) # There is currently no telegraf support in VyOS for ARM64, remove CLI definitions rm -rf $(TMPL_DIR)/service/monitoring/telegraf endif .PHONY: op_mode_definitions .ONESHELL: op_mode_definitions: $(op_xml_obj) mkdir -p $(OP_TMPL_DIR) find $(BUILD_DIR)/op-mode-definitions/ -type f -name "*.xml" | xargs -I {} $(CURDIR)/scripts/build-command-op-templates {} $(CURDIR)/schema/op-mode-definition.rng $(OP_TMPL_DIR) || exit 1 # XXX: delete top level op mode node.def's that now live in other packages rm -f $(OP_TMPL_DIR)/add/node.def rm -f $(OP_TMPL_DIR)/clear/interfaces/node.def rm -f $(OP_TMPL_DIR)/clear/node.def rm -f $(OP_TMPL_DIR)/delete/node.def # XXX: ping, traceroute and mtr must be able to recursivly call themselves as the # options are provided from the scripts themselves ln -s ../node.tag $(OP_TMPL_DIR)/ping/node.tag/node.tag/ ln -s ../node.tag $(OP_TMPL_DIR)/traceroute/node.tag/node.tag/ ln -s ../node.tag $(OP_TMPL_DIR)/mtr/node.tag/node.tag/ # XXX: test if there are empty node.def files - this is not allowed as these # could mask help strings or mandatory priority statements find $(OP_TMPL_DIR) -name node.def -type f -empty -exec false {} + || sh -c 'echo "There are empty node.def files! Check your interface definitions." && exit 1' .PHONY: vyshim vyshim: $(MAKE) -C $(SHIM_DIR) .PHONY: all all: clean interface_definitions op_mode_definitions check test j2lint vyshim .PHONY: check .ONESHELL: check: @echo "Checking which CLI scripts are not enabled to work with vyos-configd..." @for file in `ls src/conf_mode -I__pycache__` do if ! grep -q $$file data/configd-include.json; then echo "* $$file" fi done .PHONY: clean clean: rm -rf $(BUILD_DIR) rm -rf $(TMPL_DIR) rm -rf $(OP_TMPL_DIR) $(MAKE) -C $(SHIM_DIR) clean .PHONY: test test: set -e; python3 -m compileall -q -x '/vmware-tools/scripts/, /ppp/' . PYTHONPATH=python/ python3 -m "nose" --with-xunit src --with-coverage --cover-erase --cover-xml --cover-package src/conf_mode,src/op_mode,src/completion,src/helpers,src/validators,src/tests --verbose .PHONY: j2lint j2lint: ifndef J2LINT $(error "j2lint binary not found, consider installing: pip install git+https://github.com/aristanetworks/j2lint.git@341b5d5db86") endif $(J2LINT) data/ .PHONY: sonar sonar: sonar-scanner -X -Dsonar.login=${SONAR_TOKEN} .PHONY: unused-imports unused-imports: @pylint --disable=all --enable=W0611 $(PYLINT_FILES) deb: dpkg-buildpackage -uc -us -tc -b .PHONY: schema schema: trang -I rnc -O rng schema/interface_definition.rnc schema/interface_definition.rng trang -I rnc -O rng schema/op-mode-definition.rnc schema/op-mode-definition.rng diff --git a/debian/control b/debian/control index 2f32e7f35..65e580f03 100644 --- a/debian/control +++ b/debian/control @@ -1,211 +1,212 @@ Source: vyos-1x Section: contrib/net Priority: extra Maintainer: VyOS Package Maintainers <maintainers@vyos.net> Build-Depends: debhelper (>= 9), dh-python, fakeroot, gcc, iproute2, libvyosconfig0 (>= 0.0.7), libzmq3-dev, python3 (>= 3.10), + pylint, python3-coverage, python3-lxml, python3-netifaces, python3-nose, python3-jinja2, python3-psutil, python3-setuptools, python3-xmltodict, quilt, whois Standards-Version: 3.9.6 Package: vyos-1x Architecture: amd64 arm64 Pre-Depends: libnss-tacplus [amd64], libpam-tacplus [amd64], libpam-radius-auth [amd64] Depends: ${python3:Depends} (>= 3.10), aardvark-dns, accel-ppp, auditd, avahi-daemon, beep, bmon, bsdmainutils, certbot, charon-systemd, conntrack, conntrackd, conserver-client, conserver-server, console-data, cron, curl, dbus, ddclient (>= 3.11.1), dmidecode, dropbear, easy-rsa, etherwake, ethtool, fdisk, fastnetmon [amd64], file, frr (>= 7.5), frr-pythontools, frr-rpki-rtrlib, frr-snmp, fuse-overlayfs, libpam-google-authenticator, git, grc, haproxy, hostapd, hsflowd, hvinfo, igmpproxy, ipaddrcheck, iperf, iperf3, iproute2 (>= 6.0.0), iptables, iputils-arping, isc-dhcp-client, isc-dhcp-relay, isc-dhcp-server, iw, jool, keepalived (>=2.0.5), lcdproc, lcdproc-extra-drivers, libatomic1, libauparse0, libcharon-extra-plugins (>=5.9), libcharon-extauth-plugins (>=5.9), libndp-tools, libnetfilter-conntrack3, libnfnetlink0, libqmi-utils, libstrongswan-extra-plugins (>=5.9), libstrongswan-standard-plugins (>=5.9), libvyosconfig0, linux-cpupower, lldpd, lm-sensors, lsscsi, minisign, modemmanager, mtr-tiny, ndisc6, ndppd, netavark, netplug, nfct, nftables (>= 0.9.3), nginx-light, chrony, nvme-cli, ocserv, opennhrp, openssh-server, openssl, openvpn, openvpn-auth-ldap, openvpn-auth-radius, openvpn-otp, owamp-client, owamp-server, pciutils, pdns-recursor, pmacct (>= 1.6.0), podman, pppoe, procps, python3, python3-cryptography, python3-hurry.filesize, python3-inotify, python3-isc-dhcp-leases, python3-jinja2, python3-jmespath, python3-netaddr, python3-netifaces, python3-paramiko, python3-passlib, python3-psutil, python3-pyhumps, python3-pystache, python3-pyudev, python3-six, python3-tabulate, python3-vici (>= 5.7.2), python3-voluptuous, python3-xmltodict, python3-zmq, qrencode, radvd, salt-minion, sed, smartmontools, snmp, snmpd, squashfs-tools, squid, squidclient, squidguard, sshguard, ssl-cert, sstp-client, strongswan (>= 5.9), strongswan-swanctl (>= 5.9), stunnel4, sudo, systemd, telegraf (>= 1.20), tcpdump, tcptraceroute, telnet, tftpd-hpa, traceroute, tuned, twamp-client, twamp-server, udp-broadcast-relay, uidmap, usb-modeswitch, usbutils, vyatta-bash, vyatta-cfg, vyos-http-api-tools, vyos-utils, wide-dhcpv6-client, wireguard-tools, wireless-regdb, wpasupplicant (>= 0.6.7), zabbix-agent2, ndppd, miniupnpd-nftables Description: VyOS configuration scripts and data VyOS configuration scripts, interface definitions, and everything Package: vyos-1x-vmware Architecture: amd64 Depends: vyos-1x, open-vm-tools Description: VyOS configuration scripts and data for VMware Adds configuration files required for VyOS running on VMware hosts. Package: vyos-1x-smoketest Architecture: all Depends: skopeo, snmp, vyos-1x Description: VyOS build sanity checking toolkit diff --git a/smoketest/scripts/cli/test_netns.py b/smoketest/scripts/cli/test_netns.py index 55ad7c83b..d11a5d8f1 100755 --- a/smoketest/scripts/cli/test_netns.py +++ b/smoketest/scripts/cli/test_netns.py @@ -1,75 +1,74 @@ #!/usr/bin/env python3 # # Copyright (C) 2021-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import unittest -from netifaces import interfaces from base_vyostest_shim import VyOSUnitTestSHIM from vyos.utils.process import cmd base_path = ['netns'] namespaces = ['mgmt', 'front', 'back', 'ams-ix'] class NETNSTest(VyOSUnitTestSHIM.TestCase): def setUp(self): self._interfaces = ['dum10', 'dum12', 'dum50'] def test_create_netns(self): for netns in namespaces: base = base_path + ['name', netns] self.cli_set(base) # commit changes self.cli_commit() netns_list = cmd('ip netns ls') # Verify NETNS configuration for netns in namespaces: self.assertTrue(netns in netns_list) def test_netns_assign_interface(self): netns = 'foo' self.cli_set(['netns', 'name', netns]) # Set for iface in self._interfaces: self.cli_set(['interfaces', 'dummy', iface, 'netns', netns]) # commit changes self.cli_commit() netns_iface_list = cmd(f'sudo ip netns exec {netns} ip link show') for iface in self._interfaces: self.assertTrue(iface in netns_iface_list) # Delete for iface in self._interfaces: self.cli_delete(['interfaces', 'dummy', iface, 'netns', netns]) # commit changes self.cli_commit() netns_iface_list = cmd(f'sudo ip netns exec {netns} ip link show') for iface in self._interfaces: self.assertNotIn(iface, netns_iface_list) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_dhcpv6-server.py b/smoketest/scripts/cli/test_service_dhcpv6-server.py index 4d9dabc3f..cb6206632 100755 --- a/smoketest/scripts/cli/test_service_dhcpv6-server.py +++ b/smoketest/scripts/cli/test_service_dhcpv6-server.py @@ -1,182 +1,181 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2022 VyOS maintainers and contributors +# Copyright (C) 2020-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import unittest from base_vyostest_shim import VyOSUnitTestSHIM -from vyos.configsession import ConfigSessionError from vyos.template import inc_ip from vyos.utils.process import process_named_running from vyos.utils.file import read_file PROCESS_NAME = 'dhcpd' DHCPD_CONF = '/run/dhcp-server/dhcpdv6.conf' base_path = ['service', 'dhcpv6-server'] subnet = '2001:db8:f00::/64' dns_1 = '2001:db8::1' dns_2 = '2001:db8::2' domain = 'vyos.net' nis_servers = ['2001:db8:ffff::1', '2001:db8:ffff::2'] interface = 'eth0' interface_addr = inc_ip(subnet, 1) + '/64' class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): super(TestServiceDHCPv6Server, cls).setUpClass() cls.cli_set(cls, ['interfaces', 'ethernet', interface, 'address', interface_addr]) @classmethod def tearDownClass(cls): cls.cli_delete(cls, ['interfaces', 'ethernet', interface, 'address', interface_addr]) cls.cli_commit(cls) super(TestServiceDHCPv6Server, cls).tearDownClass() def tearDown(self): self.cli_delete(base_path) self.cli_commit() def test_single_pool(self): shared_net_name = 'SMOKE-1' search_domains = ['foo.vyos.net', 'bar.vyos.net'] lease_time = '1200' max_lease_time = '72000' min_lease_time = '600' preference = '10' sip_server = 'sip.vyos.net' sntp_server = inc_ip(subnet, 100) range_start = inc_ip(subnet, 256) # ::100 range_stop = inc_ip(subnet, 65535) # ::ffff pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] self.cli_set(base_path + ['preference', preference]) # we use the first subnet IP address as default gateway self.cli_set(pool + ['name-server', dns_1]) self.cli_set(pool + ['name-server', dns_2]) self.cli_set(pool + ['name-server', dns_2]) self.cli_set(pool + ['lease-time', 'default', lease_time]) self.cli_set(pool + ['lease-time', 'maximum', max_lease_time]) self.cli_set(pool + ['lease-time', 'minimum', min_lease_time]) self.cli_set(pool + ['nis-domain', domain]) self.cli_set(pool + ['nisplus-domain', domain]) self.cli_set(pool + ['sip-server', sip_server]) self.cli_set(pool + ['sntp-server', sntp_server]) self.cli_set(pool + ['address-range', 'start', range_start, 'stop', range_stop]) for server in nis_servers: self.cli_set(pool + ['nis-server', server]) self.cli_set(pool + ['nisplus-server', server]) for search in search_domains: self.cli_set(pool + ['domain-search', search]) client_base = 1 for client in ['client1', 'client2', 'client3']: cid = '00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:{}'.format(client_base) self.cli_set(pool + ['static-mapping', client, 'identifier', cid]) self.cli_set(pool + ['static-mapping', client, 'ipv6-address', inc_ip(subnet, client_base)]) self.cli_set(pool + ['static-mapping', client, 'ipv6-prefix', inc_ip(subnet, client_base << 64) + '/64']) client_base += 1 # commit changes self.cli_commit() config = read_file(DHCPD_CONF) self.assertIn(f'option dhcp6.preference {preference};', config) self.assertIn(f'subnet6 {subnet}' + r' {', config) search = '"' + '", "'.join(search_domains) + '"' nissrv = ', '.join(nis_servers) self.assertIn(f'range6 {range_start} {range_stop};', config) self.assertIn(f'default-lease-time {lease_time};', config) self.assertIn(f'default-lease-time {lease_time};', config) self.assertIn(f'max-lease-time {max_lease_time};', config) self.assertIn(f'min-lease-time {min_lease_time};', config) self.assertIn(f'option dhcp6.domain-search {search};', config) self.assertIn(f'option dhcp6.name-servers {dns_1}, {dns_2};', config) self.assertIn(f'option dhcp6.nis-domain-name "{domain}";', config) self.assertIn(f'option dhcp6.nis-servers {nissrv};', config) self.assertIn(f'option dhcp6.nisp-domain-name "{domain}";', config) self.assertIn(f'option dhcp6.nisp-servers {nissrv};', config) self.assertIn(f'set shared-networkname = "{shared_net_name}";', config) client_base = 1 for client in ['client1', 'client2', 'client3']: cid = '00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:{}'.format(client_base) ip = inc_ip(subnet, client_base) prefix = inc_ip(subnet, client_base << 64) + '/64' self.assertIn(f'host {shared_net_name}_{client}' + ' {', config) self.assertIn(f'fixed-address6 {ip};', config) self.assertIn(f'fixed-prefix6 {prefix};', config) self.assertIn(f'host-identifier option dhcp6.client-id {cid};', config) client_base += 1 # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) def test_prefix_delegation(self): shared_net_name = 'SMOKE-2' range_start = inc_ip(subnet, 256) # ::100 range_stop = inc_ip(subnet, 65535) # ::ffff delegate_start = '2001:db8:ee::' delegate_stop = '2001:db8:ee:ff00::' delegate_len = '56' pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] self.cli_set(pool + ['address-range', 'start', range_start, 'stop', range_stop]) self.cli_set(pool + ['prefix-delegation', 'start', delegate_start, 'stop', delegate_stop]) self.cli_set(pool + ['prefix-delegation', 'start', delegate_start, 'prefix-length', delegate_len]) # commit changes self.cli_commit() config = read_file(DHCPD_CONF) self.assertIn(f'subnet6 {subnet}' + r' {', config) self.assertIn(f'range6 {range_start} {range_stop};', config) self.assertIn(f'prefix6 {delegate_start} {delegate_stop} /{delegate_len};', config) # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) def test_global_nameserver(self): shared_net_name = 'SMOKE-3' ns_global_1 = '2001:db8::1111' ns_global_2 = '2001:db8::2222' self.cli_set(base_path + ['global-parameters', 'name-server', ns_global_1]) self.cli_set(base_path + ['global-parameters', 'name-server', ns_global_2]) self.cli_set(base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]) # commit changes self.cli_commit() config = read_file(DHCPD_CONF) self.assertIn(f'option dhcp6.name-servers {ns_global_1}, {ns_global_2};', config) self.assertIn(f'subnet6 {subnet}' + r' {', config) self.assertIn(f'set shared-networkname = "{shared_net_name}";', config) # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py index ee29f8198..6fffe7e0d 100755 --- a/src/conf_mode/protocols_ospf.py +++ b/src/conf_mode/protocols_ospf.py @@ -1,292 +1,290 @@ #!/usr/bin/env python3 # # Copyright (C) 2021-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import os - from sys import exit from sys import argv from vyos.config import Config from vyos.config import config_dict_merge from vyos.configdict import dict_merge from vyos.configdict import node_changed from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_route_map from vyos.configverify import verify_interface_exists from vyos.configverify import verify_access_list from vyos.template import render_to_string from vyos.utils.dict import dict_search from vyos.utils.network import get_interface_config from vyos import ConfigError from vyos import frr from vyos import airbag airbag.enable() def get_config(config=None): if config: conf = config else: conf = Config() vrf = None if len(argv) > 1: vrf = argv[1] base_path = ['protocols', 'ospf'] # eqivalent of the C foo ? 'a' : 'b' statement base = vrf and ['vrf', 'name', vrf, 'protocols', 'ospf'] or base_path ospf = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) # Assign the name of our VRF context. This MUST be done before the return # statement below, else on deletion we will delete the default instance # instead of the VRF instance. if vrf: ospf['vrf'] = vrf # FRR has VRF support for different routing daemons. As interfaces belong # to VRFs - or the global VRF, we need to check for changed interfaces so # that they will be properly rendered for the FRR config. Also this eases # removal of interfaces from the running configuration. interfaces_removed = node_changed(conf, base + ['interface']) if interfaces_removed: ospf['interface_removed'] = list(interfaces_removed) # Bail out early if configuration tree does no longer exist. this must # be done after retrieving the list of interfaces to be removed. if not conf.exists(base): ospf.update({'deleted' : ''}) return ospf # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. default_values = conf.get_config_defaults(**ospf.kwargs, recursive=True) # We have to cleanup the default dict, as default values could enable features # which are not explicitly enabled on the CLI. Example: default-information # originate comes with a default metric-type of 2, which will enable the # entire default-information originate tree, even when not set via CLI so we # need to check this first and probably drop that key. if dict_search('default_information.originate', ospf) is None: del default_values['default_information'] if 'mpls_te' not in ospf: del default_values['mpls_te'] if 'graceful_restart' not in ospf: del default_values['graceful_restart'] for area_num in default_values.get('area', []): if dict_search(f'area.{area_num}.area_type.nssa', ospf) is None: del default_values['area'][area_num]['area_type']['nssa'] for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'rip', 'static']: if dict_search(f'redistribute.{protocol}', ospf) is None: del default_values['redistribute'][protocol] if not bool(default_values['redistribute']): del default_values['redistribute'] for interface in ospf.get('interface', []): # We need to reload the defaults on every pass b/c of # hello-multiplier dependency on dead-interval # If hello-multiplier is set, we need to remove the default from # dead-interval. if 'hello_multiplier' in ospf['interface'][interface]: del default_values['interface'][interface]['dead_interval'] ospf = config_dict_merge(default_values, ospf) # We also need some additional information from the config, prefix-lists # and route-maps for instance. They will be used in verify(). # # XXX: one MUST always call this without the key_mangling() option! See # vyos.configverify.verify_common_route_maps() for more information. tmp = conf.get_config_dict(['policy']) # Merge policy dict into "regular" config dict ospf = dict_merge(tmp, ospf) return ospf def verify(ospf): if not ospf: return None verify_common_route_maps(ospf) # As we can have a default-information route-map, we need to validate it! route_map_name = dict_search('default_information.originate.route_map', ospf) if route_map_name: verify_route_map(route_map_name, ospf) # Validate if configured Access-list exists if 'area' in ospf: networks = [] for area, area_config in ospf['area'].items(): if 'import_list' in area_config: acl_import = area_config['import_list'] if acl_import: verify_access_list(acl_import, ospf) if 'export_list' in area_config: acl_export = area_config['export_list'] if acl_export: verify_access_list(acl_export, ospf) if 'network' in area_config: for network in area_config['network']: if network in networks: raise ConfigError(f'Network "{network}" already defined in different area!') networks.append(network) if 'interface' in ospf: for interface, interface_config in ospf['interface'].items(): verify_interface_exists(interface) # One can not use dead-interval and hello-multiplier at the same # time. FRR will only activate the last option set via CLI. if {'hello_multiplier', 'dead_interval'} <= set(interface_config): raise ConfigError(f'Can not use hello-multiplier and dead-interval ' \ f'concurrently for {interface}!') # One can not use the "network <prefix> area <id>" command and an # per interface area assignment at the same time. FRR will error # out using: "Please remove all network commands first." if 'area' in ospf and 'area' in interface_config: for area, area_config in ospf['area'].items(): if 'network' in area_config: raise ConfigError('Can not use OSPF interface area and area ' \ 'network configuration at the same time!') # If interface specific options are set, we must ensure that the # interface is bound to our requesting VRF. Due to the VyOS # priorities the interface is bound to the VRF after creation of # the VRF itself, and before any routing protocol is configured. if 'vrf' in ospf: vrf = ospf['vrf'] tmp = get_interface_config(interface) if 'master' not in tmp or tmp['master'] != vrf: raise ConfigError(f'Interface "{interface}" is not a member of VRF "{vrf}"!') # Segment routing checks if dict_search('segment_routing.global_block', ospf): g_high_label_value = dict_search('segment_routing.global_block.high_label_value', ospf) g_low_label_value = dict_search('segment_routing.global_block.low_label_value', ospf) # If segment routing global block high or low value is blank, throw error if not (g_low_label_value or g_high_label_value): raise ConfigError('Segment routing global-block requires both low and high value!') # If segment routing global block low value is higher than the high value, throw error if int(g_low_label_value) > int(g_high_label_value): raise ConfigError('Segment routing global-block low value must be lower than high value') if dict_search('segment_routing.local_block', ospf): if dict_search('segment_routing.global_block', ospf) == None: raise ConfigError('Segment routing local-block requires global-block to be configured!') l_high_label_value = dict_search('segment_routing.local_block.high_label_value', ospf) l_low_label_value = dict_search('segment_routing.local_block.low_label_value', ospf) # If segment routing local-block high or low value is blank, throw error if not (l_low_label_value or l_high_label_value): raise ConfigError('Segment routing local-block requires both high and low value!') # If segment routing local-block low value is higher than the high value, throw error if int(l_low_label_value) > int(l_high_label_value): raise ConfigError('Segment routing local-block low value must be lower than high value') # local-block most live outside global block global_range = range(int(g_low_label_value), int(g_high_label_value) +1) local_range = range(int(l_low_label_value), int(l_high_label_value) +1) # Check for overlapping ranges if list(set(global_range) & set(local_range)): raise ConfigError(f'Segment-Routing Global Block ({g_low_label_value}/{g_high_label_value}) '\ f'conflicts with Local Block ({l_low_label_value}/{l_high_label_value})!') # Check for a blank or invalid value per prefix if dict_search('segment_routing.prefix', ospf): for prefix, prefix_config in ospf['segment_routing']['prefix'].items(): if 'index' in prefix_config: if prefix_config['index'].get('value') is None: raise ConfigError(f'Segment routing prefix {prefix} index value cannot be blank.') # Check for explicit-null and no-php-flag configured at the same time per prefix if dict_search('segment_routing.prefix', ospf): for prefix, prefix_config in ospf['segment_routing']['prefix'].items(): if 'index' in prefix_config: if ("explicit_null" in prefix_config['index']) and ("no_php_flag" in prefix_config['index']): raise ConfigError(f'Segment routing prefix {prefix} cannot have both explicit-null '\ f'and no-php-flag configured at the same time.') # Check for index ranges being larger than the segment routing global block if dict_search('segment_routing.global_block', ospf): g_high_label_value = dict_search('segment_routing.global_block.high_label_value', ospf) g_low_label_value = dict_search('segment_routing.global_block.low_label_value', ospf) g_label_difference = int(g_high_label_value) - int(g_low_label_value) if dict_search('segment_routing.prefix', ospf): for prefix, prefix_config in ospf['segment_routing']['prefix'].items(): if 'index' in prefix_config: index_size = ospf['segment_routing']['prefix'][prefix]['index']['value'] if int(index_size) > int(g_label_difference): raise ConfigError(f'Segment routing prefix {prefix} cannot have an '\ f'index base size larger than the SRGB label base.') # Check route summarisation if 'summary_address' in ospf: for prefix, prefix_options in ospf['summary_address'].items(): if {'tag', 'no_advertise'} <= set(prefix_options): raise ConfigError(f'Can not set both "tag" and "no-advertise" for Type-5 '\ f'and Type-7 route summarisation of "{prefix}"!') return None def generate(ospf): if not ospf or 'deleted' in ospf: return None ospf['frr_ospfd_config'] = render_to_string('frr/ospfd.frr.j2', ospf) return None def apply(ospf): ospf_daemon = 'ospfd' # Save original configuration prior to starting any commit actions frr_cfg = frr.FRRConfig() # Generate empty helper string which can be ammended to FRR commands, it # will be either empty (default VRF) or contain the "vrf <name" statement vrf = '' if 'vrf' in ospf: vrf = ' vrf ' + ospf['vrf'] frr_cfg.load_configuration(ospf_daemon) frr_cfg.modify_section(f'^router ospf{vrf}', stop_pattern='^exit', remove_stop_mark=True) for key in ['interface', 'interface_removed']: if key not in ospf: continue for interface in ospf[key]: frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) if 'frr_ospfd_config' in ospf: frr_cfg.add_before(frr.default_add_before, ospf['frr_ospfd_config']) frr_cfg.commit_configuration(ospf_daemon) return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/service_dhcp-server.py b/src/conf_mode/service_dhcp-server.py index 3cfd74a19..544b759ac 100755 --- a/src/conf_mode/service_dhcp-server.py +++ b/src/conf_mode/service_dhcp-server.py @@ -1,353 +1,352 @@ #!/usr/bin/env python3 # # Copyright (C) 2018-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os from ipaddress import ip_address from ipaddress import ip_network -from netaddr import IPAddress from netaddr import IPRange from sys import exit from vyos.base import DeprecationWarning from vyos.config import Config from vyos.template import render from vyos.utils.dict import dict_search from vyos.utils.process import call from vyos.utils.process import run from vyos.utils.network import is_subnet_connected from vyos.utils.network import is_addr_assigned from vyos import ConfigError from vyos import airbag airbag.enable() config_file = '/run/dhcp-server/dhcpd.conf' systemd_override = r'/run/systemd/system/isc-dhcp-server.service.d/10-override.conf' def dhcp_slice_range(exclude_list, range_dict): """ This function is intended to slice a DHCP range. What does it mean? Lets assume we have a DHCP range from '192.0.2.1' to '192.0.2.100' but want to exclude address '192.0.2.74' and '192.0.2.75'. We will pass an input 'range_dict' in the format: {'start' : '192.0.2.1', 'stop' : '192.0.2.100' } and we will receive an output list of: [{'start' : '192.0.2.1' , 'stop' : '192.0.2.73' }, {'start' : '192.0.2.76', 'stop' : '192.0.2.100' }] The resulting list can then be used in turn to build the proper dhcpd configuration file. """ output = [] # exclude list must be sorted for this to work exclude_list = sorted(exclude_list) range_start = range_dict['start'] range_stop = range_dict['stop'] range_last_exclude = '' for e in exclude_list: if (ip_address(e) >= ip_address(range_start)) and \ (ip_address(e) <= ip_address(range_stop)): range_last_exclude = e for e in exclude_list: if (ip_address(e) >= ip_address(range_start)) and \ (ip_address(e) <= ip_address(range_stop)): # Build new address range ending one address before exclude address r = { 'start' : range_start, 'stop' : str(ip_address(e) -1) } # On the next run our address range will start one address after # the exclude address range_start = str(ip_address(e) + 1) # on subsequent exclude addresses we can not # append them to our output if not (ip_address(r['start']) > ip_address(r['stop'])): # Everything is fine, add range to result output.append(r) # Take care of last IP address range spanning from the last exclude # address (+1) to the end of the initial configured range if ip_address(e) == ip_address(range_last_exclude): r = { 'start': str(ip_address(e) + 1), 'stop': str(range_stop) } if not (ip_address(r['start']) > ip_address(r['stop'])): output.append(r) else: # if the excluded address was not part of the range, we simply return # the entire ranga again if not range_last_exclude: if range_dict not in output: output.append(range_dict) return output def get_config(config=None): if config: conf = config else: conf = Config() base = ['service', 'dhcp-server'] if not conf.exists(base): return None dhcp = conf.get_config_dict(base, key_mangling=('-', '_'), no_tag_node_value_mangle=True, get_first_key=True, with_recursive_defaults=True) if 'shared_network_name' in dhcp: for network, network_config in dhcp['shared_network_name'].items(): if 'subnet' in network_config: for subnet, subnet_config in network_config['subnet'].items(): # If exclude IP addresses are defined we need to slice them out of # the defined ranges if {'exclude', 'range'} <= set(subnet_config): new_range_id = 0 new_range_dict = {} for r, r_config in subnet_config['range'].items(): for slice in dhcp_slice_range(subnet_config['exclude'], r_config): new_range_dict.update({new_range_id : slice}) new_range_id +=1 dhcp['shared_network_name'][network]['subnet'][subnet].update( {'range' : new_range_dict}) if len(dhcp['high_availability']) == 1: ## only default value for mode is set, need to remove ha node del dhcp['high_availability'] return dhcp def verify(dhcp): # bail out early - looks like removal from running config if not dhcp or 'disable' in dhcp: return None # If DHCP is enabled we need one share-network if 'shared_network_name' not in dhcp: raise ConfigError('No DHCP shared networks configured.\n' \ 'At least one DHCP shared network must be configured.') # Inspect shared-network/subnet listen_ok = False subnets = [] failover_ok = False shared_networks = len(dhcp['shared_network_name']) disabled_shared_networks = 0 common_deprecation_msg = 'are subject of removal in VyOS 1.5! Please raise a feature request for proper CLI nodes!' if 'global_parameters' in dhcp: DeprecationWarning(f'Additional global parameters {common_deprecation_msg}') # A shared-network requires a subnet definition for network, network_config in dhcp['shared_network_name'].items(): if 'shared_network_parameters' in network_config: DeprecationWarning(f'Additional shared network parameters in "{network}" {common_deprecation_msg}') if 'disable' in network_config: disabled_shared_networks += 1 if 'subnet' not in network_config: raise ConfigError(f'No subnets defined for {network}. At least one\n' \ 'lease subnet must be configured.') for subnet, subnet_config in network_config['subnet'].items(): if 'subnet_parameters' in subnet_config: DeprecationWarning(f'Additional subnet parameters in "{subnet}" {common_deprecation_msg}') # All delivered static routes require a next-hop to be set if 'static_route' in subnet_config: for route, route_option in subnet_config['static_route'].items(): if 'next_hop' not in route_option: raise ConfigError(f'DHCP static-route "{route}" requires router to be defined!') # DHCP failover needs at least one subnet that uses it if 'enable_failover' in subnet_config: if 'high_availability' not in dhcp: raise ConfigError(f'Can not enable high availability for "{subnet}" in "{network}".\n' \ 'High availability is not configured globally!') failover_ok = True # Check if DHCP address range is inside configured subnet declaration if 'range' in subnet_config: networks = [] for range, range_config in subnet_config['range'].items(): if not {'start', 'stop'} <= set(range_config): raise ConfigError(f'DHCP range "{range}" start and stop address must be defined!') # Start/Stop address must be inside network for key in ['start', 'stop']: if ip_address(range_config[key]) not in ip_network(subnet): raise ConfigError(f'DHCP range "{range}" {key} address not within shared-network "{network}, {subnet}"!') # Stop address must be greater or equal to start address if ip_address(range_config['stop']) < ip_address(range_config['start']): raise ConfigError(f'DHCP range "{range}" stop address must be greater or equal\n' \ 'to the ranges start address!') for network in networks: start = range_config['start'] stop = range_config['stop'] if start in network: raise ConfigError(f'Range "{range}" start address "{start}" already part of another range!') if stop in network: raise ConfigError(f'Range "{range}" stop address "{stop}" already part of another range!') tmp = IPRange(range_config['start'], range_config['stop']) networks.append(tmp) # Exclude addresses must be in bound if 'exclude' in subnet_config: for exclude in subnet_config['exclude']: if ip_address(exclude) not in ip_network(subnet): raise ConfigError(f'Excluded IP address "{exclude}" not within shared-network "{network}, {subnet}"!') # At least one DHCP address range or static-mapping required if 'range' not in subnet_config and 'static_mapping' not in subnet_config: raise ConfigError(f'No DHCP address range or active static-mapping configured\n' \ f'within shared-network "{network}, {subnet}"!') if 'static_mapping' in subnet_config: # Static mappings require just a MAC address (will use an IP from the dynamic pool if IP is not set) used_ips = [] used_mac = [] for mapping, mapping_config in subnet_config['static_mapping'].items(): if 'ip_address' in mapping_config: if ip_address(mapping_config['ip_address']) not in ip_network(subnet): raise ConfigError(f'Configured static lease address for mapping "{mapping}" is\n' \ f'not within shared-network "{network}, {subnet}"!') if 'mac_address' not in mapping_config: raise ConfigError(f'MAC address required for static mapping "{mapping}"\n' \ f'within shared-network "{network}, {subnet}"!') if 'disable' not in mapping_config: if mapping_config['ip_address'] in used_ips: raise ConfigError(f'Configured IP address for static mapping "{mapping}" already exists on another static mapping') used_ips.append(mapping_config['ip_address']) if 'mac_address' in mapping_config and 'disable' not in mapping_config: if mapping_config['mac_address'] in used_mac: raise ConfigError(f'Configured MAC address for static mapping "{mapping}" already exists on another static mapping') used_mac.append(mapping_config['mac_address']) # There must be one subnet connected to a listen interface. # This only counts if the network itself is not disabled! if 'disable' not in network_config: if is_subnet_connected(subnet, primary=False): listen_ok = True # Subnets must be non overlapping if subnet in subnets: raise ConfigError(f'Configured subnets must be unique! Subnet "{subnet}"\n' 'defined multiple times!') subnets.append(subnet) # Check for overlapping subnets net = ip_network(subnet) for n in subnets: net2 = ip_network(n) if (net != net2): if net.overlaps(net2): raise ConfigError(f'Conflicting subnet ranges: "{net}" overlaps "{net2}"!') # Prevent 'disable' for shared-network if only one network is configured if (shared_networks - disabled_shared_networks) < 1: raise ConfigError(f'At least one shared network must be active!') if 'high_availability' in dhcp: if not failover_ok: raise ConfigError('DHCP failover must be enabled for at least one subnet!') for key in ['name', 'remote', 'source_address', 'status']: if key not in dhcp['high_availability']: tmp = key.replace('_', '-') raise ConfigError(f'DHCP failover requires "{tmp}" to be specified!') for address in (dict_search('listen_address', dhcp) or []): if is_addr_assigned(address, include_vrf=True): listen_ok = True # no need to probe further networks, we have one that is valid continue else: raise ConfigError(f'listen-address "{address}" not configured on any interface') if not listen_ok: raise ConfigError('None of the configured subnets have an appropriate primary IP address on any\n' 'broadcast interface configured, nor was there an explicit listen-address\n' 'configured for serving DHCP relay packets!') return None def generate(dhcp): # bail out early - looks like removal from running config if not dhcp or 'disable' in dhcp: return None # Please see: https://vyos.dev/T1129 for quoting of the raw # parameters we can pass to ISC DHCPd tmp_file = '/tmp/dhcpd.conf' render(tmp_file, 'dhcp-server/dhcpd.conf.j2', dhcp, formater=lambda _: _.replace(""", '"')) # XXX: as we have the ability for a user to pass in "raw" options via VyOS # CLI (see T3544) we now ask ISC dhcpd to test the newly rendered # configuration tmp = run(f'/usr/sbin/dhcpd -4 -q -t -cf {tmp_file}') if tmp > 0: if os.path.exists(tmp_file): os.unlink(tmp_file) raise ConfigError('Configuration file errors encountered - check your options!') # Now that we know that the newly rendered configuration is "good" we can # render the "real" configuration render(config_file, 'dhcp-server/dhcpd.conf.j2', dhcp, formater=lambda _: _.replace(""", '"')) render(systemd_override, 'dhcp-server/10-override.conf.j2', dhcp) # Clean up configuration test file if os.path.exists(tmp_file): os.unlink(tmp_file) return None def apply(dhcp): call('systemctl daemon-reload') # bail out early - looks like removal from running config if not dhcp or 'disable' in dhcp: call('systemctl stop isc-dhcp-server.service') if os.path.exists(config_file): os.unlink(config_file) return None call('systemctl restart isc-dhcp-server.service') return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/service_dhcpv6-server.py b/src/conf_mode/service_dhcpv6-server.py index 427001609..5489a744e 100755 --- a/src/conf_mode/service_dhcpv6-server.py +++ b/src/conf_mode/service_dhcpv6-server.py @@ -1,195 +1,194 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2022 VyOS maintainers and contributors +# Copyright (C) 2018-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os from ipaddress import ip_address from ipaddress import ip_network from sys import exit from vyos.config import Config from vyos.template import render -from vyos.template import is_ipv6 from vyos.utils.process import call from vyos.utils.dict import dict_search from vyos.utils.network import is_subnet_connected from vyos import ConfigError from vyos import airbag airbag.enable() config_file = '/run/dhcp-server/dhcpdv6.conf' def get_config(config=None): if config: conf = config else: conf = Config() base = ['service', 'dhcpv6-server'] if not conf.exists(base): return None dhcpv6 = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) return dhcpv6 def verify(dhcpv6): # bail out early - looks like removal from running config if not dhcpv6 or 'disable' in dhcpv6: return None # If DHCP is enabled we need one share-network if 'shared_network_name' not in dhcpv6: raise ConfigError('No DHCPv6 shared networks configured. At least '\ 'one DHCPv6 shared network must be configured.') # Inspect shared-network/subnet subnets = [] listen_ok = False for network, network_config in dhcpv6['shared_network_name'].items(): # A shared-network requires a subnet definition if 'subnet' not in network_config: raise ConfigError(f'No DHCPv6 lease subnets configured for "{network}". '\ 'At least one lease subnet must be configured for '\ 'each shared network!') for subnet, subnet_config in network_config['subnet'].items(): if 'address_range' in subnet_config: if 'start' in subnet_config['address_range']: range6_start = [] range6_stop = [] for start, start_config in subnet_config['address_range']['start'].items(): if 'stop' not in start_config: raise ConfigError(f'address-range stop address for start "{start}" is not defined!') stop = start_config['stop'] # Start address must be inside network if not ip_address(start) in ip_network(subnet): raise ConfigError(f'address-range start address "{start}" is not in subnet "{subnet}"!') # Stop address must be inside network if not ip_address(stop) in ip_network(subnet): raise ConfigError(f'address-range stop address "{stop}" is not in subnet "{subnet}"!') # Stop address must be greater or equal to start address if not ip_address(stop) >= ip_address(start): raise ConfigError(f'address-range stop address "{stop}" must be greater then or equal ' \ f'to the range start address "{start}"!') # DHCPv6 range start address must be unique - two ranges can't # start with the same address - makes no sense if start in range6_start: raise ConfigError(f'Conflicting DHCPv6 lease range: '\ f'Pool start address "{start}" defined multipe times!') range6_start.append(start) # DHCPv6 range stop address must be unique - two ranges can't # end with the same address - makes no sense if stop in range6_stop: raise ConfigError(f'Conflicting DHCPv6 lease range: '\ f'Pool stop address "{stop}" defined multipe times!') range6_stop.append(stop) if 'prefix' in subnet_config: for prefix in subnet_config['prefix']: if ip_network(prefix) not in ip_network(subnet): raise ConfigError(f'address-range prefix "{prefix}" is not in subnet "{subnet}""') # Prefix delegation sanity checks if 'prefix_delegation' in subnet_config: if 'start' not in subnet_config['prefix_delegation']: raise ConfigError('prefix-delegation start address not defined!') for prefix, prefix_config in subnet_config['prefix_delegation']['start'].items(): if 'stop' not in prefix_config: raise ConfigError(f'Stop address of delegated IPv6 prefix range "{prefix}" '\ f'must be configured') if 'prefix_length' not in prefix_config: raise ConfigError('Length of delegated IPv6 prefix must be configured') # Static mappings don't require anything (but check if IP is in subnet if it's set) if 'static_mapping' in subnet_config: for mapping, mapping_config in subnet_config['static_mapping'].items(): if 'ipv6_address' in mapping_config: # Static address must be in subnet if ip_address(mapping_config['ipv6_address']) not in ip_network(subnet): raise ConfigError(f'static-mapping address for mapping "{mapping}" is not in subnet "{subnet}"!') if 'vendor_option' in subnet_config: if len(dict_search('vendor_option.cisco.tftp_server', subnet_config)) > 2: raise ConfigError(f'No more then two Cisco tftp-servers should be defined for subnet "{subnet}"!') # Subnets must be unique if subnet in subnets: raise ConfigError(f'DHCPv6 subnets must be unique! Subnet {subnet} defined multiple times!') subnets.append(subnet) # DHCPv6 requires at least one configured address range or one static mapping # (FIXME: is not actually checked right now?) # There must be one subnet connected to a listen interface if network is not disabled. if 'disable' not in network_config: if is_subnet_connected(subnet): listen_ok = True # DHCPv6 subnet must not overlap. ISC DHCP also complains about overlapping # subnets: "Warning: subnet 2001:db8::/32 overlaps subnet 2001:db8:1::/32" net = ip_network(subnet) for n in subnets: net2 = ip_network(n) if (net != net2): if net.overlaps(net2): raise ConfigError('DHCPv6 conflicting subnet ranges: {0} overlaps {1}'.format(net, net2)) if not listen_ok: raise ConfigError('None of the DHCPv6 subnets are connected to a subnet6 on '\ 'this machine. At least one subnet6 must be connected such that '\ 'DHCPv6 listens on an interface!') return None def generate(dhcpv6): # bail out early - looks like removal from running config if not dhcpv6 or 'disable' in dhcpv6: return None render(config_file, 'dhcp-server/dhcpdv6.conf.j2', dhcpv6) return None def apply(dhcpv6): # bail out early - looks like removal from running config service_name = 'isc-dhcp-server6.service' if not dhcpv6 or 'disable' in dhcpv6: # DHCP server is removed in the commit call(f'systemctl stop {service_name}') if os.path.exists(config_file): os.unlink(config_file) return None call(f'systemctl restart {service_name}') return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/helpers/vyos_config_sync.py b/src/helpers/vyos_config_sync.py index 77f7cd810..7eec3f4f3 100755 --- a/src/helpers/vyos_config_sync.py +++ b/src/helpers/vyos_config_sync.py @@ -1,202 +1,202 @@ #!/usr/bin/env python3 # # Copyright (C) 2023-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # # import os import json import requests import urllib3 import logging -from typing import Optional, List, Union, Dict, Any +from typing import Optional, List, Dict, Any from vyos.config import Config from vyos.template import bracketize_ipv6 CONFIG_FILE = '/run/config_sync_conf.conf' # Logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) logger.name = os.path.basename(__file__) # API API_HEADERS = {'Content-Type': 'application/json'} def post_request(url: str, data: str, headers: Dict[str, str]) -> requests.Response: """Sends a POST request to the specified URL Args: url (str): The URL to send the POST request to. data (Dict[str, Any]): The data to send with the POST request. headers (Dict[str, str]): The headers to include with the POST request. Returns: requests.Response: The response object representing the server's response to the request """ response = requests.post(url, data=data, headers=headers, verify=False, timeout=timeout) return response def retrieve_config(section: Optional[List[str]] = None) -> Optional[Dict[str, Any]]: """Retrieves the configuration from the local server. Args: section: List[str]: The section of the configuration to retrieve. Default is None. Returns: Optional[Dict[str, Any]]: The retrieved configuration as a dictionary, or None if an error occurred. """ if section is None: section = [] conf = Config() config = conf.get_config_dict(section, get_first_key=True) if config: return config return None def set_remote_config( address: str, key: str, commands: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]: """Loads the VyOS configuration in JSON format to a remote host. Args: address (str): The address of the remote host. key (str): The key to use for loading the configuration. commands (list): List of set/load commands for request, given as: [{'op': str, 'path': list[str], 'section': dict}, ...] Returns: Optional[Dict[str, Any]]: The response from the remote host as a dictionary, or None if a RequestException occurred. """ headers = {'Content-Type': 'application/json'} # Disable the InsecureRequestWarning urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) url = f'https://{address}/configure-section' data = json.dumps({ 'commands': commands, 'key': key }) try: config = post_request(url, data, headers) return config.json() except requests.exceptions.RequestException as e: print(f"An error occurred: {e}") logger.error(f"An error occurred: {e}") return None def is_section_revised(section: List[str]) -> bool: from vyos.config_mgmt import is_node_revised return is_node_revised(section) def config_sync(secondary_address: str, secondary_key: str, sections: List[list[str]], mode: str): """Retrieve a config section from primary router in JSON format and send it to secondary router """ if not any(map(is_section_revised, sections)): return logger.info( f"Config synchronization: Mode={mode}, Secondary={secondary_address}" ) # Sync sections ("nat", "firewall", etc) commands = [] for section in sections: config_json = retrieve_config(section=section) # Check if config path deesn't exist, for example "set nat" # we set empty value for config_json data # As we cannot send to the remote host section "nat None" config if not config_json: config_json = {} logger.debug( f"Retrieved config for section '{section}': {config_json}") d = {'op': mode, 'path': section, 'section': config_json} commands.append(d) set_config = set_remote_config(address=secondary_address, key=secondary_key, commands=commands) logger.debug(f"Set config for sections '{sections}': {set_config}") if __name__ == '__main__': # Read configuration from file if not os.path.exists(CONFIG_FILE): logger.error(f"Post-commit: No config file '{CONFIG_FILE}' exists") exit(0) with open(CONFIG_FILE, 'r') as f: config_data = f.read() config = json.loads(config_data) mode = config.get('mode') secondary_address = config.get('secondary', {}).get('address') secondary_address = bracketize_ipv6(secondary_address) secondary_key = config.get('secondary', {}).get('key') sections = config.get('section') timeout = int(config.get('secondary', {}).get('timeout')) if not all([ mode, secondary_address, secondary_key, sections ]): logger.error( "Missing required configuration data for config synchronization.") exit(0) # Generate list_sections of sections/subsections # [ # ['interfaces', 'pseudo-ethernet'], ['interfaces', 'virtual-ethernet'], ['nat'], ['nat66'] # ] list_sections = [] for section, subsections in sections.items(): if subsections: for subsection in subsections: list_sections.append([section, subsection]) else: list_sections.append([section]) config_sync(secondary_address, secondary_key, list_sections, mode) diff --git a/src/migration-scripts/conntrack/2-to-3 b/src/migration-scripts/conntrack/2-to-3 index 6fb457b7f..6bb42be1e 100755 --- a/src/migration-scripts/conntrack/2-to-3 +++ b/src/migration-scripts/conntrack/2-to-3 @@ -1,37 +1,36 @@ #!/usr/bin/env python3 # Conntrack syntax version 3 # Enables all conntrack modules (previous default behaviour) and omits manually disabled modules. import sys from vyos.configtree import ConfigTree -from vyos.version import get_version if len(sys.argv) < 2: print('Must specify file name!') sys.exit(1) filename = sys.argv[1] with open(filename, 'r') as f: config = ConfigTree(f.read()) module_path = ['system', 'conntrack', 'modules'] # Go over all conntrack modules available as of v1.3.0. for module in ['ftp', 'h323', 'nfs', 'pptp', 'sip', 'sqlnet', 'tftp']: # 'disable' is being phased out. if config.exists(module_path + [module, 'disable']): config.delete(module_path + [module]) # If it wasn't manually 'disable'd, it was enabled by default. else: config.set(module_path + [module]) try: if config.exists(module_path): with open(filename, 'w') as f: f.write(config.to_string()) except OSError as e: print(f'Failed to save the modified config: {e}') sys.exit(1) diff --git a/src/migration-scripts/firewall/10-to-11 b/src/migration-scripts/firewall/10-to-11 index abb804a28..854d5a558 100755 --- a/src/migration-scripts/firewall/10-to-11 +++ b/src/migration-scripts/firewall/10-to-11 @@ -1,210 +1,207 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # T5160: Firewall re-writing -# cli changes from: +# cli changes from: # set firewall name <name> ... # set firewall ipv6-name <name> ... # To -# set firewall ipv4 name <name> -# set firewall ipv6 name <name> +# set firewall ipv4 name <name> +# set firewall ipv6 name <name> ## Also from 'firewall interface' removed. ## in and out: # set firewall interface <iface> [in|out] [name | ipv6-name] <name> # To # set firewall [ipv4 | ipv6] forward filter rule <5,10,15,...> [inbound-interface | outboubd-interface] interface-name <iface> # set firewall [ipv4 | ipv6] forward filter rule <5,10,15,...> action jump # set firewall [ipv4 | ipv6] forward filter rule <5,10,15,...> jump-target <name> ## local: # set firewall interface <iface> local [name | ipv6-name] <name> # To # set firewall [ipv4 | ipv6] input filter rule <5,10,15,...> inbound-interface interface-name <iface> # set firewall [ipv4 | ipv6] input filter rule <5,10,15,...> action jump # set firewall [ipv4 | ipv6] input filter rule <5,10,15,...> jump-target <name> -import re - from sys import argv from sys import exit from vyos.configtree import ConfigTree -from vyos.ifconfig import Section if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() base = ['firewall'] config = ConfigTree(config_file) if not config.exists(base): # Nothing to do exit(0) ### Migration of state policies if config.exists(base + ['state-policy']): for state in config.list_nodes(base + ['state-policy']): action = config.return_value(base + ['state-policy', state, 'action']) config.set(base + ['global-options', 'state-policy', state, 'action'], value=action) if config.exists(base + ['state-policy', state, 'log']): config.set(base + ['global-options', 'state-policy', state, 'log'], value='enable') config.delete(base + ['state-policy']) ## migration of global options: for option in ['all-ping', 'broadcast-ping', 'config-trap', 'ip-src-route', 'ipv6-receive-redirects', 'ipv6-src-route', 'log-martians', 'receive-redirects', 'resolver-cache', 'resolver-internal', 'send-redirects', 'source-validation', 'syn-cookies', 'twa-hazards-protection']: if config.exists(base + [option]): if option != 'config-trap': val = config.return_value(base + [option]) config.set(base + ['global-options', option], value=val) config.delete(base + [option]) ### Migration of firewall name and ipv6-name ### Also migrate legacy 'accept' behaviour if config.exists(base + ['name']): config.set(['firewall', 'ipv4', 'name']) config.set_tag(['firewall', 'ipv4', 'name']) for ipv4name in config.list_nodes(base + ['name']): config.copy(base + ['name', ipv4name], base + ['ipv4', 'name', ipv4name]) if config.exists(base + ['ipv4', 'name', ipv4name, 'default-action']): action = config.return_value(base + ['ipv4', 'name', ipv4name, 'default-action']) if action == 'accept': config.set(base + ['ipv4', 'name', ipv4name, 'default-action'], value='return') if config.exists(base + ['ipv4', 'name', ipv4name, 'rule']): for rule_id in config.list_nodes(base + ['ipv4', 'name', ipv4name, 'rule']): action = config.return_value(base + ['ipv4', 'name', ipv4name, 'rule', rule_id, 'action']) if action == 'accept': config.set(base + ['ipv4', 'name', ipv4name, 'rule', rule_id, 'action'], value='return') config.delete(base + ['name']) if config.exists(base + ['ipv6-name']): config.set(['firewall', 'ipv6', 'name']) config.set_tag(['firewall', 'ipv6', 'name']) for ipv6name in config.list_nodes(base + ['ipv6-name']): config.copy(base + ['ipv6-name', ipv6name], base + ['ipv6', 'name', ipv6name]) if config.exists(base + ['ipv6', 'name', ipv6name, 'default-action']): action = config.return_value(base + ['ipv6', 'name', ipv6name, 'default-action']) if action == 'accept': config.set(base + ['ipv6', 'name', ipv6name, 'default-action'], value='return') if config.exists(base + ['ipv6', 'name', ipv6name, 'rule']): for rule_id in config.list_nodes(base + ['ipv6', 'name', ipv6name, 'rule']): action = config.return_value(base + ['ipv6', 'name', ipv6name, 'rule', rule_id, 'action']) if action == 'accept': config.set(base + ['ipv6', 'name', ipv6name, 'rule', rule_id, 'action'], value='return') config.delete(base + ['ipv6-name']) ### Migration of firewall interface if config.exists(base + ['interface']): fwd_ipv4_rule = 5 inp_ipv4_rule = 5 fwd_ipv6_rule = 5 inp_ipv6_rule = 5 for direction in ['in', 'out', 'local']: for iface in config.list_nodes(base + ['interface']): if config.exists(base + ['interface', iface, direction]): if config.exists(base + ['interface', iface, direction, 'name']): target = config.return_value(base + ['interface', iface, direction, 'name']) if direction == 'in': # Add default-action== accept for compatibility reasons: config.set(base + ['ipv4', 'forward', 'filter', 'default-action'], value='accept') new_base = base + ['ipv4', 'forward', 'filter', 'rule'] config.set(new_base) config.set_tag(new_base) config.set(new_base + [fwd_ipv4_rule, 'inbound-interface', 'interface-name'], value=iface) config.set(new_base + [fwd_ipv4_rule, 'action'], value='jump') config.set(new_base + [fwd_ipv4_rule, 'jump-target'], value=target) fwd_ipv4_rule = fwd_ipv4_rule + 5 elif direction == 'out': # Add default-action== accept for compatibility reasons: config.set(base + ['ipv4', 'forward', 'filter', 'default-action'], value='accept') new_base = base + ['ipv4', 'forward', 'filter', 'rule'] config.set(new_base) config.set_tag(new_base) config.set(new_base + [fwd_ipv4_rule, 'outbound-interface', 'interface-name'], value=iface) config.set(new_base + [fwd_ipv4_rule, 'action'], value='jump') config.set(new_base + [fwd_ipv4_rule, 'jump-target'], value=target) fwd_ipv4_rule = fwd_ipv4_rule + 5 else: # Add default-action== accept for compatibility reasons: config.set(base + ['ipv4', 'input', 'filter', 'default-action'], value='accept') new_base = base + ['ipv4', 'input', 'filter', 'rule'] config.set(new_base) config.set_tag(new_base) config.set(new_base + [inp_ipv4_rule, 'inbound-interface', 'interface-name'], value=iface) config.set(new_base + [inp_ipv4_rule, 'action'], value='jump') config.set(new_base + [inp_ipv4_rule, 'jump-target'], value=target) inp_ipv4_rule = inp_ipv4_rule + 5 if config.exists(base + ['interface', iface, direction, 'ipv6-name']): target = config.return_value(base + ['interface', iface, direction, 'ipv6-name']) if direction == 'in': # Add default-action== accept for compatibility reasons: config.set(base + ['ipv6', 'forward', 'filter', 'default-action'], value='accept') new_base = base + ['ipv6', 'forward', 'filter', 'rule'] config.set(new_base) config.set_tag(new_base) config.set(new_base + [fwd_ipv6_rule, 'inbound-interface', 'interface-name'], value=iface) config.set(new_base + [fwd_ipv6_rule, 'action'], value='jump') config.set(new_base + [fwd_ipv6_rule, 'jump-target'], value=target) fwd_ipv6_rule = fwd_ipv6_rule + 5 elif direction == 'out': # Add default-action== accept for compatibility reasons: config.set(base + ['ipv6', 'forward', 'filter', 'default-action'], value='accept') new_base = base + ['ipv6', 'forward', 'filter', 'rule'] config.set(new_base) config.set_tag(new_base) config.set(new_base + [fwd_ipv6_rule, 'outbound-interface', 'interface-name'], value=iface) config.set(new_base + [fwd_ipv6_rule, 'action'], value='jump') config.set(new_base + [fwd_ipv6_rule, 'jump-target'], value=target) fwd_ipv6_rule = fwd_ipv6_rule + 5 else: new_base = base + ['ipv6', 'input', 'filter', 'rule'] # Add default-action== accept for compatibility reasons: config.set(base + ['ipv6', 'input', 'filter', 'default-action'], value='accept') config.set(new_base) config.set_tag(new_base) config.set(new_base + [inp_ipv6_rule, 'inbound-interface', 'interface-name'], value=iface) config.set(new_base + [inp_ipv6_rule, 'action'], value='jump') config.set(new_base + [inp_ipv6_rule, 'jump-target'], value=target) inp_ipv6_rule = inp_ipv6_rule + 5 config.delete(base + ['interface']) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) - exit(1) \ No newline at end of file + exit(1) diff --git a/src/migration-scripts/firewall/11-to-12 b/src/migration-scripts/firewall/11-to-12 index ba8374d66..f9122e74c 100755 --- a/src/migration-scripts/firewall/11-to-12 +++ b/src/migration-scripts/firewall/11-to-12 @@ -1,74 +1,71 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # T5681: Firewall re-writing. Simplify cli when mathcing interface # From # set firewall ... rule <rule> [inbound-interface | outboubd-interface] interface-name <iface> # set firewall ... rule <rule> [inbound-interface | outboubd-interface] interface-group <iface_group> # To # set firewall ... rule <rule> [inbound-interface | outboubd-interface] name <iface> # set firewall ... rule <rule> [inbound-interface | outboubd-interface] group <iface_group> -import re - from sys import argv from sys import exit from vyos.configtree import ConfigTree -from vyos.ifconfig import Section if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() base = ['firewall'] config = ConfigTree(config_file) if not config.exists(base): # Nothing to do exit(0) ## Migration from base chains #if config.exists(base + ['interface', iface, direction]): for family in ['ipv4', 'ipv6']: if config.exists(base + [family]): for hook in ['forward', 'input', 'output', 'name']: if config.exists(base + [family, hook]): for priority in config.list_nodes(base + [family, hook]): if config.exists(base + [family, hook, priority, 'rule']): for rule in config.list_nodes(base + [family, hook, priority, 'rule']): for direction in ['inbound-interface', 'outbound-interface']: if config.exists(base + [family, hook, priority, 'rule', rule, direction]): if config.exists(base + [family, hook, priority, 'rule', rule, direction, 'interface-name']): iface = config.return_value(base + [family, hook, priority, 'rule', rule, direction, 'interface-name']) config.set(base + [family, hook, priority, 'rule', rule, direction, 'name'], value=iface) config.delete(base + [family, hook, priority, 'rule', rule, direction, 'interface-name']) elif config.exists(base + [family, hook, priority, 'rule', rule, direction, 'interface-group']): group = config.return_value(base + [family, hook, priority, 'rule', rule, direction, 'interface-group']) config.set(base + [family, hook, priority, 'rule', rule, direction, 'group'], value=group) config.delete(base + [family, hook, priority, 'rule', rule, direction, 'interface-group']) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) - exit(1) \ No newline at end of file + exit(1) diff --git a/src/migration-scripts/firewall/12-to-13 b/src/migration-scripts/firewall/12-to-13 index 8396dd9d1..d72ba834d 100755 --- a/src/migration-scripts/firewall/12-to-13 +++ b/src/migration-scripts/firewall/12-to-13 @@ -1,92 +1,89 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # T5729: Switch to valueless whenever is possible. # From # set firewall ... rule <rule> log enable # set firewall ... rule <rule> state <state> enable # set firewall ... rule <rule> log disable # set firewall ... rule <rule> state <state> disable # To # set firewall ... rule <rule> log # set firewall ... rule <rule> state <state> # Remove command if log=disable or <state>=disable -import re - from sys import argv from sys import exit from vyos.configtree import ConfigTree -from vyos.ifconfig import Section if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() base = ['firewall'] config = ConfigTree(config_file) if not config.exists(base): # Nothing to do exit(0) # State Policy logs: if config.exists(base + ['global-options', 'state-policy']): for state in config.list_nodes(base + ['global-options', 'state-policy']): if config.exists(base + ['global-options', 'state-policy', state, 'log']): log_value = config.return_value(base + ['global-options', 'state-policy', state, 'log']) config.delete(base + ['global-options', 'state-policy', state, 'log']) if log_value == 'enable': config.set(base + ['global-options', 'state-policy', state, 'log']) for family in ['ipv4', 'ipv6', 'bridge']: if config.exists(base + [family]): for hook in ['forward', 'input', 'output', 'name']: if config.exists(base + [family, hook]): for priority in config.list_nodes(base + [family, hook]): if config.exists(base + [family, hook, priority, 'rule']): for rule in config.list_nodes(base + [family, hook, priority, 'rule']): # Log if config.exists(base + [family, hook, priority, 'rule', rule, 'log']): log_value = config.return_value(base + [family, hook, priority, 'rule', rule, 'log']) config.delete(base + [family, hook, priority, 'rule', rule, 'log']) if log_value == 'enable': config.set(base + [family, hook, priority, 'rule', rule, 'log']) # State if config.exists(base + [family, hook, priority, 'rule', rule, 'state']): flag_enable = 'False' for state in ['established', 'invalid', 'new', 'related']: if config.exists(base + [family, hook, priority, 'rule', rule, 'state', state]): state_value = config.return_value(base + [family, hook, priority, 'rule', rule, 'state', state]) config.delete(base + [family, hook, priority, 'rule', rule, 'state', state]) if state_value == 'enable': config.set(base + [family, hook, priority, 'rule', rule, 'state'], value=state, replace=False) flag_enable = 'True' if flag_enable == 'False': config.delete(base + [family, hook, priority, 'rule', rule, 'state']) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) - exit(1) \ No newline at end of file + exit(1) diff --git a/src/migration-scripts/firewall/7-to-8 b/src/migration-scripts/firewall/7-to-8 index d06c3150a..bbaba113a 100755 --- a/src/migration-scripts/firewall/7-to-8 +++ b/src/migration-scripts/firewall/7-to-8 @@ -1,98 +1,95 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2022-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # T2199: Migrate interface firewall nodes to firewall interfaces <ifname> <direction> name/ipv6-name <name> # T2199: Migrate zone-policy to firewall node -import re - from sys import argv from sys import exit from vyos.configtree import ConfigTree -from vyos.ifconfig import Section if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() base = ['firewall'] zone_base = ['zone-policy'] config = ConfigTree(config_file) if not config.exists(base) and not config.exists(zone_base): # Nothing to do exit(0) def migrate_interface(config, iftype, ifname, vif=None, vifs=None, vifc=None): if_path = ['interfaces', iftype, ifname] ifname_full = ifname if vif: if_path += ['vif', vif] ifname_full = f'{ifname}.{vif}' elif vifs: if_path += ['vif-s', vifs] ifname_full = f'{ifname}.{vifs}' if vifc: if_path += ['vif-c', vifc] ifname_full = f'{ifname}.{vifs}.{vifc}' if not config.exists(if_path + ['firewall']): return if not config.exists(['firewall', 'interface']): config.set(['firewall', 'interface']) config.set_tag(['firewall', 'interface']) config.copy(if_path + ['firewall'], ['firewall', 'interface', ifname_full]) config.delete(if_path + ['firewall']) for iftype in config.list_nodes(['interfaces']): for ifname in config.list_nodes(['interfaces', iftype]): migrate_interface(config, iftype, ifname) if config.exists(['interfaces', iftype, ifname, 'vif']): for vif in config.list_nodes(['interfaces', iftype, ifname, 'vif']): migrate_interface(config, iftype, ifname, vif=vif) if config.exists(['interfaces', iftype, ifname, 'vif-s']): for vifs in config.list_nodes(['interfaces', iftype, ifname, 'vif-s']): migrate_interface(config, iftype, ifname, vifs=vifs) if config.exists(['interfaces', iftype, ifname, 'vif-s', vifs, 'vif-c']): for vifc in config.list_nodes(['interfaces', iftype, ifname, 'vif-s', vifs, 'vif-c']): migrate_interface(config, iftype, ifname, vifs=vifs, vifc=vifc) if config.exists(zone_base + ['zone']): config.set(['firewall', 'zone']) config.set_tag(['firewall', 'zone']) for zone in config.list_nodes(zone_base + ['zone']): config.copy(zone_base + ['zone', zone], ['firewall', 'zone', zone]) config.delete(zone_base) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/firewall/8-to-9 b/src/migration-scripts/firewall/8-to-9 index d7647354a..6e019beb2 100755 --- a/src/migration-scripts/firewall/8-to-9 +++ b/src/migration-scripts/firewall/8-to-9 @@ -1,91 +1,88 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2022-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # T4780: Add firewall interface group -# cli changes from: +# cli changes from: # set firewall [name | ipv6-name] <name> rule <number> [inbound-interface | outbound-interface] <interface_name> # To # set firewall [name | ipv6-name] <name> rule <number> [inbound-interface | outbound-interface] [interface-name | interface-group] <interface_name | interface_group> -import re - from sys import argv from sys import exit from vyos.configtree import ConfigTree -from vyos.ifconfig import Section if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() base = ['firewall'] config = ConfigTree(config_file) if not config.exists(base): # Nothing to do exit(0) if config.exists(base + ['name']): for name in config.list_nodes(base + ['name']): if not config.exists(base + ['name', name, 'rule']): continue for rule in config.list_nodes(base + ['name', name, 'rule']): rule_iiface = base + ['name', name, 'rule', rule, 'inbound-interface'] rule_oiface = base + ['name', name, 'rule', rule, 'outbound-interface'] if config.exists(rule_iiface): tmp = config.return_value(rule_iiface) config.delete(rule_iiface) config.set(rule_iiface + ['interface-name'], value=tmp) if config.exists(rule_oiface): tmp = config.return_value(rule_oiface) config.delete(rule_oiface) config.set(rule_oiface + ['interface-name'], value=tmp) if config.exists(base + ['ipv6-name']): for name in config.list_nodes(base + ['ipv6-name']): if not config.exists(base + ['ipv6-name', name, 'rule']): continue for rule in config.list_nodes(base + ['ipv6-name', name, 'rule']): rule_iiface = base + ['ipv6-name', name, 'rule', rule, 'inbound-interface'] rule_oiface = base + ['ipv6-name', name, 'rule', rule, 'outbound-interface'] if config.exists(rule_iiface): tmp = config.return_value(rule_iiface) config.delete(rule_iiface) config.set(rule_iiface + ['interface-name'], value=tmp) if config.exists(rule_oiface): tmp = config.return_value(rule_oiface) config.delete(rule_oiface) config.set(rule_oiface + ['interface-name'], value=tmp) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) - exit(1) \ No newline at end of file + exit(1) diff --git a/src/migration-scripts/firewall/9-to-10 b/src/migration-scripts/firewall/9-to-10 index a70460718..ce509a731 100755 --- a/src/migration-scripts/firewall/9-to-10 +++ b/src/migration-scripts/firewall/9-to-10 @@ -1,80 +1,77 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # T5050: Log options -# cli changes from: +# cli changes from: # set firewall [name | ipv6-name] <name> rule <number> log-level <log_level> # To # set firewall [name | ipv6-name] <name> rule <number> log-options level <log_level> -import re - from sys import argv from sys import exit from vyos.configtree import ConfigTree -from vyos.ifconfig import Section if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() base = ['firewall'] config = ConfigTree(config_file) if not config.exists(base): # Nothing to do exit(0) if config.exists(base + ['name']): for name in config.list_nodes(base + ['name']): if not config.exists(base + ['name', name, 'rule']): continue for rule in config.list_nodes(base + ['name', name, 'rule']): - log_options_base = base + ['name', name, 'rule', rule, 'log-options'] + log_options_base = base + ['name', name, 'rule', rule, 'log-options'] rule_log_level = base + ['name', name, 'rule', rule, 'log-level'] if config.exists(rule_log_level): tmp = config.return_value(rule_log_level) config.delete(rule_log_level) config.set(log_options_base + ['level'], value=tmp) if config.exists(base + ['ipv6-name']): for name in config.list_nodes(base + ['ipv6-name']): if not config.exists(base + ['ipv6-name', name, 'rule']): continue for rule in config.list_nodes(base + ['ipv6-name', name, 'rule']): - log_options_base = base + ['ipv6-name', name, 'rule', rule, 'log-options'] + log_options_base = base + ['ipv6-name', name, 'rule', rule, 'log-options'] rule_log_level = base + ['ipv6-name', name, 'rule', rule, 'log-level'] if config.exists(rule_log_level): tmp = config.return_value(rule_log_level) config.delete(rule_log_level) config.set(log_options_base + ['level'], value=tmp) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) - exit(1) \ No newline at end of file + exit(1) diff --git a/src/migration-scripts/interfaces/26-to-27 b/src/migration-scripts/interfaces/26-to-27 index 4967a29fa..429ab650f 100755 --- a/src/migration-scripts/interfaces/26-to-27 +++ b/src/migration-scripts/interfaces/26-to-27 @@ -1,54 +1,52 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2022-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # T4384: pppoe: replace default-route CLI option with common CLI nodes already # present for DHCP from sys import argv - -from vyos.ethtool import Ethtool from vyos.configtree import ConfigTree if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() base = ['interfaces', 'pppoe'] config = ConfigTree(config_file) if not config.exists(base): exit(0) for ifname in config.list_nodes(base): tmp_config = base + [ifname, 'default-route'] if config.exists(tmp_config): # Retrieve current config value value = config.return_value(tmp_config) # Delete old Config node config.delete(tmp_config) if value == 'none': config.set(base + [ifname, 'no-default-route']) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print(f'Failed to save the modified config: {e}') exit(1) diff --git a/src/migration-scripts/interfaces/27-to-28 b/src/migration-scripts/interfaces/27-to-28 index a0d043d11..9f5e93b5f 100755 --- a/src/migration-scripts/interfaces/27-to-28 +++ b/src/migration-scripts/interfaces/27-to-28 @@ -1,49 +1,48 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # T4995: pppoe, wwan, sstpc-client rename "authentication user" CLI node # to "authentication username" from sys import argv -from vyos.ethtool import Ethtool from vyos.configtree import ConfigTree if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) for type in ['pppoe', 'sstpc-client', 'wwam']: base = ['interfaces', type] if not config.exists(base): continue for interface in config.list_nodes(base): auth_base = base + [interface, 'authentication', 'user'] if config.exists(auth_base): config.rename(auth_base, 'username') try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print(f'Failed to save the modified config: {e}') exit(1) diff --git a/src/migration-scripts/interfaces/28-to-29 b/src/migration-scripts/interfaces/28-to-29 index ad5bfa653..0437977dc 100755 --- a/src/migration-scripts/interfaces/28-to-29 +++ b/src/migration-scripts/interfaces/28-to-29 @@ -1,54 +1,52 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # T5034: tunnel: rename "multicast enable" CLI node to "enable-multicast" # valueless node. from sys import argv - -from vyos.ethtool import Ethtool from vyos.configtree import ConfigTree if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() base = ['interfaces', 'tunnel'] config = ConfigTree(config_file) if not config.exists(base): exit(0) for ifname in config.list_nodes(base): multicast_base = base + [ifname, 'multicast'] if config.exists(multicast_base): tmp = config.return_value(multicast_base) print(tmp) # Delete old Config node config.delete(multicast_base) if tmp == 'enable': config.set(base + [ifname, 'enable-multicast']) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print(f'Failed to save the modified config: {e}') exit(1) diff --git a/src/migration-scripts/interfaces/29-to-30 b/src/migration-scripts/interfaces/29-to-30 index acb6ee1fb..80aad1d44 100755 --- a/src/migration-scripts/interfaces/29-to-30 +++ b/src/migration-scripts/interfaces/29-to-30 @@ -1,49 +1,47 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # T5286: remove XDP support in favour of VPP from sys import argv - -from vyos.ethtool import Ethtool from vyos.configtree import ConfigTree if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() supports_xdp = ['bonding', 'ethernet'] config = ConfigTree(config_file) for if_type in supports_xdp: base = ['interfaces', if_type] if not config.exists(base): continue for interface in config.list_nodes(base): if_base = base + [interface] if config.exists(if_base + ['xdp']): config.delete(if_base + ['xdp']) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print(f'Failed to save the modified config: {e}') exit(1) diff --git a/src/migration-scripts/ipoe-server/0-to-1 b/src/migration-scripts/ipoe-server/0-to-1 index ac9d13abc..a6dd46ac1 100755 --- a/src/migration-scripts/ipoe-server/0-to-1 +++ b/src/migration-scripts/ipoe-server/0-to-1 @@ -1,74 +1,71 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2022-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # - T4703: merge vlan-id and vlan-range to vlan CLI node # L2|L3 -> l2|l3 # mac-address -> mac # network-mode -> mode -import os -import sys - from sys import argv, exit from vyos.configtree import ConfigTree if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) base = ['service', 'ipoe-server'] if not config.exists(base): # Nothing to do exit(0) if config.exists(base + ['authentication', 'interface']): for interface in config.list_nodes(base + ['authentication', 'interface']): config.rename(base + ['authentication', 'interface', interface, 'mac-address'], 'mac') mac_base = base + ['authentication', 'interface', interface, 'mac'] for mac in config.list_nodes(mac_base): vlan_config = mac_base + [mac, 'vlan-id'] if config.exists(vlan_config): config.rename(vlan_config, 'vlan') for interface in config.list_nodes(base + ['interface']): base_path = base + ['interface', interface] for vlan in ['vlan-id', 'vlan-range']: if config.exists(base_path + [vlan]): print(interface, vlan) for tmp in config.return_values(base_path + [vlan]): config.set(base_path + ['vlan'], value=tmp, replace=False) config.delete(base_path + [vlan]) if config.exists(base_path + ['network-mode']): tmp = config.return_value(base_path + ['network-mode']) config.delete(base_path + ['network-mode']) # Change L2|L3 to lower case l2|l3 config.set(base_path + ['mode'], value=tmp.lower()) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/ipoe-server/1-to-2 b/src/migration-scripts/ipoe-server/1-to-2 index 11d7911e9..378702693 100755 --- a/src/migration-scripts/ipoe-server/1-to-2 +++ b/src/migration-scripts/ipoe-server/1-to-2 @@ -1,87 +1,85 @@ #!/usr/bin/env python3 # # Copyright (C) 2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # - changed cli of all named pools # - moved gateway-address from pool to global configuration with / netmask # gateway can exist without pool if radius is used # and Framed-ip-address is transmited # - There are several gateway-addresses in ipoe # - default-pool by migration. # 1. The first pool that contains next-poll. # 2. Else, the first pool in the list -import os - from sys import argv from sys import exit from vyos.configtree import ConfigTree if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) base = ['service', 'ipoe-server'] pool_base = base + ['client-ip-pool'] if not config.exists(base): exit(0) if not config.exists(pool_base): exit(0) default_pool = '' gateway = '' #named pool migration namedpools_base = pool_base + ['name'] for pool_name in config.list_nodes(namedpools_base): pool_path = namedpools_base + [pool_name] if config.exists(pool_path + ['subnet']): subnet = config.return_value(pool_path + ['subnet']) config.set(pool_base + [pool_name, 'range'], value=subnet, replace=False) # Get netmask from subnet mask = subnet.split("/")[1] if config.exists(pool_path + ['next-pool']): next_pool = config.return_value(pool_path + ['next-pool']) config.set(pool_base + [pool_name, 'next-pool'], value=next_pool) if not default_pool: default_pool = pool_name if config.exists(pool_path + ['gateway-address']) and mask: gateway = f'{config.return_value(pool_path + ["gateway-address"])}/{mask}' config.set(base + ['gateway-address'], value=gateway, replace=False) if not default_pool and config.list_nodes(namedpools_base): default_pool = config.list_nodes(namedpools_base)[0] config.delete(namedpools_base) if default_pool: config.set(base + ['default-pool'], value=default_pool) # format as tag node config.set_tag(pool_base) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/ipoe-server/2-to-3 b/src/migration-scripts/ipoe-server/2-to-3 index d4ae0a7ba..0909315a8 100755 --- a/src/migration-scripts/ipoe-server/2-to-3 +++ b/src/migration-scripts/ipoe-server/2-to-3 @@ -1,61 +1,58 @@ #!/usr/bin/env python3 # # Copyright (C) 2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # Migrating to named ipv6 pools -import os - from sys import argv from sys import exit from vyos.configtree import ConfigTree - if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) base = ['service', 'ipoe-server'] pool_base = base + ['client-ipv6-pool'] if not config.exists(base): exit(0) if not config.exists(pool_base): exit(0) ipv6_pool_name = 'ipv6-pool' config.copy(pool_base, pool_base + [ipv6_pool_name]) if config.exists(pool_base + ['prefix']): config.delete(pool_base + ['prefix']) config.set(base + ['default-ipv6-pool'], value=ipv6_pool_name) if config.exists(pool_base + ['delegate']): config.delete(pool_base + ['delegate']) # format as tag node config.set_tag(pool_base) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/ipsec/11-to-12 b/src/migration-scripts/ipsec/11-to-12 index e34882c23..4833d0876 100755 --- a/src/migration-scripts/ipsec/11-to-12 +++ b/src/migration-scripts/ipsec/11-to-12 @@ -1,53 +1,51 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # Remove legacy ipsec.conf and ipsec.secrets - Not supported with swanctl -import re - from sys import argv from sys import exit from vyos.configtree import ConfigTree if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() base = ['vpn', 'ipsec'] config = ConfigTree(config_file) if not config.exists(base): # Nothing to do exit(0) if config.exists(base + ['include-ipsec-conf']): config.delete(base + ['include-ipsec-conf']) if config.exists(base + ['include-ipsec-secrets']): config.delete(base + ['include-ipsec-secrets']) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print(f'Failed to save the modified config: {e}') exit(1) diff --git a/src/migration-scripts/ipsec/12-to-13 b/src/migration-scripts/ipsec/12-to-13 index c11f708bd..d90c70314 100755 --- a/src/migration-scripts/ipsec/12-to-13 +++ b/src/migration-scripts/ipsec/12-to-13 @@ -1,59 +1,57 @@ #!/usr/bin/env python3 # # Copyright (C) 2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # Changed value of dead-peer-detection.action from hold to trap # Changed value of close-action from hold to trap and from restart to start -import re - from sys import argv from sys import exit from vyos.configtree import ConfigTree if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() base = ['vpn', 'ipsec', 'ike-group'] config = ConfigTree(config_file) if not config.exists(base): # Nothing to do exit(0) else: for ike_group in config.list_nodes(base): base_dpd_action = base + [ike_group, 'dead-peer-detection', 'action'] base_close_action = base + [ike_group, 'close-action'] if config.exists(base_dpd_action) and config.return_value(base_dpd_action) == 'hold': config.set(base_dpd_action, 'trap', replace=True) if config.exists(base_close_action): if config.return_value(base_close_action) == 'hold': config.set(base_close_action, 'trap', replace=True) if config.return_value(base_close_action) == 'restart': config.set(base_close_action, 'start', replace=True) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print(f'Failed to save the modified config: {e}') exit(1) diff --git a/src/migration-scripts/ipsec/7-to-8 b/src/migration-scripts/ipsec/7-to-8 index e002db0b1..9acc737d5 100755 --- a/src/migration-scripts/ipsec/7-to-8 +++ b/src/migration-scripts/ipsec/7-to-8 @@ -1,125 +1,124 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # Migrate rsa keys into PKI configuration import base64 import os import struct from cryptography.hazmat.primitives.asymmetric import rsa from sys import argv from sys import exit from vyos.configtree import ConfigTree -from vyos.pki import load_public_key from vyos.pki import load_private_key from vyos.pki import encode_public_key from vyos.pki import encode_private_key if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() pki_base = ['pki'] ipsec_site_base = ['vpn', 'ipsec', 'site-to-site', 'peer'] rsa_keys_base = ['vpn', 'rsa-keys'] config = ConfigTree(config_file) LOCAL_KEY_PATHS = ['/config/auth/', '/config/ipsec.d/rsa-keys/'] def migrate_from_vyatta_key(data): data = base64.b64decode(data[2:]) length = struct.unpack('B', data[:1])[0] e = int.from_bytes(data[1:1+length], 'big') n = int.from_bytes(data[1+length:], 'big') public_numbers = rsa.RSAPublicNumbers(e, n) return public_numbers.public_key() def wrapped_pem_to_config_value(pem): return "".join(pem.strip().split("\n")[1:-1]) local_key_name = 'localhost' if config.exists(rsa_keys_base): if not config.exists(pki_base + ['key-pair']): config.set(pki_base + ['key-pair']) config.set_tag(pki_base + ['key-pair']) if config.exists(rsa_keys_base + ['local-key', 'file']): local_file = config.return_value(rsa_keys_base + ['local-key', 'file']) local_path = None local_key = None for path in LOCAL_KEY_PATHS: full_path = os.path.join(path, local_file) if os.path.exists(full_path): local_path = full_path break if local_path: with open(local_path, 'r') as f: local_key_data = f.read() local_key = load_private_key(local_key_data, wrap_tags=False) if local_key: local_key_pem = encode_private_key(local_key) config.set(pki_base + ['key-pair', local_key_name, 'private', 'key'], value=wrapped_pem_to_config_value(local_key_pem)) else: print('Failed to migrate local RSA key') if config.exists(rsa_keys_base + ['rsa-key-name']): for rsa_name in config.list_nodes(rsa_keys_base + ['rsa-key-name']): if not config.exists(rsa_keys_base + ['rsa-key-name', rsa_name, 'rsa-key']): continue vyatta_key = config.return_value(rsa_keys_base + ['rsa-key-name', rsa_name, 'rsa-key']) public_key = migrate_from_vyatta_key(vyatta_key) if public_key: public_key_pem = encode_public_key(public_key) config.set(pki_base + ['key-pair', rsa_name, 'public', 'key'], value=wrapped_pem_to_config_value(public_key_pem)) else: print(f'Failed to migrate rsa-key "{rsa_name}"') config.delete(rsa_keys_base) if config.exists(ipsec_site_base): for peer in config.list_nodes(ipsec_site_base): mode = config.return_value(ipsec_site_base + [peer, 'authentication', 'mode']) if mode != 'rsa': continue config.set(ipsec_site_base + [peer, 'authentication', 'rsa', 'local-key'], value=local_key_name) remote_key_name = config.return_value(ipsec_site_base + [peer, 'authentication', 'rsa-key-name']) config.set(ipsec_site_base + [peer, 'authentication', 'rsa', 'remote-key'], value=remote_key_name) config.delete(ipsec_site_base + [peer, 'authentication', 'rsa-key-name']) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) sys.exit(1) diff --git a/src/migration-scripts/ipsec/9-to-10 b/src/migration-scripts/ipsec/9-to-10 index a4a71d38e..bc10e1997 100755 --- a/src/migration-scripts/ipsec/9-to-10 +++ b/src/migration-scripts/ipsec/9-to-10 @@ -1,134 +1,131 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2022-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import re from sys import argv from sys import exit from vyos.configtree import ConfigTree -from vyos.template import is_ipv4 -from vyos.template import is_ipv6 - if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() base = ['vpn', 'ipsec'] config = ConfigTree(config_file) if not config.exists(base): # Nothing to do exit(0) # IKE changes, T4118: if config.exists(base + ['ike-group']): for ike_group in config.list_nodes(base + ['ike-group']): # replace 'ipsec ike-group <tag> mobike disable' # => 'ipsec ike-group <tag> disable-mobike' mobike = base + ['ike-group', ike_group, 'mobike'] if config.exists(mobike): if config.return_value(mobike) == 'disable': config.set(base + ['ike-group', ike_group, 'disable-mobike']) config.delete(mobike) # replace 'ipsec ike-group <tag> ikev2-reauth yes' # => 'ipsec ike-group <tag> ikev2-reauth' reauth = base + ['ike-group', ike_group, 'ikev2-reauth'] if config.exists(reauth): if config.return_value(reauth) == 'yes': config.delete(reauth) config.set(reauth) else: config.delete(reauth) # ESP changes # replace 'ipsec esp-group <tag> compression enable' # => 'ipsec esp-group <tag> compression' if config.exists(base + ['esp-group']): for esp_group in config.list_nodes(base + ['esp-group']): compression = base + ['esp-group', esp_group, 'compression'] if config.exists(compression): if config.return_value(compression) == 'enable': config.delete(compression) config.set(compression) else: config.delete(compression) # PEER changes if config.exists(base + ['site-to-site', 'peer']): for peer in config.list_nodes(base + ['site-to-site', 'peer']): peer_base = base + ['site-to-site', 'peer', peer] # replace: 'peer <tag> id x' # => 'peer <tag> local-id x' if config.exists(peer_base + ['authentication', 'id']): config.rename(peer_base + ['authentication', 'id'], 'local-id') # For the peer '@foo' set remote-id 'foo' if remote-id is not defined # For the peer '192.0.2.1' set remote-id '192.0.2.1' if remote-id is not defined if not config.exists(peer_base + ['authentication', 'remote-id']): tmp = peer.replace('@', '') if peer.startswith('@') else peer config.set(peer_base + ['authentication', 'remote-id'], value=tmp) # replace: 'peer <tag> force-encapsulation enable' # => 'peer <tag> force-udp-encapsulation' force_enc = peer_base + ['force-encapsulation'] if config.exists(force_enc): if config.return_value(force_enc) == 'enable': config.delete(force_enc) config.set(peer_base + ['force-udp-encapsulation']) else: config.delete(force_enc) # add option: 'peer <tag> remote-address x.x.x.x' remote_address = peer if peer.startswith('@'): remote_address = 'any' config.set(peer_base + ['remote-address'], value=remote_address) # Peer name it is swanctl connection name and shouldn't contain dots or colons # rename peer: # peer 192.0.2.1 => peer peer_192-0-2-1 # peer 2001:db8::2 => peer peer_2001-db8--2 # peer @foo => peer peer_foo re_peer_name = re.sub(':|\.', '-', peer) if re_peer_name.startswith('@'): re_peer_name = re.sub('@', '', re_peer_name) new_peer_name = f'peer_{re_peer_name}' config.rename(peer_base, new_peer_name) # remote-access/road-warrior changes if config.exists(base + ['remote-access', 'connection']): for connection in config.list_nodes(base + ['remote-access', 'connection']): ra_base = base + ['remote-access', 'connection', connection] # replace: 'remote-access connection <tag> authentication id x' # => 'remote-access connection <tag> authentication local-id x' if config.exists(ra_base + ['authentication', 'id']): config.rename(ra_base + ['authentication', 'id'], 'local-id') try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print(f'Failed to save the modified config: {e}') exit(1) diff --git a/src/migration-scripts/l2tp/2-to-3 b/src/migration-scripts/l2tp/2-to-3 index b46b0f22e..8527c2d4a 100755 --- a/src/migration-scripts/l2tp/2-to-3 +++ b/src/migration-scripts/l2tp/2-to-3 @@ -1,111 +1,107 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # - remove primary/secondary identifier from nameserver # - TODO: remove radius server req-limit -import os -import sys - from sys import argv, exit from vyos.configtree import ConfigTree if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) base = ['vpn', 'l2tp', 'remote-access'] if not config.exists(base): # Nothing to do exit(0) else: - # Migrate IPv4 DNS servers dns_base = base + ['dns-servers'] if config.exists(dns_base): for server in ['server-1', 'server-2']: if config.exists(dns_base + [server]): dns = config.return_value(dns_base + [server]) config.set(base + ['name-server'], value=dns, replace=False) config.delete(dns_base) # Migrate IPv6 DNS servers dns_base = base + ['dnsv6-servers'] if config.exists(dns_base): for server in config.return_values(dns_base): config.set(base + ['name-server'], value=server, replace=False) config.delete(dns_base) # Migrate IPv4 WINS servers wins_base = base + ['wins-servers'] if config.exists(wins_base): for server in ['server-1', 'server-2']: if config.exists(wins_base + [server]): wins = config.return_value(wins_base + [server]) config.set(base + ['wins-server'], value=wins, replace=False) config.delete(wins_base) # Remove RADIUS server req-limit node radius_base = base + ['authentication', 'radius'] if config.exists(radius_base): for server in config.list_nodes(radius_base + ['server']): if config.exists(radius_base + ['server', server, 'req-limit']): config.delete(radius_base + ['server', server, 'req-limit']) # Migrate IPv6 prefixes ipv6_base = base + ['client-ipv6-pool'] if config.exists(ipv6_base + ['prefix']): prefix_old = config.return_values(ipv6_base + ['prefix']) # delete old prefix CLI nodes config.delete(ipv6_base + ['prefix']) # create ned prefix tag node config.set(ipv6_base + ['prefix']) config.set_tag(ipv6_base + ['prefix']) for p in prefix_old: prefix = p.split(',')[0] mask = p.split(',')[1] config.set(ipv6_base + ['prefix', prefix, 'mask'], value=mask) if config.exists(ipv6_base + ['delegate-prefix']): prefix_old = config.return_values(ipv6_base + ['delegate-prefix']) # delete old delegate prefix CLI nodes config.delete(ipv6_base + ['delegate-prefix']) # create ned delegation tag node config.set(ipv6_base + ['delegate']) config.set_tag(ipv6_base + ['delegate']) for p in prefix_old: prefix = p.split(',')[0] mask = p.split(',')[1] config.set(ipv6_base + ['delegate', prefix, 'delegate-prefix'], value=mask) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/l2tp/3-to-4 b/src/migration-scripts/l2tp/3-to-4 index 8c2b909b7..14b86ff04 100755 --- a/src/migration-scripts/l2tp/3-to-4 +++ b/src/migration-scripts/l2tp/3-to-4 @@ -1,169 +1,168 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # - remove primary/secondary identifier from nameserver # - TODO: remove radius server req-limit import os from sys import argv from sys import exit from vyos.configtree import ConfigTree from vyos.pki import load_certificate -from vyos.pki import load_crl from vyos.pki import load_private_key from vyos.pki import encode_certificate from vyos.pki import encode_private_key from vyos.utils.process import run if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) base = ['vpn', 'l2tp', 'remote-access', 'ipsec-settings'] pki_base = ['pki'] if not config.exists(base): exit(0) AUTH_DIR = '/config/auth' def wrapped_pem_to_config_value(pem): return "".join(pem.strip().split("\n")[1:-1]) if not config.exists(base + ['authentication', 'x509']): exit(0) x509_base = base + ['authentication', 'x509'] pki_name = 'l2tp_remote_access' if not config.exists(pki_base + ['ca']): config.set(pki_base + ['ca']) config.set_tag(pki_base + ['ca']) if not config.exists(pki_base + ['certificate']): config.set(pki_base + ['certificate']) config.set_tag(pki_base + ['certificate']) if config.exists(x509_base + ['ca-cert-file']): cert_file = config.return_value(x509_base + ['ca-cert-file']) cert_path = os.path.join(AUTH_DIR, cert_file) cert = None if os.path.isfile(cert_path): if not os.access(cert_path, os.R_OK): run(f'sudo chmod 644 {cert_path}') with open(cert_path, 'r') as f: cert_data = f.read() cert = load_certificate(cert_data, wrap_tags=False) if cert: cert_pem = encode_certificate(cert) config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) config.set(x509_base + ['ca-certificate'], value=pki_name) else: print(f'Failed to migrate CA certificate on l2tp remote-access config') config.delete(x509_base + ['ca-cert-file']) if config.exists(x509_base + ['crl-file']): crl_file = config.return_value(x509_base + ['crl-file']) crl_path = os.path.join(AUTH_DIR, crl_file) crl = None if os.path.isfile(crl_path): if not os.access(crl_path, os.R_OK): run(f'sudo chmod 644 {crl_path}') with open(crl_path, 'r') as f: crl_data = f.read() crl = load_certificate(crl_data, wrap_tags=False) if crl: crl_pem = encode_certificate(crl) config.set(pki_base + ['ca', pki_name, 'crl'], value=wrapped_pem_to_config_value(crl_pem)) else: print(f'Failed to migrate CRL on l2tp remote-access config') config.delete(x509_base + ['crl-file']) if config.exists(x509_base + ['server-cert-file']): cert_file = config.return_value(x509_base + ['server-cert-file']) cert_path = os.path.join(AUTH_DIR, cert_file) cert = None if os.path.isfile(cert_path): if not os.access(cert_path, os.R_OK): run(f'sudo chmod 644 {cert_path}') with open(cert_path, 'r') as f: cert_data = f.read() cert = load_certificate(cert_data, wrap_tags=False) if cert: cert_pem = encode_certificate(cert) config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) config.set(x509_base + ['certificate'], value=pki_name) else: print(f'Failed to migrate certificate on l2tp remote-access config') config.delete(x509_base + ['server-cert-file']) if config.exists(x509_base + ['server-key-file']): key_file = config.return_value(x509_base + ['server-key-file']) key_passphrase = None if config.exists(x509_base + ['server-key-password']): key_passphrase = config.return_value(x509_base + ['server-key-password']) key_path = os.path.join(AUTH_DIR, key_file) key = None if os.path.isfile(key_path): if not os.access(key_path, os.R_OK): run(f'sudo chmod 644 {key_path}') with open(key_path, 'r') as f: key_data = f.read() key = load_private_key(key_data, passphrase=key_passphrase, wrap_tags=False) if key: key_pem = encode_private_key(key, passphrase=key_passphrase) config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem)) if key_passphrase: config.set(pki_base + ['certificate', pki_name, 'private', 'password-protected']) config.set(x509_base + ['private-key-passphrase'], value=key_passphrase) else: print(f'Failed to migrate private key on l2tp remote-access config') config.delete(x509_base + ['server-key-file']) if config.exists(x509_base + ['server-key-password']): config.delete(x509_base + ['server-key-password']) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/l2tp/4-to-5 b/src/migration-scripts/l2tp/4-to-5 index 3176f895a..b7f4d2677 100755 --- a/src/migration-scripts/l2tp/4-to-5 +++ b/src/migration-scripts/l2tp/4-to-5 @@ -1,87 +1,85 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # - move all pool to named pools # 'start-stop' migrate to namedpool 'default-range-pool' # 'subnet' migrate to namedpool 'default-subnet-pool' # 'default-subnet-pool' is the next pool for 'default-range-pool' -import os - from sys import argv from sys import exit from vyos.configtree import ConfigTree from vyos.base import Warning if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) base = ['vpn', 'l2tp', 'remote-access'] pool_base = base + ['client-ip-pool'] if not config.exists(base): exit(0) if not config.exists(pool_base): exit(0) default_pool = '' range_pool_name = 'default-range-pool' if config.exists(pool_base + ['start']) and config.exists(pool_base + ['stop']): def is_legalrange(ip1: str, ip2: str, mask: str): from ipaddress import IPv4Interface interface1 = IPv4Interface(f'{ip1}/{mask}') interface2 = IPv4Interface(f'{ip2}/{mask}') return interface1.network.network_address == interface2.network.network_address and interface2.ip > interface1.ip start_ip = config.return_value(pool_base + ['start']) stop_ip = config.return_value(pool_base + ['stop']) if is_legalrange(start_ip, stop_ip,'24'): ip_range = f'{start_ip}-{stop_ip}' config.set(pool_base + [range_pool_name, 'range'], value=ip_range, replace=False) default_pool = range_pool_name else: Warning( f'L2TP client-ip-pool range start-ip:{start_ip} and stop-ip:{stop_ip} can not be migrated.') config.delete(pool_base + ['start']) config.delete(pool_base + ['stop']) if config.exists(pool_base + ['subnet']): for subnet in config.return_values(pool_base + ['subnet']): config.set(pool_base + [range_pool_name, 'range'], value=subnet, replace=False) config.delete(pool_base + ['subnet']) default_pool = range_pool_name if default_pool: config.set(base + ['default-pool'], value=default_pool) # format as tag node config.set_tag(pool_base) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/l2tp/5-to-6 b/src/migration-scripts/l2tp/5-to-6 index ca0b13dcc..ac40b89c8 100755 --- a/src/migration-scripts/l2tp/5-to-6 +++ b/src/migration-scripts/l2tp/5-to-6 @@ -1,110 +1,106 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. - -import os - from sys import argv from sys import exit from vyos.configtree import ConfigTree - if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) base = ['vpn', 'l2tp', 'remote-access'] if not config.exists(base): exit(0) #migrate idle to ppp option lcp-echo-timeout idle_path = base + ['idle'] if config.exists(idle_path): config.set(base + ['ppp-options', 'lcp-echo-timeout'], value=config.return_value(idle_path)) config.delete(idle_path) #migrate mppe from authentication to ppp-otion mppe_path = base + ['authentication', 'mppe'] if config.exists(mppe_path): config.set(base + ['ppp-options', 'mppe'], value=config.return_value(mppe_path)) config.delete(mppe_path) #migrate require to protocol require_path = base + ['authentication', 'require'] if config.exists(require_path): protocols = list(config.return_values(require_path)) for protocol in protocols: config.set(base + ['authentication', 'protocols'], value=protocol, replace=False) config.delete(require_path) else: config.set(base + ['authentication', 'protocols'], value='mschap-v2') #migrate default gateway if not exist if not config.exists(base + ['gateway-address']): config.set(base + ['gateway-address'], value='10.255.255.0') #migrate authentication radius timeout rad_timeout_path = base + ['authentication', 'radius', 'timeout'] if config.exists(rad_timeout_path): if int(config.return_value(rad_timeout_path)) > 60: config.set(rad_timeout_path, value=60) #migrate authentication radius acct timeout rad_acct_timeout_path = base + ['authentication', 'radius', 'acct-timeout'] if config.exists(rad_acct_timeout_path): if int(config.return_value(rad_acct_timeout_path)) > 60: config.set(rad_acct_timeout_path,value=60) #migrate authentication radius max-try rad_max_try_path = base + ['authentication', 'radius', 'max-try'] if config.exists(rad_max_try_path): if int(config.return_value(rad_max_try_path)) > 20: config.set(rad_max_try_path, value=20) #migrate dae-server to dynamic-author dae_path_old = base + ['authentication', 'radius', 'dae-server'] dae_path_new = base + ['authentication', 'radius', 'dynamic-author'] if config.exists(dae_path_old + ['ip-address']): config.set(dae_path_new + ['server'], value=config.return_value(dae_path_old + ['ip-address'])) if config.exists(dae_path_old + ['port']): config.set(dae_path_new + ['port'], value=config.return_value(dae_path_old + ['port'])) if config.exists(dae_path_old + ['secret']): config.set(dae_path_new + ['key'], value=config.return_value(dae_path_old + ['secret'])) if config.exists(dae_path_old): config.delete(dae_path_old) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/l2tp/6-to-7 b/src/migration-scripts/l2tp/6-to-7 index f49c4ab08..1c536585c 100755 --- a/src/migration-scripts/l2tp/6-to-7 +++ b/src/migration-scripts/l2tp/6-to-7 @@ -1,60 +1,57 @@ #!/usr/bin/env python3 # # Copyright (C) 2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # Migrating to named ipv6 pools -import os - from sys import argv from sys import exit from vyos.configtree import ConfigTree - if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) base = ['vpn', 'l2tp', 'remote-access'] pool_base = base + ['client-ipv6-pool'] if not config.exists(base): exit(0) if not config.exists(pool_base): exit(0) ipv6_pool_name = 'ipv6-pool' config.copy(pool_base, pool_base + [ipv6_pool_name]) if config.exists(pool_base + ['prefix']): config.delete(pool_base + ['prefix']) config.set(base + ['default-ipv6-pool'], value=ipv6_pool_name) if config.exists(pool_base + ['delegate']): config.delete(pool_base + ['delegate']) # format as tag node config.set_tag(pool_base) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/l2tp/7-to-8 b/src/migration-scripts/l2tp/7-to-8 index 4956e1155..e429ed057 100755 --- a/src/migration-scripts/l2tp/7-to-8 +++ b/src/migration-scripts/l2tp/7-to-8 @@ -1,68 +1,65 @@ #!/usr/bin/env python3 # # Copyright (C) 2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # Migrate from 'ccp-disable' to 'ppp-options.disable-ccp' # Migration ipv6 options -import os - from sys import argv from sys import exit from vyos.configtree import ConfigTree - if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) base = ['vpn', 'l2tp', 'remote-access'] if not config.exists(base): exit(0) #CCP migration if config.exists(base + ['ccp-disable']): config.delete(base + ['ccp-disable']) config.set(base + ['ppp-options', 'disable-ccp']) #IPV6 options migrations if config.exists(base + ['ppp-options','ipv6-peer-intf-id']): intf_peer_id = config.return_value(base + ['ppp-options','ipv6-peer-intf-id']) if intf_peer_id == 'ipv4': intf_peer_id = 'ipv4-addr' config.set(base + ['ppp-options','ipv6-peer-interface-id'], value=intf_peer_id, replace=True) config.delete(base + ['ppp-options','ipv6-peer-intf-id']) if config.exists(base + ['ppp-options','ipv6-intf-id']): intf_id = config.return_value(base + ['ppp-options','ipv6-intf-id']) config.set(base + ['ppp-options','ipv6-interface-id'], value=intf_id, replace=True) config.delete(base + ['ppp-options','ipv6-intf-id']) if config.exists(base + ['ppp-options','ipv6-accept-peer-intf-id']): config.set(base + ['ppp-options','ipv6-accept-peer-interface-id']) config.delete(base + ['ppp-options','ipv6-accept-peer-intf-id']) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/l2tp/8-to-9 b/src/migration-scripts/l2tp/8-to-9 index e85a3892b..672180e25 100755 --- a/src/migration-scripts/l2tp/8-to-9 +++ b/src/migration-scripts/l2tp/8-to-9 @@ -1,49 +1,46 @@ #!/usr/bin/env python3 # # Copyright (C) 2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # Deleted 'dhcp-interface' from l2tp -import os - from sys import argv from sys import exit from vyos.configtree import ConfigTree - if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) base = ['vpn', 'l2tp', 'remote-access'] if not config.exists(base): exit(0) -#deleting unused dhcp-interface +# deleting unused dhcp-interface if config.exists(base + ['dhcp-interface']): config.delete(base + ['dhcp-interface']) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/openconnect/0-to-1 b/src/migration-scripts/openconnect/0-to-1 index 8be15fad1..c64b16cb2 100755 --- a/src/migration-scripts/openconnect/0-to-1 +++ b/src/migration-scripts/openconnect/0-to-1 @@ -1,136 +1,135 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # - Update SSL to use PKI configuration import os from sys import argv from sys import exit from vyos.configtree import ConfigTree from vyos.pki import load_certificate -from vyos.pki import load_crl from vyos.pki import load_private_key from vyos.pki import encode_certificate from vyos.pki import encode_private_key from vyos.utils.process import run if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) base = ['vpn', 'openconnect'] pki_base = ['pki'] if not config.exists(base): exit(0) AUTH_DIR = '/config/auth' def wrapped_pem_to_config_value(pem): return "".join(pem.strip().split("\n")[1:-1]) if not config.exists(base + ['ssl']): exit(0) x509_base = base + ['ssl'] pki_name = 'openconnect' if not config.exists(pki_base + ['ca']): config.set(pki_base + ['ca']) config.set_tag(pki_base + ['ca']) if not config.exists(pki_base + ['certificate']): config.set(pki_base + ['certificate']) config.set_tag(pki_base + ['certificate']) if config.exists(x509_base + ['ca-cert-file']): cert_file = config.return_value(x509_base + ['ca-cert-file']) cert_path = os.path.join(AUTH_DIR, cert_file) cert = None if os.path.isfile(cert_path): if not os.access(cert_path, os.R_OK): run(f'sudo chmod 644 {cert_path}') with open(cert_path, 'r') as f: cert_data = f.read() cert = load_certificate(cert_data, wrap_tags=False) if cert: cert_pem = encode_certificate(cert) config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) config.set(x509_base + ['ca-certificate'], value=pki_name) else: print(f'Failed to migrate CA certificate on openconnect config') config.delete(x509_base + ['ca-cert-file']) if config.exists(x509_base + ['cert-file']): cert_file = config.return_value(x509_base + ['cert-file']) cert_path = os.path.join(AUTH_DIR, cert_file) cert = None if os.path.isfile(cert_path): if not os.access(cert_path, os.R_OK): run(f'sudo chmod 644 {cert_path}') with open(cert_path, 'r') as f: cert_data = f.read() cert = load_certificate(cert_data, wrap_tags=False) if cert: cert_pem = encode_certificate(cert) config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) config.set(x509_base + ['certificate'], value=pki_name) else: print(f'Failed to migrate certificate on openconnect config') config.delete(x509_base + ['cert-file']) if config.exists(x509_base + ['key-file']): key_file = config.return_value(x509_base + ['key-file']) key_path = os.path.join(AUTH_DIR, key_file) key = None if os.path.isfile(key_path): if not os.access(key_path, os.R_OK): run(f'sudo chmod 644 {key_path}') with open(key_path, 'r') as f: key_data = f.read() key = load_private_key(key_data, passphrase=None, wrap_tags=False) if key: key_pem = encode_private_key(key, passphrase=None) config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem)) else: print(f'Failed to migrate private key on openconnect config') config.delete(x509_base + ['key-file']) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/policy/4-to-5 b/src/migration-scripts/policy/4-to-5 index 5b8fee17e..738850f67 100755 --- a/src/migration-scripts/policy/4-to-5 +++ b/src/migration-scripts/policy/4-to-5 @@ -1,138 +1,135 @@ #!/usr/bin/env python3 # # Copyright (C) 2022-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # T2199: Migrate interface policy nodes to policy route <name> interface <ifname> -import re - from sys import argv from sys import exit from vyos.configtree import ConfigTree -from vyos.ifconfig import Section if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() base4 = ['policy', 'route'] base6 = ['policy', 'route6'] config = ConfigTree(config_file) def delete_orphaned_interface_policy(config, iftype, ifname, vif=None, vifs=None, vifc=None): """Delete unexpected policy on interfaces in cases when policy does not exist but inreface has a policy configuration Example T5941: set interfaces bonding bond0 vif 995 policy """ if_path = ['interfaces', iftype, ifname] if vif: if_path += ['vif', vif] elif vifs: if_path += ['vif-s', vifs] if vifc: if_path += ['vif-c', vifc] if not config.exists(if_path + ['policy']): return config.delete(if_path + ['policy']) if not config.exists(base4) and not config.exists(base6): # Delete orphaned nodes on interfaces T5941 for iftype in config.list_nodes(['interfaces']): for ifname in config.list_nodes(['interfaces', iftype]): delete_orphaned_interface_policy(config, iftype, ifname) if config.exists(['interfaces', iftype, ifname, 'vif']): for vif in config.list_nodes(['interfaces', iftype, ifname, 'vif']): delete_orphaned_interface_policy(config, iftype, ifname, vif=vif) if config.exists(['interfaces', iftype, ifname, 'vif-s']): for vifs in config.list_nodes(['interfaces', iftype, ifname, 'vif-s']): delete_orphaned_interface_policy(config, iftype, ifname, vifs=vifs) if config.exists(['interfaces', iftype, ifname, 'vif-s', vifs, 'vif-c']): for vifc in config.list_nodes(['interfaces', iftype, ifname, 'vif-s', vifs, 'vif-c']): delete_orphaned_interface_policy(config, iftype, ifname, vifs=vifs, vifc=vifc) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) # Nothing to do exit(0) def migrate_interface(config, iftype, ifname, vif=None, vifs=None, vifc=None): if_path = ['interfaces', iftype, ifname] ifname_full = ifname if vif: if_path += ['vif', vif] ifname_full = f'{ifname}.{vif}' elif vifs: if_path += ['vif-s', vifs] ifname_full = f'{ifname}.{vifs}' if vifc: if_path += ['vif-c', vifc] ifname_full = f'{ifname}.{vifs}.{vifc}' if not config.exists(if_path + ['policy']): return if config.exists(if_path + ['policy', 'route']): route_name = config.return_value(if_path + ['policy', 'route']) config.set(base4 + [route_name, 'interface'], value=ifname_full, replace=False) if config.exists(if_path + ['policy', 'route6']): route_name = config.return_value(if_path + ['policy', 'route6']) config.set(base6 + [route_name, 'interface'], value=ifname_full, replace=False) config.delete(if_path + ['policy']) for iftype in config.list_nodes(['interfaces']): for ifname in config.list_nodes(['interfaces', iftype]): migrate_interface(config, iftype, ifname) if config.exists(['interfaces', iftype, ifname, 'vif']): for vif in config.list_nodes(['interfaces', iftype, ifname, 'vif']): migrate_interface(config, iftype, ifname, vif=vif) if config.exists(['interfaces', iftype, ifname, 'vif-s']): for vifs in config.list_nodes(['interfaces', iftype, ifname, 'vif-s']): migrate_interface(config, iftype, ifname, vifs=vifs) if config.exists(['interfaces', iftype, ifname, 'vif-s', vifs, 'vif-c']): for vifc in config.list_nodes(['interfaces', iftype, ifname, 'vif-s', vifs, 'vif-c']): migrate_interface(config, iftype, ifname, vifs=vifs, vifc=vifc) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/policy/5-to-6 b/src/migration-scripts/policy/5-to-6 index f1545cddb..86287d578 100755 --- a/src/migration-scripts/policy/5-to-6 +++ b/src/migration-scripts/policy/5-to-6 @@ -1,65 +1,62 @@ #!/usr/bin/env python3 # # Copyright (C) 2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # T5165: Migrate policy local-route rule <tag> destination|source -import re - from sys import argv from sys import exit from vyos.configtree import ConfigTree -from vyos.ifconfig import Section if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() base4 = ['policy', 'local-route'] base6 = ['policy', 'local-route6'] config = ConfigTree(config_file) if not config.exists(base4) and not config.exists(base6): # Nothing to do exit(0) # replace 'policy local-route{v6} rule <tag> destination|source <x.x.x.x>' # => 'policy local-route{v6} rule <tag> destination|source address <x.x.x.x>' for base in [base4, base6]: if config.exists(base + ['rule']): for rule in config.list_nodes(base + ['rule']): dst_path = base + ['rule', rule, 'destination'] src_path = base + ['rule', rule, 'source'] # Destination if config.exists(dst_path): for dst_addr in config.return_values(dst_path): config.set(dst_path + ['address'], value=dst_addr, replace=False) # Source if config.exists(src_path): for src_addr in config.return_values(src_path): config.set(src_path + ['address'], value=src_addr, replace=False) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/policy/6-to-7 b/src/migration-scripts/policy/6-to-7 index e07822ffd..cdefc6837 100755 --- a/src/migration-scripts/policy/6-to-7 +++ b/src/migration-scripts/policy/6-to-7 @@ -1,79 +1,76 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # T5729: Switch to valueless whenever is possible. # From # set policy [route | route6] ... rule <rule> log enable # set policy [route | route6] ... rule <rule> log disable # To # set policy [route | route6] ... rule <rule> log # Remove command if log=disable -import re - from sys import argv from sys import exit from vyos.configtree import ConfigTree -from vyos.ifconfig import Section if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() base = ['policy'] config = ConfigTree(config_file) if not config.exists(base): # Nothing to do exit(0) for family in ['route', 'route6']: if config.exists(base + [family]): for policy_name in config.list_nodes(base + [family]): if config.exists(base + [family, policy_name, 'rule']): for rule in config.list_nodes(base + [family, policy_name, 'rule']): # Log if config.exists(base + [family, policy_name, 'rule', rule, 'log']): log_value = config.return_value(base + [family, policy_name, 'rule', rule, 'log']) config.delete(base + [family, policy_name, 'rule', rule, 'log']) if log_value == 'enable': config.set(base + [family, policy_name, 'rule', rule, 'log']) # State if config.exists(base + [family, policy_name, 'rule', rule, 'state']): flag_enable = 'False' for state in ['established', 'invalid', 'new', 'related']: if config.exists(base + [family, policy_name, 'rule', rule, 'state', state]): state_value = config.return_value(base + [family, policy_name, 'rule', rule, 'state', state]) config.delete(base + [family, policy_name, 'rule', rule, 'state', state]) if state_value == 'enable': config.set(base + [family, policy_name, 'rule', rule, 'state'], value=state, replace=False) flag_enable = 'True' if flag_enable == 'False': config.delete(base + [family, policy_name, 'rule', rule, 'state']) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) - exit(1) \ No newline at end of file + exit(1) diff --git a/src/migration-scripts/pppoe-server/1-to-2 b/src/migration-scripts/pppoe-server/1-to-2 index c73899ca1..b266893c0 100755 --- a/src/migration-scripts/pppoe-server/1-to-2 +++ b/src/migration-scripts/pppoe-server/1-to-2 @@ -1,61 +1,58 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # change mppe node to a leaf node with value prefer -import os - from sys import argv, exit from vyos.configtree import ConfigTree if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) base = ['service', 'pppoe-server'] if not config.exists(base): # Nothing to do exit(0) else: mppe_base = base + ['ppp-options', 'mppe'] if config.exists(mppe_base): # get current values tmp = config.list_nodes(mppe_base) # drop node(s) first ... config.delete(mppe_base) print(tmp) # set new value based on preference if 'require' in tmp: config.set(mppe_base, value='require') elif 'prefer' in tmp: config.set(mppe_base, value='prefer') elif 'deny' in tmp: config.set(mppe_base, value='deny') try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) - diff --git a/src/migration-scripts/pppoe-server/3-to-4 b/src/migration-scripts/pppoe-server/3-to-4 index c07bbb1df..477ed6f22 100755 --- a/src/migration-scripts/pppoe-server/3-to-4 +++ b/src/migration-scripts/pppoe-server/3-to-4 @@ -1,141 +1,139 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # - remove primary/secondary identifier from nameserver -import os - from sys import argv, exit from vyos.configtree import ConfigTree if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) base = ['service', 'pppoe-server'] if not config.exists(base): # Nothing to do exit(0) else: # Migrate IPv4 DNS servers dns_base = base + ['dns-servers'] if config.exists(dns_base): for server in ['server-1', 'server-2']: if config.exists(dns_base + [server]): dns = config.return_value(dns_base + [server]) config.set(base + ['name-server'], value=dns, replace=False) config.delete(dns_base) # Migrate IPv6 DNS servers dns_base = base + ['dnsv6-servers'] if config.exists(dns_base): for server in ['server-1', 'server-2', 'server-3']: if config.exists(dns_base + [server]): dns = config.return_value(dns_base + [server]) config.set(base + ['name-server'], value=dns, replace=False) config.delete(dns_base) # Migrate IPv4 WINS servers wins_base = base + ['wins-servers'] if config.exists(wins_base): for server in ['server-1', 'server-2']: if config.exists(wins_base + [server]): wins = config.return_value(wins_base + [server]) config.set(base + ['wins-server'], value=wins, replace=False) config.delete(wins_base) # Migrate radius-settings node to RADIUS and use this as base for the # later migration of the RADIUS servers - this will save a lot of code radius_settings = base + ['authentication', 'radius-settings'] if config.exists(radius_settings): config.rename(radius_settings, 'radius') # Migrate RADIUS dynamic author / change of authorisation server dae_old = base + ['authentication', 'radius', 'dae-server'] if config.exists(dae_old): config.rename(dae_old, 'dynamic-author') dae_new = base + ['authentication', 'radius', 'dynamic-author'] if config.exists(dae_new + ['ip-address']): config.rename(dae_new + ['ip-address'], 'server') if config.exists(dae_new + ['secret']): config.rename(dae_new + ['secret'], 'key') # Migrate RADIUS server radius_server = base + ['authentication', 'radius-server'] if config.exists(radius_server): new_base = base + ['authentication', 'radius', 'server'] config.set(new_base) config.set_tag(new_base) for server in config.list_nodes(radius_server): old_base = radius_server + [server] config.copy(old_base, new_base + [server]) # migrate key if config.exists(new_base + [server, 'secret']): config.rename(new_base + [server, 'secret'], 'key') # remove old req-limit node if config.exists(new_base + [server, 'req-limit']): config.delete(new_base + [server, 'req-limit']) config.delete(radius_server) # Migrate IPv6 prefixes ipv6_base = base + ['client-ipv6-pool'] if config.exists(ipv6_base + ['prefix']): prefix_old = config.return_values(ipv6_base + ['prefix']) # delete old prefix CLI nodes config.delete(ipv6_base + ['prefix']) # create ned prefix tag node config.set(ipv6_base + ['prefix']) config.set_tag(ipv6_base + ['prefix']) for p in prefix_old: prefix = p.split(',')[0] mask = p.split(',')[1] config.set(ipv6_base + ['prefix', prefix, 'mask'], value=mask) if config.exists(ipv6_base + ['delegate-prefix']): prefix_old = config.return_values(ipv6_base + ['delegate-prefix']) # delete old delegate prefix CLI nodes config.delete(ipv6_base + ['delegate-prefix']) # create ned delegation tag node config.set(ipv6_base + ['delegate']) config.set_tag(ipv6_base + ['delegate']) for p in prefix_old: prefix = p.split(',')[0] mask = p.split(',')[1] config.set(ipv6_base + ['delegate', prefix, 'delegation-prefix'], value=mask) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/pppoe-server/6-to-7 b/src/migration-scripts/pppoe-server/6-to-7 index b94ce57f9..d51c1c9d8 100755 --- a/src/migration-scripts/pppoe-server/6-to-7 +++ b/src/migration-scripts/pppoe-server/6-to-7 @@ -1,119 +1,117 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # - move all pool to named pools # 'start-stop' migrate to namedpool 'default-range-pool' # 'subnet' migrate to namedpool 'default-subnet-pool' # 'default-subnet-pool' is the next pool for 'default-range-pool' # - There is only one gateway-address, take the first which is configured # - default-pool by migration. # 1. If authentication mode = 'local' then it is first named pool. # If there are not named pools, namedless pool will be default. # 2. If authentication mode = 'radius' then namedless pool will be default -import os - from sys import argv from sys import exit from vyos.configtree import ConfigTree from vyos.base import Warning if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) base = ['service', 'pppoe-server'] pool_base = base + ['client-ip-pool'] if not config.exists(base): exit(0) if not config.exists(pool_base): exit(0) default_pool = '' range_pool_name = 'default-range-pool' #Default nameless pools migrations if config.exists(pool_base + ['start']) and config.exists(pool_base + ['stop']): def is_legalrange(ip1: str, ip2: str, mask: str): from ipaddress import IPv4Interface interface1 = IPv4Interface(f'{ip1}/{mask}') interface2 = IPv4Interface(f'{ip2}/{mask}') return interface1.network.network_address == interface2.network.network_address and interface2.ip > interface1.ip start_ip = config.return_value(pool_base + ['start']) stop_ip = config.return_value(pool_base + ['stop']) if is_legalrange(start_ip, stop_ip, '24'): ip_range = f'{start_ip}-{stop_ip}' config.set(pool_base + [range_pool_name, 'range'], value=ip_range, replace=False) default_pool = range_pool_name else: Warning( f'PPPoE client-ip-pool range start-ip:{start_ip} and stop-ip:{stop_ip} can not be migrated.') config.delete(pool_base + ['start']) config.delete(pool_base + ['stop']) if config.exists(pool_base + ['subnet']): default_pool = range_pool_name for subnet in config.return_values(pool_base + ['subnet']): config.set(pool_base + [range_pool_name, 'range'], value=subnet, replace=False) config.delete(pool_base + ['subnet']) gateway = '' if config.exists(base + ['gateway-address']): gateway = config.return_value(base + ['gateway-address']) #named pool migration namedpools_base = pool_base + ['name'] if config.exists(namedpools_base): if config.exists(base + ['authentication', 'mode']): if config.return_value(base + ['authentication', 'mode']) == 'local': if config.list_nodes(namedpools_base): default_pool = config.list_nodes(namedpools_base)[0] for pool_name in config.list_nodes(namedpools_base): pool_path = namedpools_base + [pool_name] if config.exists(pool_path + ['subnet']): subnet = config.return_value(pool_path + ['subnet']) config.set(pool_base + [pool_name, 'range'], value=subnet, replace=False) if config.exists(pool_path + ['next-pool']): next_pool = config.return_value(pool_path + ['next-pool']) config.set(pool_base + [pool_name, 'next-pool'], value=next_pool) if not gateway: if config.exists(pool_path + ['gateway-address']): gateway = config.return_value(pool_path + ['gateway-address']) config.delete(namedpools_base) if gateway: config.set(base + ['gateway-address'], value=gateway) if default_pool: config.set(base + ['default-pool'], value=default_pool) # format as tag node config.set_tag(pool_base) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/pppoe-server/7-to-8 b/src/migration-scripts/pppoe-server/7-to-8 index b0d9bb464..0381f0bf9 100755 --- a/src/migration-scripts/pppoe-server/7-to-8 +++ b/src/migration-scripts/pppoe-server/7-to-8 @@ -1,61 +1,58 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # Migrating to named ipv6 pools -import os - from sys import argv from sys import exit from vyos.configtree import ConfigTree - if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) base = ['service', 'pppoe-server'] pool_base = base + ['client-ipv6-pool'] if not config.exists(base): exit(0) if not config.exists(pool_base): exit(0) ipv6_pool_name = 'ipv6-pool' config.copy(pool_base, pool_base + [ipv6_pool_name]) if config.exists(pool_base + ['prefix']): config.delete(pool_base + ['prefix']) config.set(base + ['default-ipv6-pool'], value=ipv6_pool_name) if config.exists(pool_base + ['delegate']): config.delete(pool_base + ['delegate']) # format as tag node config.set_tag(pool_base) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/pppoe-server/8-to-9 b/src/migration-scripts/pppoe-server/8-to-9 index ad75c28a1..4932a766f 100755 --- a/src/migration-scripts/pppoe-server/8-to-9 +++ b/src/migration-scripts/pppoe-server/8-to-9 @@ -1,69 +1,66 @@ #!/usr/bin/env python3 # # Copyright (C) 2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # Change from 'ccp' to 'disable-ccp' in ppp-option section # Migration ipv6 options -import os - from sys import argv from sys import exit from vyos.configtree import ConfigTree - if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) base = ['service', 'pppoe-server'] if not config.exists(base): exit(0) #CCP migration if config.exists(base + ['ppp-options', 'ccp']): config.delete(base + ['ppp-options', 'ccp']) else: config.set(base + ['ppp-options', 'disable-ccp']) #IPV6 options migrations if config.exists(base + ['ppp-options','ipv6-peer-intf-id']): intf_peer_id = config.return_value(base + ['ppp-options','ipv6-peer-intf-id']) if intf_peer_id == 'ipv4': intf_peer_id = 'ipv4-addr' config.set(base + ['ppp-options','ipv6-peer-interface-id'], value=intf_peer_id, replace=True) config.delete(base + ['ppp-options','ipv6-peer-intf-id']) if config.exists(base + ['ppp-options','ipv6-intf-id']): intf_id = config.return_value(base + ['ppp-options','ipv6-intf-id']) config.set(base + ['ppp-options','ipv6-interface-id'], value=intf_id, replace=True) config.delete(base + ['ppp-options','ipv6-intf-id']) if config.exists(base + ['ppp-options','ipv6-accept-peer-intf-id']): config.set(base + ['ppp-options','ipv6-accept-peer-interface-id']) config.delete(base + ['ppp-options','ipv6-accept-peer-intf-id']) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/pptp/2-to-3 b/src/migration-scripts/pptp/2-to-3 index 091cb68ec..42c4dedf4 100755 --- a/src/migration-scripts/pptp/2-to-3 +++ b/src/migration-scripts/pptp/2-to-3 @@ -1,75 +1,73 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # - move all pool to named pools # 'start-stop' migrate to namedpool 'default-range-pool' # 'default-subnet-pool' is the next pool for 'default-range-pool' -import os - from sys import argv from sys import exit from vyos.configtree import ConfigTree from vyos.base import Warning if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) base = ['vpn', 'pptp', 'remote-access'] pool_base = base + ['client-ip-pool'] if not config.exists(base): exit(0) if not config.exists(pool_base): exit(0) range_pool_name = 'default-range-pool' if config.exists(pool_base + ['start']) and config.exists(pool_base + ['stop']): def is_legalrange(ip1: str, ip2: str, mask: str): from ipaddress import IPv4Interface interface1 = IPv4Interface(f'{ip1}/{mask}') interface2 = IPv4Interface(f'{ip2}/{mask}') return interface1.network.network_address == interface2.network.network_address and interface2.ip > interface1.ip start_ip = config.return_value(pool_base + ['start']) stop_ip = config.return_value(pool_base + ['stop']) if is_legalrange(start_ip, stop_ip, '24'): ip_range = f'{start_ip}-{stop_ip}' config.set(pool_base + [range_pool_name, 'range'], value=ip_range, replace=False) config.set(base + ['default-pool'], value=range_pool_name) else: Warning( f'PPTP client-ip-pool range start-ip:{start_ip} and stop-ip:{stop_ip} can not be migrated.') config.delete(pool_base + ['start']) config.delete(pool_base + ['stop']) # format as tag node config.set_tag(pool_base) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/pptp/3-to-4 b/src/migration-scripts/pptp/3-to-4 index 0a8dad2f4..ebd343028 100755 --- a/src/migration-scripts/pptp/3-to-4 +++ b/src/migration-scripts/pptp/3-to-4 @@ -1,51 +1,48 @@ #!/usr/bin/env python3 # # Copyright (C) 2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # - Move 'mppe' from 'authentication' node to 'ppp-options' -import os - from sys import argv from sys import exit from vyos.configtree import ConfigTree - if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) base = ['vpn', 'pptp', 'remote-access'] if not config.exists(base): exit(0) if config.exists(base + ['authentication','mppe']): mppe = config.return_value(base + ['authentication','mppe']) config.set(base + ['ppp-options', 'mppe'], value=mppe, replace=True) config.delete(base + ['authentication','mppe']) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/pptp/4-to-5 b/src/migration-scripts/pptp/4-to-5 index d4b3f9a14..83632b6d8 100755 --- a/src/migration-scripts/pptp/4-to-5 +++ b/src/migration-scripts/pptp/4-to-5 @@ -1,66 +1,63 @@ #!/usr/bin/env python3 # # Copyright (C) 2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # - Move 'require' from 'protocols' in 'authentication' node # - Migrate to new default values in radius timeout and acct-timeout -import os - from sys import argv from sys import exit from vyos.configtree import ConfigTree - if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) base = ['vpn', 'pptp', 'remote-access'] if not config.exists(base): exit(0) #migrate require to protocols require_path = base + ['authentication', 'require'] if config.exists(require_path): protocols = list(config.return_values(require_path)) for protocol in protocols: config.set(base + ['authentication', 'protocols'], value=protocol, replace=False) config.delete(require_path) else: config.set(base + ['authentication', 'protocols'], value='mschap-v2') radius_path = base + ['authentication', 'radius'] if config.exists(radius_path): if not config.exists(radius_path + ['timeout']): config.set(radius_path + ['timeout'], value=3) if not config.exists(radius_path + ['acct-timeout']): config.set(radius_path + ['acct-timeout'], value=3) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/snmp/2-to-3 b/src/migration-scripts/snmp/2-to-3 index 30911aa27..ab9b5dcba 100755 --- a/src/migration-scripts/snmp/2-to-3 +++ b/src/migration-scripts/snmp/2-to-3 @@ -1,57 +1,54 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2022-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # T4857: Implement FRR SNMP recomendations # cli changes from: # set service snmp oid-enable route-table # To # set service snmp oid-enable ip-forward -import re - from sys import argv from sys import exit from vyos.configtree import ConfigTree -from vyos.ifconfig import Section if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() base = ['service snmp'] config = ConfigTree(config_file) if not config.exists(base): # Nothing to do exit(0) if config.exists(base + ['oid-enable']): config.delete(base + ['oid-enable']) config.set(base + ['oid-enable'], 'ip-forward') try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/sstp/0-to-1 b/src/migration-scripts/sstp/0-to-1 index e2fe1ea8f..150127aaf 100755 --- a/src/migration-scripts/sstp/0-to-1 +++ b/src/migration-scripts/sstp/0-to-1 @@ -1,129 +1,128 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # - migrate from "service sstp-server" to "vpn sstp" # - remove primary/secondary identifier from nameserver # - migrate RADIUS configuration to a more uniform syntax accross the system # - authentication radius-server x.x.x.x to authentication radius server x.x.x.x # - authentication radius-settings to authentication radius # - do not migrate radius server req-limit, use default of unlimited # - migrate SSL certificate path -import os import sys from vyos.configtree import ConfigTree if len(sys.argv) < 2: print("Must specify file name!") sys.exit(1) file_name = sys.argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) old_base = ['service', 'sstp-server'] if not config.exists(old_base): # Nothing to do sys.exit(0) else: # ensure new base path exists if not config.exists(['vpn']): config.set(['vpn']) new_base = ['vpn', 'sstp'] # copy entire tree config.copy(old_base, new_base) config.delete(old_base) # migrate DNS servers dns_base = new_base + ['network-settings', 'dns-server'] if config.exists(dns_base): if config.exists(dns_base + ['primary-dns']): dns = config.return_value(dns_base + ['primary-dns']) config.set(new_base + ['network-settings', 'name-server'], value=dns, replace=False) if config.exists(dns_base + ['secondary-dns']): dns = config.return_value(dns_base + ['secondary-dns']) config.set(new_base + ['network-settings', 'name-server'], value=dns, replace=False) config.delete(dns_base) # migrate radius options - copy subtree # thus must happen before migration of the individual RADIUS servers old_options = new_base + ['authentication', 'radius-settings'] if config.exists(old_options): new_options = new_base + ['authentication', 'radius'] config.copy(old_options, new_options) config.delete(old_options) # migrate radius dynamic author / change of authorisation server dae_old = new_base + ['authentication', 'radius', 'dae-server'] if config.exists(dae_old): config.rename(dae_old, 'dynamic-author') dae_new = new_base + ['authentication', 'radius', 'dynamic-author'] if config.exists(dae_new + ['ip-address']): config.rename(dae_new + ['ip-address'], 'server') if config.exists(dae_new + ['secret']): config.rename(dae_new + ['secret'], 'key') # migrate radius server radius_server = new_base + ['authentication', 'radius-server'] if config.exists(radius_server): for server in config.list_nodes(radius_server): base = radius_server + [server] new = new_base + ['authentication', 'radius', 'server', server] # convert secret to key if config.exists(base + ['secret']): tmp = config.return_value(base + ['secret']) config.set(new + ['key'], value=tmp) if config.exists(base + ['fail-time']): tmp = config.return_value(base + ['fail-time']) config.set(new + ['fail-time'], value=tmp) config.set_tag(new_base + ['authentication', 'radius', 'server']) config.delete(radius_server) # migrate SSL certificates old_ssl = new_base + ['sstp-settings'] new_ssl = new_base + ['ssl'] config.copy(old_ssl + ['ssl-certs'], new_ssl) config.delete(old_ssl) if config.exists(new_ssl + ['ca']): config.rename(new_ssl + ['ca'], 'ca-cert-file') if config.exists(new_ssl + ['server-cert']): config.rename(new_ssl + ['server-cert'], 'cert-file') if config.exists(new_ssl + ['server-key']): config.rename(new_ssl + ['server-key'], 'key-file') try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) sys.exit(1) diff --git a/src/migration-scripts/sstp/3-to-4 b/src/migration-scripts/sstp/3-to-4 index 00ca7a52d..5b7757e60 100755 --- a/src/migration-scripts/sstp/3-to-4 +++ b/src/migration-scripts/sstp/3-to-4 @@ -1,136 +1,135 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # - Update SSL to use PKI configuration import os from sys import argv from sys import exit from vyos.configtree import ConfigTree from vyos.pki import load_certificate -from vyos.pki import load_crl from vyos.pki import load_private_key from vyos.pki import encode_certificate from vyos.pki import encode_private_key from vyos.utils.process import run if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) base = ['vpn', 'sstp'] pki_base = ['pki'] if not config.exists(base): exit(0) AUTH_DIR = '/config/auth' def wrapped_pem_to_config_value(pem): return "".join(pem.strip().split("\n")[1:-1]) if not config.exists(base + ['ssl']): exit(0) x509_base = base + ['ssl'] pki_name = 'sstp' if not config.exists(pki_base + ['ca']): config.set(pki_base + ['ca']) config.set_tag(pki_base + ['ca']) if not config.exists(pki_base + ['certificate']): config.set(pki_base + ['certificate']) config.set_tag(pki_base + ['certificate']) if config.exists(x509_base + ['ca-cert-file']): cert_file = config.return_value(x509_base + ['ca-cert-file']) cert_path = os.path.join(AUTH_DIR, cert_file) cert = None if os.path.isfile(cert_path): if not os.access(cert_path, os.R_OK): run(f'sudo chmod 644 {cert_path}') with open(cert_path, 'r') as f: cert_data = f.read() cert = load_certificate(cert_data, wrap_tags=False) if cert: cert_pem = encode_certificate(cert) config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) config.set(x509_base + ['ca-certificate'], value=pki_name) else: print(f'Failed to migrate CA certificate on sstp config') config.delete(x509_base + ['ca-cert-file']) if config.exists(x509_base + ['cert-file']): cert_file = config.return_value(x509_base + ['cert-file']) cert_path = os.path.join(AUTH_DIR, cert_file) cert = None if os.path.isfile(cert_path): if not os.access(cert_path, os.R_OK): run(f'sudo chmod 644 {cert_path}') with open(cert_path, 'r') as f: cert_data = f.read() cert = load_certificate(cert_data, wrap_tags=False) if cert: cert_pem = encode_certificate(cert) config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) config.set(x509_base + ['certificate'], value=pki_name) else: print(f'Failed to migrate certificate on sstp config') config.delete(x509_base + ['cert-file']) if config.exists(x509_base + ['key-file']): key_file = config.return_value(x509_base + ['key-file']) key_path = os.path.join(AUTH_DIR, key_file) key = None if os.path.isfile(key_path): if not os.access(key_path, os.R_OK): run(f'sudo chmod 644 {key_path}') with open(key_path, 'r') as f: key_data = f.read() key = load_private_key(key_data, passphrase=None, wrap_tags=False) if key: key_pem = encode_private_key(key, passphrase=None) config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem)) else: print(f'Failed to migrate private key on sstp config') config.delete(x509_base + ['key-file']) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/sstp/4-to-5 b/src/migration-scripts/sstp/4-to-5 index 95e482713..6907240a0 100755 --- a/src/migration-scripts/sstp/4-to-5 +++ b/src/migration-scripts/sstp/4-to-5 @@ -1,62 +1,59 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # - move all pool to named pools # 'subnet' migrate to namedpool 'default-subnet-pool' # 'default-subnet-pool' is the next pool for 'default-range-pool' -import os - from sys import argv from sys import exit from vyos.configtree import ConfigTree - if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) base = ['vpn', 'sstp'] pool_base = base + ['client-ip-pool'] if not config.exists(base): exit(0) if not config.exists(pool_base): exit(0) range_pool_name = 'default-range-pool' if config.exists(pool_base + ['subnet']): default_pool = range_pool_name for subnet in config.return_values(pool_base + ['subnet']): config.set(pool_base + [range_pool_name, 'range'], value=subnet, replace=False) config.delete(pool_base + ['subnet']) config.set(base + ['default-pool'], value=default_pool) # format as tag node config.set_tag(pool_base) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/sstp/5-to-6 b/src/migration-scripts/sstp/5-to-6 index bac9975b2..43b99044d 100755 --- a/src/migration-scripts/sstp/5-to-6 +++ b/src/migration-scripts/sstp/5-to-6 @@ -1,62 +1,58 @@ #!/usr/bin/env python3 # # Copyright (C) 2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # Migrating to named ipv6 pools -import os -import pprint - from sys import argv from sys import exit from vyos.configtree import ConfigTree - if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) base = ['vpn', 'sstp'] pool_base = base + ['client-ipv6-pool'] if not config.exists(base): exit(0) if not config.exists(pool_base): exit(0) ipv6_pool_name = 'ipv6-pool' config.copy(pool_base, pool_base + [ipv6_pool_name]) if config.exists(pool_base + ['prefix']): config.delete(pool_base + ['prefix']) config.set(base + ['default-ipv6-pool'], value=ipv6_pool_name) if config.exists(pool_base + ['delegate']): config.delete(pool_base + ['delegate']) # format as tag node config.set_tag(pool_base) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/system/15-to-16 b/src/migration-scripts/system/15-to-16 index aa1c34032..2944cdb1e 100755 --- a/src/migration-scripts/system/15-to-16 +++ b/src/migration-scripts/system/15-to-16 @@ -1,37 +1,36 @@ #!/usr/bin/env python3 # # Make 'system options reboot-on-panic' valueless -import os import sys from vyos.configtree import ConfigTree if len(sys.argv) < 2: print("Must specify file name!") sys.exit(1) file_name = sys.argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) base = ['system', 'options'] if not config.exists(base): # Nothing to do sys.exit(0) else: if config.exists(base + ['reboot-on-panic']): reboot = config.return_value(base + ['reboot-on-panic']) config.delete(base + ['reboot-on-panic']) # create new valueless node if action was true if reboot == "true": config.set(base + ['reboot-on-panic']) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) sys.exit(1) diff --git a/src/migration-scripts/system/16-to-17 b/src/migration-scripts/system/16-to-17 index 37e02611d..afa171a9b 100755 --- a/src/migration-scripts/system/16-to-17 +++ b/src/migration-scripts/system/16-to-17 @@ -1,55 +1,54 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # * remove "system login user <user> group" node, Why should be add a user to a # 3rd party group when the system is fully managed by CLI? # * remove "system login user <user> level" node # This is the only privilege level left and also the default, what is the # sense in keeping this orphaned node? -import os import sys from vyos.configtree import ConfigTree if len(sys.argv) < 2: print("Must specify file name!") sys.exit(1) file_name = sys.argv[1] with open(file_name, 'r') as f: config_file = f.read() config = ConfigTree(config_file) base = ['system', 'login', 'user'] if not config.exists(base): # Nothing to do sys.exit(0) else: for user in config.list_nodes(base): if config.exists(base + [user, 'group']): config.delete(base + [user, 'group']) if config.exists(base + [user, 'level']): config.delete(base + [user, 'level']) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) sys.exit(1) diff --git a/src/migration-scripts/system/19-to-20 b/src/migration-scripts/system/19-to-20 index c04e6a5a6..177173c50 100755 --- a/src/migration-scripts/system/19-to-20 +++ b/src/migration-scripts/system/19-to-20 @@ -1,64 +1,62 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # T3048: remove smp-affinity node from ethernet and use tuned instead -import os - from sys import exit, argv from vyos.configtree import ConfigTree if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() base = ['system', 'options'] base_new = ['system', 'option'] config = ConfigTree(config_file) if not config.exists(base): # Nothing to do exit(0) if config.exists(base_new): for node in config.list_nodes(base): config.copy(base + [node], base_new + [node]) else: config.copy(base, base_new) config.delete(base) # Rename "system option beep-if-fully-booted" -> "system option startup-beep" base_beep = base_new + ['beep-if-fully-booted'] if config.exists(base_beep): config.rename(base_beep, 'startup-beep') # Rename "system option ctrl-alt-del-action" -> "system option ctrl-alt-delete" base_ctrl_alt_del = base_new + ['ctrl-alt-del-action'] if config.exists(base_ctrl_alt_del): config.rename(base_ctrl_alt_del, 'ctrl-alt-delete') try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/system/20-to-21 b/src/migration-scripts/system/20-to-21 index 4bcf4edab..24e042ce2 100755 --- a/src/migration-scripts/system/20-to-21 +++ b/src/migration-scripts/system/20-to-21 @@ -1,48 +1,46 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # T3795: merge "system name-servers-dhcp" into "system name-server" -import os - from sys import argv from vyos.configtree import ConfigTree if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() base = ['system', 'name-servers-dhcp'] config = ConfigTree(config_file) if not config.exists(base): # Nothing to do exit(0) for interface in config.return_values(base): config.set(['system', 'name-server'], value=interface, replace=False) config.delete(base) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/system/21-to-22 b/src/migration-scripts/system/21-to-22 index 810b634ab..2a1b603c6 100755 --- a/src/migration-scripts/system/21-to-22 +++ b/src/migration-scripts/system/21-to-22 @@ -1,57 +1,55 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import os - from sys import exit, argv from vyos.configtree import ConfigTree if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() base = ['system', 'sysctl'] config = ConfigTree(config_file) if not config.exists(base): # Nothing to do exit(0) for all_custom in ['all', 'custom']: if config.exists(base + [all_custom]): for key in config.list_nodes(base + [all_custom]): tmp = config.return_value(base + [all_custom, key, 'value']) config.set(base + ['parameter', key, 'value'], value=tmp) config.set_tag(base + ['parameter']) config.delete(base + [all_custom]) for ipv4_param in ['net.ipv4.igmp_max_memberships', 'net.ipv4.ipfrag_time']: if config.exists(base + [ipv4_param]): tmp = config.return_value(base + [ipv4_param]) config.set(base + ['parameter', ipv4_param, 'value'], value=tmp) config.set_tag(base + ['parameter']) config.delete(base + [ipv4_param]) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) exit(1) diff --git a/src/migration-scripts/system/22-to-23 b/src/migration-scripts/system/22-to-23 index 8ed198383..f83279b88 100755 --- a/src/migration-scripts/system/22-to-23 +++ b/src/migration-scripts/system/22-to-23 @@ -1,50 +1,48 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2022-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import os - from sys import exit, argv from vyos.configtree import ConfigTree if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() base = ['system', 'ipv6'] config = ConfigTree(config_file) if not config.exists(base): # Nothing to do exit(0) # T4346: drop support to disbale IPv6 address family within the OS Kernel if config.exists(base + ['disable']): config.delete(base + ['disable']) # IPv6 address family disable was the only CLI option set - we can cleanup # the entire tree if len(config.list_nodes(base)) == 0: config.delete(base) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print(f'Failed to save the modified config: {e}') exit(1) diff --git a/src/migration-scripts/system/23-to-24 b/src/migration-scripts/system/23-to-24 index fd68dbf22..1fd61d83b 100755 --- a/src/migration-scripts/system/23-to-24 +++ b/src/migration-scripts/system/23-to-24 @@ -1,89 +1,87 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2022-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import os - from ipaddress import ip_interface from ipaddress import ip_address from sys import exit, argv from vyos.configtree import ConfigTree from vyos.template import is_ipv4 if len(argv) < 2: print("Must specify file name!") exit(1) file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() base = ['protocols', 'static', 'arp'] tmp_base = ['protocols', 'static', 'arp-tmp'] config = ConfigTree(config_file) def fixup_cli(config, path, interface): if config.exists(path + ['address']): for address in config.return_values(path + ['address']): tmp = ip_interface(address) # ARP is only available for IPv4 ;-) if not is_ipv4(tmp): continue if ip_address(host) in tmp.network.hosts(): mac = config.return_value(tmp_base + [host, 'hwaddr']) iface_path = ['protocols', 'static', 'arp', 'interface'] config.set(iface_path + [interface, 'address', host, 'mac'], value=mac) config.set_tag(iface_path) config.set_tag(iface_path + [interface, 'address']) continue if not config.exists(base): # Nothing to do exit(0) # We need a temporary copy of the config tree as the original one needs to be # deleted first due to a change iun thge tagNode structure. config.copy(base, tmp_base) config.delete(base) for host in config.list_nodes(tmp_base): for type in config.list_nodes(['interfaces']): for interface in config.list_nodes(['interfaces', type]): if_base = ['interfaces', type, interface] fixup_cli(config, if_base, interface) if config.exists(if_base + ['vif']): for vif in config.list_nodes(if_base + ['vif']): vif_base = ['interfaces', type, interface, 'vif', vif] fixup_cli(config, vif_base, f'{interface}.{vif}') if config.exists(if_base + ['vif-s']): for vif_s in config.list_nodes(if_base + ['vif-s']): vif_s_base = ['interfaces', type, interface, 'vif-s', vif_s] fixup_cli(config, vif_s_base, f'{interface}.{vif_s}') if config.exists(if_base + ['vif-s', vif_s, 'vif-c']): for vif_c in config.list_nodes(if_base + ['vif-s', vif_s, 'vif-c']): vif_c_base = ['interfaces', type, interface, 'vif-s', vif_s, 'vif-c', vif_c] fixup_cli(config, vif_c_base, f'{interface}.{vif_s}.{vif_c}') config.delete(tmp_base) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print(f'Failed to save the modified config: {e}') exit(1) diff --git a/src/op_mode/clear_dhcp_lease.py b/src/op_mode/clear_dhcp_lease.py index f372d3af0..b37b62bca 100755 --- a/src/op_mode/clear_dhcp_lease.py +++ b/src/op_mode/clear_dhcp_lease.py @@ -1,78 +1,77 @@ #!/usr/bin/env python3 import argparse import re -from isc_dhcp_leases import Lease from isc_dhcp_leases import IscDhcpLeases from vyos.configquery import ConfigTreeQuery from vyos.utils.io import ask_yes_no from vyos.utils.process import call from vyos.utils.commit import commit_in_progress config = ConfigTreeQuery() base = ['service', 'dhcp-server'] lease_file = '/config/dhcpd.leases' def del_lease_ip(address): """ Read lease_file and write data to this file without specific section "lease ip" Delete section "lease x.x.x.x { x;x;x; }" """ with open(lease_file, encoding='utf-8') as f: data = f.read().rstrip() lease_config_ip = '{(?P<config>[\s\S]+?)\n}' pattern = rf"lease {address} {lease_config_ip}" # Delete lease for ip block data = re.sub(pattern, '', data) # Write new data to original lease_file with open(lease_file, 'w', encoding='utf-8') as f: f.write(data) def is_ip_in_leases(address): """ Return True if address found in the lease file """ leases = IscDhcpLeases(lease_file) lease_ips = [] for lease in leases.get(): lease_ips.append(lease.ip) if address not in lease_ips: print(f'Address "{address}" not found in "{lease_file}"') return False return True if not config.exists(base): print('DHCP-server not configured!') exit(0) if config.exists(base + ['failover']): print('Lease cannot be reset in failover mode!') exit(0) if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--ip', help='IPv4 address', action='store', required=True) args = parser.parse_args() address = args.ip if not is_ip_in_leases(address): exit(1) if commit_in_progress(): print('Cannot clear DHCP lease while a commit is in progress') exit(1) if not ask_yes_no(f'This will restart DHCP server.\nContinue?'): exit(1) else: del_lease_ip(address) call('systemctl restart isc-dhcp-server.service') diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py index 2f90865fd..a7143d664 100755 --- a/src/op_mode/dhcp.py +++ b/src/op_mode/dhcp.py @@ -1,435 +1,432 @@ #!/usr/bin/env python3 # # Copyright (C) 2022-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os import sys import typing from datetime import datetime from glob import glob from ipaddress import ip_address from isc_dhcp_leases import IscDhcpLeases from tabulate import tabulate import vyos.opmode from vyos.base import Warning from vyos.configquery import ConfigTreeQuery -from vyos.utils.dict import dict_search -from vyos.utils.file import read_file -from vyos.utils.process import cmd from vyos.utils.process import is_systemd_service_running from vyos.utils.process import call time_string = "%a %b %d %H:%M:%S %Z %Y" config = ConfigTreeQuery() lease_valid_states = ['all', 'active', 'free', 'expired', 'released', 'abandoned', 'reset', 'backup'] sort_valid_inet = ['end', 'mac', 'hostname', 'ip', 'pool', 'remaining', 'start', 'state'] sort_valid_inet6 = ['end', 'iaid_duid', 'ip', 'last_communication', 'pool', 'remaining', 'state', 'type'] ArgFamily = typing.Literal['inet', 'inet6'] ArgState = typing.Literal['all', 'active', 'free', 'expired', 'released', 'abandoned', 'reset', 'backup'] ArgOrigin = typing.Literal['local', 'remote'] def _utc_to_local(utc_dt): return datetime.fromtimestamp((datetime.fromtimestamp(utc_dt) - datetime(1970, 1, 1)).total_seconds()) def _format_hex_string(in_str): out_str = "" # if input is divisible by 2, add : every 2 chars if len(in_str) > 0 and len(in_str) % 2 == 0: out_str = ':'.join(a+b for a,b in zip(in_str[::2], in_str[1::2])) else: out_str = in_str return out_str def _find_list_of_dict_index(lst, key='ip', value='') -> int: """ Find the index entry of list of dict matching the dict value Exampe: % lst = [{'ip': '192.0.2.1'}, {'ip': '192.0.2.2'}] % _find_list_of_dict_index(lst, key='ip', value='192.0.2.2') % 1 """ idx = next((index for (index, d) in enumerate(lst) if d[key] == value), None) return idx def _get_raw_server_leases(family='inet', pool=None, sorted=None, state=[], origin=None) -> list: """ Get DHCP server leases :return list """ lease_file = '/config/dhcpdv6.leases' if family == 'inet6' else '/config/dhcpd.leases' data = [] leases = IscDhcpLeases(lease_file).get(include_backups=True) if pool is None: pool = _get_dhcp_pools(family=family) aux = False else: pool = [pool] aux = True ## Search leases for every pool for pool_name in pool: for lease in leases: if lease.sets.get('shared-networkname', '') == pool_name or lease.sets.get('shared-networkname', '') == '': #if lease.sets.get('shared-networkname', '') == pool_name: data_lease = {} data_lease['ip'] = lease.ip data_lease['state'] = lease.binding_state #data_lease['pool'] = pool_name if lease.sets.get('shared-networkname', '') != '' else 'Fail-Over Server' data_lease['pool'] = lease.sets.get('shared-networkname', '') data_lease['end'] = lease.end.timestamp() if lease.end else None data_lease['origin'] = 'local' if data_lease['pool'] != '' else 'remote' if family == 'inet': data_lease['mac'] = lease.ethernet data_lease['start'] = lease.start.timestamp() data_lease['hostname'] = lease.hostname if family == 'inet6': data_lease['last_communication'] = lease.last_communication.timestamp() data_lease['iaid_duid'] = _format_hex_string(lease.host_identifier_string) lease_types_long = {'na': 'non-temporary', 'ta': 'temporary', 'pd': 'prefix delegation'} data_lease['type'] = lease_types_long[lease.type] data_lease['remaining'] = '-' if lease.end: data_lease['remaining'] = lease.end - datetime.utcnow() if data_lease['remaining'].days >= 0: # substraction gives us a timedelta object which can't be formatted with strftime # so we use str(), split gets rid of the microseconds data_lease['remaining'] = str(data_lease["remaining"]).split('.')[0] # Do not add old leases if data_lease['remaining'] != '' and data_lease['state'] != 'free': if not state or data_lease['state'] in state or state == 'all': if not origin or data_lease['origin'] in origin: if not aux or (aux and data_lease['pool'] == pool_name): data.append(data_lease) # deduplicate checked = [] for entry in data: addr = entry.get('ip') if addr not in checked: checked.append(addr) else: idx = _find_list_of_dict_index(data, key='ip', value=addr) data.pop(idx) if sorted: if sorted == 'ip': data.sort(key = lambda x:ip_address(x['ip'])) else: data.sort(key = lambda x:x[sorted]) return data def _get_formatted_server_leases(raw_data, family='inet'): data_entries = [] if family == 'inet': for lease in raw_data: ipaddr = lease.get('ip') hw_addr = lease.get('mac') state = lease.get('state') start = lease.get('start') start = _utc_to_local(start).strftime('%Y/%m/%d %H:%M:%S') end = lease.get('end') end = _utc_to_local(end).strftime('%Y/%m/%d %H:%M:%S') if end else '-' remain = lease.get('remaining') pool = lease.get('pool') hostname = lease.get('hostname') origin = lease.get('origin') data_entries.append([ipaddr, hw_addr, state, start, end, remain, pool, hostname, origin]) headers = ['IP Address', 'MAC address', 'State', 'Lease start', 'Lease expiration', 'Remaining', 'Pool', 'Hostname', 'Origin'] if family == 'inet6': for lease in raw_data: ipaddr = lease.get('ip') state = lease.get('state') start = lease.get('last_communication') start = _utc_to_local(start).strftime('%Y/%m/%d %H:%M:%S') end = lease.get('end') end = _utc_to_local(end).strftime('%Y/%m/%d %H:%M:%S') remain = lease.get('remaining') lease_type = lease.get('type') pool = lease.get('pool') host_identifier = lease.get('iaid_duid') data_entries.append([ipaddr, state, start, end, remain, lease_type, pool, host_identifier]) headers = ['IPv6 address', 'State', 'Last communication', 'Lease expiration', 'Remaining', 'Type', 'Pool', 'IAID_DUID'] output = tabulate(data_entries, headers, numalign='left') return output def _get_dhcp_pools(family='inet') -> list: v = 'v6' if family == 'inet6' else '' pools = config.list_nodes(f'service dhcp{v}-server shared-network-name') return pools def _get_pool_size(pool, family='inet'): v = 'v6' if family == 'inet6' else '' base = f'service dhcp{v}-server shared-network-name {pool}' size = 0 subnets = config.list_nodes(f'{base} subnet') for subnet in subnets: if family == 'inet6': ranges = config.list_nodes(f'{base} subnet {subnet} address-range start') else: ranges = config.list_nodes(f'{base} subnet {subnet} range') for range in ranges: if family == 'inet6': start = config.list_nodes(f'{base} subnet {subnet} address-range start')[0] stop = config.value(f'{base} subnet {subnet} address-range start {start} stop') else: start = config.value(f'{base} subnet {subnet} range {range} start') stop = config.value(f'{base} subnet {subnet} range {range} stop') # Add +1 because both range boundaries are inclusive size += int(ip_address(stop)) - int(ip_address(start)) + 1 return size def _get_raw_pool_statistics(family='inet', pool=None): if pool is None: pool = _get_dhcp_pools(family=family) else: pool = [pool] v = 'v6' if family == 'inet6' else '' stats = [] for p in pool: subnet = config.list_nodes(f'service dhcp{v}-server shared-network-name {p} subnet') size = _get_pool_size(family=family, pool=p) leases = len(_get_raw_server_leases(family=family, pool=p)) use_percentage = round(leases / size * 100) if size != 0 else 0 pool_stats = {'pool': p, 'size': size, 'leases': leases, 'available': (size - leases), 'use_percentage': use_percentage, 'subnet': subnet} stats.append(pool_stats) return stats def _get_formatted_pool_statistics(pool_data, family='inet'): data_entries = [] for entry in pool_data: pool = entry.get('pool') size = entry.get('size') leases = entry.get('leases') available = entry.get('available') use_percentage = entry.get('use_percentage') use_percentage = f'{use_percentage}%' data_entries.append([pool, size, leases, available, use_percentage]) headers = ['Pool', 'Size','Leases', 'Available', 'Usage'] output = tabulate(data_entries, headers, numalign='left') return output def _verify(func): """Decorator checks if DHCP(v6) config exists""" from functools import wraps @wraps(func) def _wrapper(*args, **kwargs): config = ConfigTreeQuery() family = kwargs.get('family') v = 'v6' if family == 'inet6' else '' unconf_message = f'DHCP{v} server is not configured' # Check if config does not exist if not config.exists(f'service dhcp{v}-server'): raise vyos.opmode.UnconfiguredSubsystem(unconf_message) return func(*args, **kwargs) return _wrapper def _verify_client(func): """Decorator checks if interface is configured as DHCP client""" from functools import wraps from vyos.ifconfig import Section @wraps(func) def _wrapper(*args, **kwargs): config = ConfigTreeQuery() family = kwargs.get('family') v = 'v6' if family == 'inet6' else '' interface = kwargs.get('interface') interface_path = Section.get_config_path(interface) unconf_message = f'DHCP{v} client not configured on interface {interface}!' # Check if config does not exist if not config.exists(f'interfaces {interface_path} address dhcp{v}'): raise vyos.opmode.UnconfiguredSubsystem(unconf_message) return func(*args, **kwargs) return _wrapper @_verify def show_pool_statistics(raw: bool, family: ArgFamily, pool: typing.Optional[str]): pool_data = _get_raw_pool_statistics(family=family, pool=pool) if raw: return pool_data else: return _get_formatted_pool_statistics(pool_data, family=family) @_verify def show_server_leases(raw: bool, family: ArgFamily, pool: typing.Optional[str], sorted: typing.Optional[str], state: typing.Optional[ArgState], origin: typing.Optional[ArgOrigin] ): # if dhcp server is down, inactive leases may still be shown as active, so warn the user. v = '6' if family == 'inet6' else '' service_name = 'DHCPv6' if family == 'inet6' else 'DHCP' if not is_systemd_service_running(f'isc-dhcp-server{v}.service'): Warning(f'{service_name} server is configured but not started. Data may be stale.') v = 'v6' if family == 'inet6' else '' if pool and pool not in _get_dhcp_pools(family=family): raise vyos.opmode.IncorrectValue(f'DHCP{v} pool "{pool}" does not exist!') if state and state not in lease_valid_states: raise vyos.opmode.IncorrectValue(f'DHCP{v} state "{state}" is invalid!') sort_valid = sort_valid_inet6 if family == 'inet6' else sort_valid_inet if sorted and sorted not in sort_valid: raise vyos.opmode.IncorrectValue(f'DHCP{v} sort "{sorted}" is invalid!') lease_data = _get_raw_server_leases(family=family, pool=pool, sorted=sorted, state=state, origin=origin) if raw: return lease_data else: return _get_formatted_server_leases(lease_data, family=family) def _get_raw_client_leases(family='inet', interface=None): from time import mktime from datetime import datetime from vyos.defaults import directories from vyos.utils.network import get_interface_vrf lease_dir = directories['isc_dhclient_dir'] lease_files = [] lease_data = [] if interface: tmp = f'{lease_dir}/dhclient_{interface}.lease' if os.path.exists(tmp): lease_files.append(tmp) else: # All DHCP leases lease_files = glob(f'{lease_dir}/dhclient_*.lease') for lease in lease_files: tmp = {} with open(lease, 'r') as f: for line in f.readlines(): line = line.rstrip() if 'last_update' not in tmp: # ISC dhcp client contains least_update timestamp in human readable # format this makes less sense for an API and also the expiry # timestamp is provided in UNIX time. Convert string (e.g. Sun Jul # 30 18:13:44 CEST 2023) to UNIX time (1690733624) tmp.update({'last_update' : int(mktime(datetime.strptime(line, time_string).timetuple()))}) continue k, v = line.split('=') tmp.update({k : v.replace("'", "")}) if 'interface' in tmp: vrf = get_interface_vrf(tmp['interface']) if vrf: tmp.update({'vrf' : vrf}) lease_data.append(tmp) return lease_data def _get_formatted_client_leases(lease_data, family): from time import localtime from time import strftime from vyos.utils.network import is_intf_addr_assigned data_entries = [] for lease in lease_data: if not lease.get('new_ip_address'): continue data_entries.append(["Interface", lease['interface']]) if 'new_ip_address' in lease: tmp = '[Active]' if is_intf_addr_assigned(lease['interface'], lease['new_ip_address']) else '[Inactive]' data_entries.append(["IP address", lease['new_ip_address'], tmp]) if 'new_subnet_mask' in lease: data_entries.append(["Subnet Mask", lease['new_subnet_mask']]) if 'new_domain_name' in lease: data_entries.append(["Domain Name", lease['new_domain_name']]) if 'new_routers' in lease: data_entries.append(["Router", lease['new_routers']]) if 'new_domain_name_servers' in lease: data_entries.append(["Name Server", lease['new_domain_name_servers']]) if 'new_dhcp_server_identifier' in lease: data_entries.append(["DHCP Server", lease['new_dhcp_server_identifier']]) if 'new_dhcp_lease_time' in lease: data_entries.append(["DHCP Server", lease['new_dhcp_lease_time']]) if 'vrf' in lease: data_entries.append(["VRF", lease['vrf']]) if 'last_update' in lease: tmp = strftime(time_string, localtime(int(lease['last_update']))) data_entries.append(["Last Update", tmp]) if 'new_expiry' in lease: tmp = strftime(time_string, localtime(int(lease['new_expiry']))) data_entries.append(["Expiry", tmp]) # Add empty marker data_entries.append(['']) output = tabulate(data_entries, tablefmt='plain') return output def show_client_leases(raw: bool, family: ArgFamily, interface: typing.Optional[str]): lease_data = _get_raw_client_leases(family=family, interface=interface) if raw: return lease_data else: return _get_formatted_client_leases(lease_data, family=family) @_verify_client def renew_client_lease(raw: bool, family: ArgFamily, interface: str): if not raw: v = 'v6' if family == 'inet6' else '' print(f'Restarting DHCP{v} client on interface {interface}...') if family == 'inet6': call(f'systemctl restart dhcp6c@{interface}.service') else: call(f'systemctl restart dhclient@{interface}.service') if __name__ == '__main__': try: res = vyos.opmode.run(sys.modules[__name__]) if res: print(res) except (ValueError, vyos.opmode.Error) as e: print(e) sys.exit(1)