diff options
-rw-r--r-- | data/templates/dhcp-server/dhcpd.conf.tmpl | 5 | ||||
-rw-r--r-- | data/templates/dhcp-server/dhcpdv6.conf.tmpl | 175 | ||||
-rw-r--r-- | smoketest/scripts/cli/base_interfaces_test.py | 12 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_service_dhcp-server.py | 12 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_service_dhcpv6-server.py | 155 | ||||
-rwxr-xr-x | src/conf_mode/dhcpv6_relay.py | 2 | ||||
-rwxr-xr-x | src/conf_mode/dhcpv6_server.py | 406 | ||||
-rwxr-xr-x | src/migration-scripts/interfaces/13-to-14 | 2 | ||||
-rwxr-xr-x | src/migration-scripts/interfaces/14-to-15 | 2 | ||||
-rwxr-xr-x | src/migration-scripts/interfaces/15-to-16 | 2 | ||||
-rw-r--r-- | src/tests/test_template.py | 49 |
11 files changed, 411 insertions, 411 deletions
diff --git a/data/templates/dhcp-server/dhcpd.conf.tmpl b/data/templates/dhcp-server/dhcpd.conf.tmpl index e8425aa6c..bcf425abd 100644 --- a/data/templates/dhcp-server/dhcpd.conf.tmpl +++ b/data/templates/dhcp-server/dhcpd.conf.tmpl @@ -4,7 +4,6 @@ # https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html # # log-facility local7; - {% if hostfile_update is defined %} on release { set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); @@ -13,7 +12,6 @@ on release { set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!"); execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", ClientName, ClientIp, ClientMac, ClientDomain); } - on expiry { set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); set ClientIp = binary-to-ascii(10, 8, ".",leased-address); @@ -25,7 +23,6 @@ on expiry { {{ 'use-host-decl-names on;' if host_decl_name is defined }} ddns-update-style {{ 'interim' if dynamic_dns_update is defined else 'none' }}; - option rfc3442-static-route code 121 = array of integer 8; option windows-static-route code 249 = array of integer 8; option wpad-url code 252 = text; @@ -87,7 +84,7 @@ shared-network {{ network | replace('_','-') }} { option domain-name-servers {{ subnet_config.dns_server | join(', ') }}; {% endif %} {% if subnet_config.domain_search is defined and subnet_config.domain_search is not none %} - option domain-search "{{ subnet_config.domain_search | join(', ') }}"; + option domain-search "{{ subnet_config.domain_search | join('", "') }}"; {% endif %} {% if subnet_config.ntp_server is defined and subnet_config.ntp_server is not none %} option ntp-servers {{ subnet_config.ntp_server | join(', ') }}; diff --git a/data/templates/dhcp-server/dhcpdv6.conf.tmpl b/data/templates/dhcp-server/dhcpdv6.conf.tmpl index aa6d7fb5d..de7c9b29c 100644 --- a/data/templates/dhcp-server/dhcpdv6.conf.tmpl +++ b/data/templates/dhcp-server/dhcpdv6.conf.tmpl @@ -4,87 +4,114 @@ # https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html log-facility local7; -{% if preference %} +{% if preference is defined and preference is not none %} option dhcp6.preference {{ preference }}; {% endif %} # Shared network configration(s) -{% for network in shared_network %} -{% if not network.disabled %} -shared-network {{ network.name }} { - {% if network.common.info_refresh_time %} - option dhcp6.info-refresh-time {{ network.common.info_refresh_time }}; - {% endif %} - {% if network.common.domain_search %} - option dhcp6.domain-search "{{ network.common.domain_search | join('", "') }}"; - {% endif %} - {% if network.common.dns_server %} - option dhcp6.name-servers {{ network.common.dns_server | join(', ') }}; - {% endif %} - {% for subnet in network.subnet %} - subnet6 {{ subnet.network }} { - {% for range in subnet.range6_prefix %} - range6 {{ range.prefix }}{{ " temporary" if range.temporary }}; - {% endfor %} - {% for range in subnet.range6 %} - range6 {{ range.start }} {{ range.stop }}; - {% endfor %} - {% if subnet.domain_search %} - option dhcp6.domain-search "{{ subnet.domain_search | join('", "') }}"; - {% endif %} - {% if subnet.lease_def %} - default-lease-time {{ subnet.lease_def }}; - {% endif %} - {% if subnet.lease_max %} - max-lease-time {{ subnet.lease_max }}; - {% endif %} - {% if subnet.lease_min %} - min-lease-time {{ subnet.lease_min }}; - {% endif %} - {% if subnet.dns_server %} - option dhcp6.name-servers {{ subnet.dns_server | join(', ') }}; - {% endif %} - {% if subnet.nis_domain %} - option dhcp6.nis-domain-name "{{ subnet.nis_domain }}"; - {% endif %} - {% if subnet.nis_server %} - option dhcp6.nis-servers {{ subnet.nis_server | join(', ') }}; - {% endif %} - {% if subnet.nisp_domain %} - option dhcp6.nisp-domain-name "{{ subnet.nisp_domain }}"; - {% endif %} - {% if subnet.nisp_server %} - option dhcp6.nisp-servers {{ subnet.nisp_server | join(', ') }}; - {% endif %} - {% if subnet.sip_address %} - option dhcp6.sip-servers-addresses {{ subnet.sip_address | join(', ') }}; - {% endif %} - {% if subnet.sip_hostname %} - option dhcp6.sip-servers-names "{{ subnet.sip_hostname | join('", "') }}"; - {% endif %} - {% if subnet.sntp_server %} - option dhcp6.sntp-servers {{ subnet.sntp_server | join(', ') }}; - {% endif %} - {% for prefix in subnet.prefix_delegation %} - prefix6 {{ prefix.start }} {{ prefix.stop }} /{{ prefix.length }}; - {% endfor %} - {% for host in subnet.static_mapping %} - {% if not host.disabled %} - host {{ network.name }}_{{ host.name }} { - {% if host.client_identifier %} - host-identifier option dhcp6.client-id {{ host.client_identifier }}; - {% endif %} - {% if host.ipv6_address %} - fixed-address6 {{ host.ipv6_address }}; - {% endif %} +{% if shared_network_name is defined and shared_network_name is not none %} +{% for network, network_config in shared_network_name.items() if network_config.disable is not defined %} +shared-network {{ network | replace('_','-') }} { +{% if network_config.common_options is defined and network_config.common_options is not none %} +{% if network_config.common_options.info_refresh_time is defined and network_config.common_options.info_refresh_time is not none %} + option dhcp6.info-refresh-time {{ network_config.common_options.info_refresh_time }}; +{% endif %} +{% if network_config.common_options.domain_search is defined and network_config.common_options.domain_search is not none %} + option dhcp6.domain-search "{{ network_config.common_options.domain_search | join('", "') }}"; +{% endif %} +{% if network_config.common_options.name_server is defined and network_config.common_options.name_server is not none %} + option dhcp6.name-servers {{ network_config.common_options.name_server | join(', ') }}; +{% endif %} +{% endif %} +{% if network_config.subnet is defined and network_config.subnet is not none %} +{% for subnet, subnet_config in network_config.subnet.items() %} + subnet6 {{ subnet }} { +{% if subnet_config.address_range is defined and subnet_config.address_range is not none %} +{% if subnet_config.address_range.prefix is defined and subnet_config.address_range.prefix is not none %} +{% for prefix, prefix_config in subnet_config.address_range.prefix.items() %} + range6 {{ prefix }} {{ "temporary" if prefix_config.temporary is defined }}; +{% endfor %} +{% endif %} +{% if subnet_config.address_range.start is defined and subnet_config.address_range.start is not none %} +{% for address, address_config in subnet_config.address_range.start.items() %} + range6 {{ address }} {{ address_config.stop }}; +{% endfor %} +{% endif %} +{% endif %} +{% if subnet_config.domain_search is defined and subnet_config.domain_search is not none %} + option dhcp6.domain-search "{{ subnet_config.domain_search | join('", "') }}"; +{% endif %} +{% if subnet_config.lease_time is defined and subnet_config.lease_time is not none %} +{% if subnet_config.lease_time.default is defined and subnet_config.lease_time.default is not none %} + default-lease-time {{ subnet_config.lease_time.default }}; +{% endif %} +{% if subnet_config.lease_time.maximum is defined and subnet_config.lease_time.maximum is not none %} + max-lease-time {{ subnet_config.lease_time.maximum }}; +{% endif %} +{% if subnet_config.lease_time.minimum is defined and subnet_config.lease_time.minimum is not none %} + min-lease-time {{ subnet_config.lease_time.minimum }}; +{% endif %} +{% endif %} +{% if subnet_config.name_server is defined and subnet_config.name_server is not none %} + option dhcp6.name-servers {{ subnet_config.name_server | join(', ') }}; +{% endif %} +{% if subnet_config.nis_domain is defined and subnet_config.nis_domain is not none %} + option dhcp6.nis-domain-name "{{ subnet_config.nis_domain }}"; +{% endif %} +{% if subnet_config.nis_server is defined and subnet_config.nis_server is not none %} + option dhcp6.nis-servers {{ subnet_config.nis_server | join(', ') }}; +{% endif %} +{% if subnet_config.nisplus_domain is defined and subnet_config.nisplus_domain is not none %} + option dhcp6.nisp-domain-name "{{ subnet_config.nisplus_domain }}"; +{% endif %} +{% if subnet_config.nisplus_server is defined and subnet_config.nisplus_server is not none %} + option dhcp6.nisp-servers {{ subnet_config.nisplus_server | join(', ') }}; +{% endif %} +{% if subnet_config.sip_server is defined and subnet_config.sip_server is not none %} +{% set server_ip = [] %} +{% set server_fqdn = [] %} +{% for address in subnet_config.sip_server %} +{% if address | is_ipv6 %} +{% set server_ip = server_ip.append(address) %} +{% else %} +{% set server_fqdn = server_fqdn.append(address) %} +{% endif %} +{% endfor %} +{% if server_ip is defined and server_ip | length > 0 %} + option dhcp6.sip-servers-addresses {{ server_ip | join(', ') }}; +{% endif %} +{% if server_fqdn is defined and server_fqdn | length > 0 %} + option dhcp6.sip-servers-names "{{ server_fqdn | join('", "') }}"; +{% endif %} +{% endif %} +{% if subnet_config.sntp_server is defined and subnet_config.sntp_server is not none %} + option dhcp6.sntp-servers {{ subnet_config.sntp_server | join(', ') }}; +{% endif %} +{% if subnet_config.prefix_delegation is defined and subnet_config.prefix_delegation.start is defined and subnet_config.prefix_delegation.start is not none %} +{% for prefix, prefix_config in subnet_config.prefix_delegation.start.items() %} + prefix6 {{ prefix }} {{ prefix_config.stop }} /{{ prefix_config.prefix_length }}; +{% endfor %} +{% endif %} +{% if subnet_config.static_mapping is defined and subnet_config.static_mapping is not none %} + + # begin configuration of static client mappings +{% for host, host_config in subnet_config.static_mapping.items() if host_config.disable is not defined %} + host {{ network | replace('_','-') }}_{{ host | replace('_','-') }} { +{% if host_config.identifier is defined and host_config.identifier is not none %} + host-identifier option dhcp6.client-id {{ host_config.identifier }}; +{% endif %} +{% if host_config.ipv6_address is defined and host_config.ipv6_address is not none %} + fixed-address6 {{ host_config.ipv6_address }}; +{% endif %} } - {% endif %} - {% endfor %} +{% endfor %} +{% endif %} } - {% endfor %} +{% endfor %} +{% endif %} on commit { - set shared-networkname = "{{ network.name }}"; + set shared-networkname = "{{ network | replace('_','-') }}"; } } +{% endfor %} {% endif %} -{% endfor %} diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py index 615317368..4187fd77c 100644 --- a/smoketest/scripts/cli/base_interfaces_test.py +++ b/smoketest/scripts/cli/base_interfaces_test.py @@ -16,7 +16,9 @@ import os import unittest import json -from netifaces import ifaddresses, AF_INET, AF_INET6 +from netifaces import ifaddresses +from netifaces import AF_INET +from netifaces import AF_INET6 from vyos.configsession import ConfigSession from vyos.ifconfig import Interface @@ -99,7 +101,7 @@ class BasicInterfaceTest: Check if description can be added to interface """ for intf in self._interfaces: - test_string='Description-Test-{}'.format(intf) + test_string=f'Description-Test-{intf}' self.session.set(self._base_path + [intf, 'description', test_string]) for option in self._options.get(intf, []): self.session.set(self._base_path + [intf] + option.split()) @@ -108,8 +110,8 @@ class BasicInterfaceTest: # Validate interface description for intf in self._interfaces: - test_string='Description-Test-{}'.format(intf) - with open('/sys/class/net/{}/ifalias'.format(intf), 'r') as f: + test_string=f'Description-Test-{intf}' + with open(f'/sys/class/net/{intf}/ifalias', 'r') as f: tmp = f.read().rstrip() self.assertTrue(tmp, test_string) @@ -182,7 +184,7 @@ class BasicInterfaceTest: def _mtu_test(self, intf): """ helper function to verify MTU size """ - with open('/sys/class/net/{}/mtu'.format(intf), 'r') as f: + with open(f'/sys/class/net/{intf}/mtu', 'r') as f: tmp = f.read().rstrip() self.assertEqual(tmp, self._mtu) diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py index 2ee26c8bb..e13896095 100755 --- a/smoketest/scripts/cli/test_service_dhcp-server.py +++ b/smoketest/scripts/cli/test_service_dhcp-server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2020 VyOS maintainers and contributors +# Copyright (C) 2020 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 @@ -101,7 +101,7 @@ class TestServiceDHCPServer(unittest.TestCase): smtp_server = '1.2.3.4' time_server = '4.3.2.1' tftp_server = 'tftp.vyos.io' - search_domain = 'foo.vyos.net' + search_domains = ['foo.vyos.net', 'bar.vyos.net'] bootfile_name = 'vyos' bootfile_server = '192.0.2.1' wpad = 'http://wpad.vyos.io/foo/bar' @@ -118,7 +118,8 @@ class TestServiceDHCPServer(unittest.TestCase): self.session.set(pool + ['pop-server', smtp_server]) self.session.set(pool + ['time-server', time_server]) self.session.set(pool + ['tftp-server-name', tftp_server]) - self.session.set(pool + ['domain-search', search_domain]) + for search in search_domains: + self.session.set(pool + ['domain-search', search]) self.session.set(pool + ['bootfile-name', bootfile_name]) self.session.set(pool + ['bootfile-server', bootfile_server]) self.session.set(pool + ['wpad-url', wpad]) @@ -168,7 +169,10 @@ class TestServiceDHCPServer(unittest.TestCase): self.assertIn(f'option domain-name-servers {dns_1}, {dns_2};', config) self.assertIn(f'option routers {router};', config) self.assertIn(f'option domain-name "{domain_name}";', config) - self.assertIn(f'option domain-search "{search_domain}";', config) + + search = '"' + ('", "').join(search_domains) + '"' + self.assertIn(f'option domain-search {search};', config) + self.assertIn(f'option ip-forwarding true;', config) self.assertIn(f'option smtp-server {smtp_server};', config) self.assertIn(f'option pop-server {smtp_server};', config) diff --git a/smoketest/scripts/cli/test_service_dhcpv6-server.py b/smoketest/scripts/cli/test_service_dhcpv6-server.py new file mode 100755 index 000000000..56fc16d2b --- /dev/null +++ b/smoketest/scripts/cli/test_service_dhcpv6-server.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 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 +import os +import unittest + +from vyos.configsession import ConfigSession +from vyos.configsession import ConfigSessionError +from vyos.template import inc_ip +from vyos.util import cmd +from vyos.util import process_named_running +from vyos.util 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 = 'eth1' +interface_addr = inc_ip(subnet, 1) + '/64' + +class TestServiceDHCPServer(unittest.TestCase): + def setUp(self): + self.session = ConfigSession(os.getpid()) + self.session.set(['interfaces', 'ethernet', interface, 'address', interface_addr]) + + def tearDown(self): + self.session.delete(base_path) + self.session.delete(['interfaces', 'ethernet', interface, 'address', interface_addr]) + self.session.commit() + del self.session + + 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.session.set(base_path + ['preference', preference]) + + # we use the first subnet IP address as default gateway + self.session.set(pool + ['name-server', dns_1]) + self.session.set(pool + ['name-server', dns_2]) + self.session.set(pool + ['name-server', dns_2]) + self.session.set(pool + ['lease-time', 'default', lease_time]) + self.session.set(pool + ['lease-time', 'maximum', max_lease_time]) + self.session.set(pool + ['lease-time', 'minimum', min_lease_time]) + self.session.set(pool + ['nis-domain', domain]) + self.session.set(pool + ['nisplus-domain', domain]) + self.session.set(pool + ['sip-server', sip_server]) + self.session.set(pool + ['sntp-server', sntp_server]) + self.session.set(pool + ['address-range', 'start', range_start, 'stop', range_stop]) + + for server in nis_servers: + self.session.set(pool + ['nis-server', server]) + self.session.set(pool + ['nisplus-server', server]) + + for search in search_domains: + self.session.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.session.set(pool + ['static-mapping', client, 'identifier', cid]) + self.session.set(pool + ['static-mapping', client, 'ipv6-address', inc_ip(subnet, client_base)]) + client_base += 1 + + # commit changes + self.session.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) + self.assertIn(f'host {shared_net_name}_{client}' + ' {', config) + self.assertIn(f'fixed-address6 {ip};', 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.session.set(pool + ['address-range', 'start', range_start, 'stop', range_stop]) + self.session.set(pool + ['prefix-delegation', 'start', delegate_start, 'stop', delegate_stop]) + self.session.set(pool + ['prefix-delegation', 'start', delegate_start, 'prefix-length', delegate_len]) + + # commit changes + self.session.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)) + +if __name__ == '__main__': + unittest.main() diff --git a/src/conf_mode/dhcpv6_relay.py b/src/conf_mode/dhcpv6_relay.py index cf8a26674..aea2c3b73 100755 --- a/src/conf_mode/dhcpv6_relay.py +++ b/src/conf_mode/dhcpv6_relay.py @@ -69,7 +69,7 @@ def verify(relay): for interface in relay['listen_interface']: has_global = False for addr in Interface(interface).get_addr(): - if not is_ipv6_link_local(addr.split('/')[0]): + if not is_ipv6_link_local(addr): has_global = True if not has_global: raise ConfigError(f'Interface {interface} does not have global '\ diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py index db248de50..175300bb0 100755 --- a/src/conf_mode/dhcpv6_server.py +++ b/src/conf_mode/dhcpv6_server.py @@ -15,31 +15,24 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import ipaddress +from ipaddress import ip_address +from ipaddress import ip_network from sys import exit -from copy import deepcopy from vyos.config import Config from vyos.template import render from vyos.template import is_ipv6 from vyos.util import call +from vyos.util import dict_search from vyos.validate import is_subnet_connected - from vyos import ConfigError from vyos import airbag airbag.enable() -config_file = r'/run/dhcp-server/dhcpdv6.conf' - -default_config_data = { - 'preference': '', - 'disabled': False, - 'shared_network': [] -} +config_file = '/run/dhcp-server/dhcpdv6.conf' def get_config(config=None): - dhcpv6 = deepcopy(default_config_data) if config: conf = config else: @@ -47,333 +40,110 @@ def get_config(config=None): base = ['service', 'dhcpv6-server'] if not conf.exists(base): return None - else: - conf.set_level(base) - - # Check for global disable of DHCPv6 service - if conf.exists(['disable']): - dhcpv6['disabled'] = True - return dhcpv6 - - # Preference of this DHCPv6 server compared with others - if conf.exists(['preference']): - dhcpv6['preference'] = conf.return_value(['preference']) - - # check for multiple, shared networks served with DHCPv6 addresses - if conf.exists(['shared-network-name']): - for network in conf.list_nodes(['shared-network-name']): - conf.set_level(base + ['shared-network-name', network]) - config = { - 'name': network, - 'disabled': False, - 'common': {}, - 'subnet': [] - } - - # If disabled, the shared-network configuration becomes inactive - if conf.exists(['disable']): - config['disabled'] = True - - # Common options shared among subnets. These can be overridden if - # the same option is specified on a per-subnet or per-host - # basis. These are the only options that can be handed out to - # stateless clients via an information-request message. - if conf.exists(['common-options']): - conf.set_level(base + ['shared-network-name', network, 'common-options']) - - # How often stateless clients should refresh their information. This is - # mostly taken as a hint by clients, and only if they request it. - # (if not specified, the server does not supply this to the client) - if conf.exists(['info-refresh-time']): - config['common']['info_refresh_time'] = conf.return_value(['info-refresh-time']) - - # The domain-search option specifies a 'search list' of Domain Names to be used - # by the client to locate not-fully-qualified domain names. - if conf.exists(['domain-search']): - config['common']['domain_search'] = conf.return_values(['domain-search']) - - # Specifies a list of Domain Name System name servers available to the client. - # Servers should be listed in order of preference. - if conf.exists(['name-server']): - config['common']['dns_server'] = conf.return_values(['name-server']) - - conf.set_level(base + ['shared-network-name', network]) - - # check for multiple subnet configurations in a shared network - if conf.exists(['subnet']): - for net in conf.list_nodes(['subnet']): - conf.set_level(base + ['shared-network-name', network, 'subnet', net]) - subnet = { - 'network': net, - 'range6_prefix': [], - 'range6': [], - 'default_router': '', - 'dns_server': [], - 'domain_name': '', - 'domain_search': [], - 'lease_def': '', - 'lease_min': '', - 'lease_max': '', - 'nis_domain': '', - 'nis_server': [], - 'nisp_domain': '', - 'nisp_server': [], - 'prefix_delegation': [], - 'sip_address': [], - 'sip_hostname': [], - 'sntp_server': [], - 'static_mapping': [] - } - - # For any subnet on which addresses will be assigned dynamically, there must be at - # least one address range statement. The range statement gives the lowest and highest - # IP addresses in a range. All IP addresses in the range should be in the subnet in - # which the range statement is declared. - if conf.exists(['address-range', 'prefix']): - for prefix in conf.list_nodes(['address-range', 'prefix']): - range = { - 'prefix': prefix, - 'temporary': False - } - - # Address range will be used for temporary addresses - if conf.exists(['address-range' 'prefix', prefix, 'temporary']): - range['temporary'] = True - - # Append to subnet temporary range6 list - subnet['range6_prefix'].append(range) - - if conf.exists(['address-range', 'start']): - for range in conf.list_nodes(['address-range', 'start']): - range = { - 'start': range, - 'stop': conf.return_value(['address-range', 'start', range, 'stop']) - } - - # Append to subnet range6 list - subnet['range6'].append(range) - - # The domain-search option specifies a 'search list' of Domain Names to be used - # by the client to locate not-fully-qualified domain names. - if conf.exists(['domain-search']): - subnet['domain_search'] = conf.return_values(['domain-search']) - - # IPv6 address valid lifetime - # (at the end the address is no longer usable by the client) - # (set to 30 days, the usual IPv6 default) - if conf.exists(['lease-time', 'default']): - subnet['lease_def'] = conf.return_value(['lease-time', 'default']) - - # Time should be the maximum length in seconds that will be assigned to a lease. - # The only exception to this is that Dynamic BOOTP lease lengths, which are not - # specified by the client, are not limited by this maximum. - if conf.exists(['lease-time', 'maximum']): - subnet['lease_max'] = conf.return_value(['lease-time', 'maximum']) - - # Time should be the minimum length in seconds that will be assigned to a lease - if conf.exists(['lease-time', 'minimum']): - subnet['lease_min'] = conf.return_value(['lease-time', 'minimum']) - - # Specifies a list of Domain Name System name servers available to the client. - # Servers should be listed in order of preference. - if conf.exists(['name-server']): - subnet['dns_server'] = conf.return_values(['name-server']) - - # Ancient NIS (Network Information Service) domain name - if conf.exists(['nis-domain']): - subnet['nis_domain'] = conf.return_value(['nis-domain']) - - # Ancient NIS (Network Information Service) servers - if conf.exists(['nis-server']): - subnet['nis_server'] = conf.return_values(['nis-server']) - - # Ancient NIS+ (Network Information Service) domain name - if conf.exists(['nisplus-domain']): - subnet['nisp_domain'] = conf.return_value(['nisplus-domain']) - - # Ancient NIS+ (Network Information Service) servers - if conf.exists(['nisplus-server']): - subnet['nisp_server'] = conf.return_values(['nisplus-server']) - - # Local SIP server that is to be used for all outbound SIP requests - IPv6 address - if conf.exists(['sip-server']): - for value in conf.return_values(['sip-server']): - if is_ipv6(value): - subnet['sip_address'].append(value) - else: - subnet['sip_hostname'].append(value) - - # List of local SNTP servers available for the client to synchronize their clocks - if conf.exists(['sntp-server']): - subnet['sntp_server'] = conf.return_values(['sntp-server']) - - # Prefix Delegation (RFC 3633) - if conf.exists(['prefix-delegation', 'start']): - for address in conf.list_nodes(['prefix-delegation', 'start']): - conf.set_level(base + ['shared-network-name', network, 'subnet', net, 'prefix-delegation', 'start', address]) - prefix = { - 'start' : address, - 'stop' : '', - 'length' : '' - } - - if conf.exists(['prefix-length']): - prefix['length'] = conf.return_value(['prefix-length']) - - if conf.exists(['stop']): - prefix['stop'] = conf.return_value(['stop']) - - subnet['prefix_delegation'].append(prefix) - - # - # Static DHCP v6 leases - # - conf.set_level(base + ['shared-network-name', network, 'subnet', net]) - if conf.exists(['static-mapping']): - for mapping in conf.list_nodes(['static-mapping']): - conf.set_level(base + ['shared-network-name', network, 'subnet', net, 'static-mapping', mapping]) - mapping = { - 'name': mapping, - 'disabled': False, - 'ipv6_address': '', - 'client_identifier': '', - } - - # This static lease is disabled - if conf.exists(['disable']): - mapping['disabled'] = True - - # IPv6 address used for this DHCP client - if conf.exists(['ipv6-address']): - mapping['ipv6_address'] = conf.return_value(['ipv6-address']) - - # This option specifies the client’s DUID identifier. DUIDs are similar but different from DHCPv4 client identifiers - if conf.exists(['identifier']): - mapping['client_identifier'] = conf.return_value(['identifier']) - - # append static mapping configuration tu subnet list - subnet['static_mapping'].append(mapping) - - # append subnet configuration to shared network subnet list - config['subnet'].append(subnet) - - # append shared network configuration to config dictionary - dhcpv6['shared_network'].append(config) - - # If all shared-networks are disabled, there's nothing to do. - if all(net['disabled'] for net in dhcpv6['shared_network']): - return None + dhcpv6 = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) return dhcpv6 def verify(dhcpv6): - if not dhcpv6 or dhcpv6['disabled']: + # 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 len(dhcpv6['shared_network']) == 0: - raise ConfigError('No DHCPv6 shared networks configured.\n' \ - 'At least one DHCPv6 shared network must be configured.') + if 'shared_network_name' not in dhcpv6: + raise ConfigError('No DHCPv6 shared networks configured. At least\n' \ + 'one DHCPv6 shared network must be configured.') # Inspect shared-network/subnet subnets = [] listen_ok = False - - for network in dhcpv6['shared_network']: + for network, network_config in dhcpv6['shared_network_name'].items(): # A shared-network requires a subnet definition - if len(network['subnet']) == 0: - raise ConfigError('No DHCPv6 lease subnets configured for {0}. At least one\n' \ - 'lease subnet must be configured for each shared network.'.format(network['name'])) - - range6_start = [] - range6_stop = [] - for subnet in network['subnet']: - # Ususal range declaration with a start and stop address - for range6 in subnet['range6']: - # shorten names - start = range6['start'] - stop = range6['stop'] - - # DHCPv6 stop address is required - if start and not stop: - raise ConfigError('DHCPv6 range stop address for start {0} is not defined!'.format(start)) - - # Start address must be inside network - if not ipaddress.ip_address(start) in ipaddress.ip_network(subnet['network']): - raise ConfigError('DHCPv6 range start address {0} is not in subnet {1}\n' \ - 'specified for shared network {2}!'.format(start, subnet['network'], network['name'])) - - # Stop address must be inside network - if not ipaddress.ip_address(stop) in ipaddress.ip_network(subnet['network']): - raise ConfigError('DHCPv6 range stop address {0} is not in subnet {1}\n' \ - 'specified for shared network {2}!'.format(stop, subnet['network'], network['name'])) - - # Stop address must be greater or equal to start address - if not ipaddress.ip_address(stop) >= ipaddress.ip_address(start): - raise ConfigError('DHCPv6 range stop address {0} must be greater or equal\n' \ - 'to the range start address {1}!'.format(stop, 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('Conflicting DHCPv6 lease range:\n' \ - 'Pool start address {0} defined multipe times!'.format(start)) - else: - 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('Conflicting DHCPv6 lease range:\n' \ - 'Pool stop address {0} defined multipe times!'.format(stop)) - else: - range6_stop.append(stop) + if 'subnet' not in network_config: + raise ConfigError(f'No DHCPv6 lease subnets configured for "{network}". At least one\n' \ + '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 or equal\n' \ + 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:\n' \ + 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:\n' \ + 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 - for prefix in subnet['prefix_delegation']: - if not prefix['stop']: - raise ConfigError('Stop address of delegated IPv6 prefix range must be configured') - - if not prefix['length']: - raise ConfigError('Length of delegated IPv6 prefix must be configured') - - # We also have prefixes that require checking - for prefix in subnet['range6_prefix']: - # If configured prefix does not match our subnet, we have to check that it's inside - if ipaddress.ip_network(prefix['prefix']) != ipaddress.ip_network(subnet['network']): - # Configured prefixes must be inside our network - if not ipaddress.ip_network(prefix['prefix']) in ipaddress.ip_network(subnet['network']): - raise ConfigError('DHCPv6 prefix {0} is not in subnet {1}\n' \ - 'specified for shared network {2}!'.format(prefix['prefix'], subnet['network'], network['name'])) + 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}"\n' + 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) - for mapping in subnet['static_mapping']: - if mapping['ipv6_address']: - # Static address must be in subnet - if not ipaddress.ip_address(mapping['ipv6_address']) in ipaddress.ip_network(subnet['network']): - raise ConfigError('DHCPv6 static mapping IPv6 address {0} for static mapping {1}\n' \ - 'in shared network {2} is outside subnet {3}!' \ - .format(mapping['ipv6_address'], mapping['name'], network['name'], subnet['network'])) + 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}"!') # Subnets must be unique - if subnet['network'] in subnets: + if subnet in subnets: raise ConfigError('DHCPv6 subnets must be unique! Subnet {0} defined multiple times!'.format(subnet['network'])) - else: - subnets.append(subnet['network']) + subnets.append(subnet) - # DHCPv6 requires at least one configured address range or one static mapping - # (FIXME: is not actually checked right now?) + # 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 not network['disabled']: - if is_subnet_connected(subnet['network']): - listen_ok = True + # 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 = ipaddress.ip_network(subnet['network']) + net = ip_network(subnet) for n in subnets: - net2 = ipaddress.ip_network(n) + net2 = ip_network(n) if (net != net2): if net.overlaps(net2): raise ConfigError('DHCPv6 conflicting subnet ranges: {0} overlaps {1}'.format(net, net2)) @@ -387,22 +157,24 @@ def verify(dhcpv6): return None def generate(dhcpv6): - if not dhcpv6 or dhcpv6['disabled']: + # 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.tmpl', dhcpv6) return None def apply(dhcpv6): - if not dhcpv6 or dhcpv6['disabled']: + # bail out early - looks like removal from running config + if not dhcpv6 or 'disable' in dhcpv6: # DHCP server is removed in the commit call('systemctl stop isc-dhcp-server6.service') if os.path.exists(config_file): os.unlink(config_file) - else: - call('systemctl restart isc-dhcp-server6.service') + return None + call('systemctl restart isc-dhcp-server6.service') return None if __name__ == '__main__': diff --git a/src/migration-scripts/interfaces/13-to-14 b/src/migration-scripts/interfaces/13-to-14 index fc6d7f443..6e6439c36 100755 --- a/src/migration-scripts/interfaces/13-to-14 +++ b/src/migration-scripts/interfaces/13-to-14 @@ -17,8 +17,6 @@ # T3043: rename Wireless interface security mode 'both' to 'wpa+wpa2' # T3043: move "system wifi-regulatory-domain" to indicidual wireless interface -import os - from sys import exit, argv from vyos.configtree import ConfigTree diff --git a/src/migration-scripts/interfaces/14-to-15 b/src/migration-scripts/interfaces/14-to-15 index 5c25f8628..c38db0bf8 100755 --- a/src/migration-scripts/interfaces/14-to-15 +++ b/src/migration-scripts/interfaces/14-to-15 @@ -16,8 +16,6 @@ # T3048: remove smp-affinity node from ethernet and use tuned instead -import os - from sys import exit, argv from vyos.configtree import ConfigTree diff --git a/src/migration-scripts/interfaces/15-to-16 b/src/migration-scripts/interfaces/15-to-16 index 126911ccd..804c48be0 100755 --- a/src/migration-scripts/interfaces/15-to-16 +++ b/src/migration-scripts/interfaces/15-to-16 @@ -16,8 +16,6 @@ # remove pppoe "ipv6 enable" option -import os - from sys import exit, argv from vyos.configtree import ConfigTree diff --git a/src/tests/test_template.py b/src/tests/test_template.py index 6dc2f075e..544755692 100644 --- a/src/tests/test_template.py +++ b/src/tests/test_template.py @@ -44,3 +44,52 @@ class TestVyOSTemplate(TestCase): self.assertFalse(vyos.template.is_ipv6('192.0.2.0/24')) self.assertFalse(vyos.template.is_ipv6('192.0.2.1/32')) self.assertFalse(vyos.template.is_ipv6('VyOS')) + + def test_address_from_cidr(self): + self.assertEqual(vyos.template.address_from_cidr('192.0.2.0/24'), '192.0.2.0') + self.assertEqual(vyos.template.address_from_cidr('2001:db8::/48'), '2001:db8::') + + with self.assertRaises(ValueError): + # ValueError: 192.0.2.1/24 has host bits set + self.assertEqual(vyos.template.address_from_cidr('192.0.2.1/24'), '192.0.2.1') + + with self.assertRaises(ValueError): + # ValueError: 2001:db8::1/48 has host bits set + self.assertEqual(vyos.template.address_from_cidr('2001:db8::1/48'), '2001:db8::1') + + def test_netmask_from_cidr(self): + self.assertEqual(vyos.template.netmask_from_cidr('192.0.2.0/24'), '255.255.255.0') + self.assertEqual(vyos.template.netmask_from_cidr('192.0.2.128/25'), '255.255.255.128') + self.assertEqual(vyos.template.netmask_from_cidr('2001:db8::/48'), 'ffff:ffff:ffff::') + + with self.assertRaises(ValueError): + # ValueError: 192.0.2.1/24 has host bits set + self.assertEqual(vyos.template.netmask_from_cidr('192.0.2.1/24'), '255.255.255.0') + + with self.assertRaises(ValueError): + # ValueError: 2001:db8:1:/64 has host bits set + self.assertEqual(vyos.template.netmask_from_cidr('2001:db8:1:/64'), 'ffff:ffff:ffff:ffff::') + + def test_first_host_address(self): + self.assertEqual(vyos.template.first_host_address('10.0.0.0/24'), '10.0.0.1') + self.assertEqual(vyos.template.first_host_address('10.0.0.128/25'), '10.0.0.129') + self.assertEqual(vyos.template.first_host_address('2001:db8::/64'), '2001:db8::') + + def test_last_host_address(self): + self.assertEqual(vyos.template.last_host_address('10.0.0.0/24'), '10.0.0.254') + self.assertEqual(vyos.template.last_host_address('10.0.0.128/25'), '10.0.0.254') + self.assertEqual(vyos.template.last_host_address('2001:db8::/64'), '2001:db8::ffff:ffff:ffff:ffff') + + def test_increment_ip(self): + self.assertEqual(vyos.template.inc_ip('10.0.0.0/24', '2'), '10.0.0.2') + self.assertEqual(vyos.template.inc_ip('10.0.0.0', '2'), '10.0.0.2') + self.assertEqual(vyos.template.inc_ip('10.0.0.0', '10'), '10.0.0.10') + self.assertEqual(vyos.template.inc_ip('2001:db8::/64', '2'), '2001:db8::2') + self.assertEqual(vyos.template.inc_ip('2001:db8::', '10'), '2001:db8::a') + + def test_decrement_ip(self): + self.assertEqual(vyos.template.dec_ip('10.0.0.100/24', '1'), '10.0.0.99') + self.assertEqual(vyos.template.dec_ip('10.0.0.90', '10'), '10.0.0.80') + self.assertEqual(vyos.template.dec_ip('2001:db8::b/64', '10'), '2001:db8::1') + self.assertEqual(vyos.template.dec_ip('2001:db8::f', '5'), '2001:db8::a') + |