summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/templates/dhcp-server/dhcpd.conf.tmpl5
-rw-r--r--data/templates/dhcp-server/dhcpdv6.conf.tmpl175
-rw-r--r--smoketest/scripts/cli/base_interfaces_test.py12
-rwxr-xr-xsmoketest/scripts/cli/test_service_dhcp-server.py12
-rwxr-xr-xsmoketest/scripts/cli/test_service_dhcpv6-server.py155
-rwxr-xr-xsrc/conf_mode/dhcpv6_relay.py2
-rwxr-xr-xsrc/conf_mode/dhcpv6_server.py406
-rwxr-xr-xsrc/migration-scripts/interfaces/13-to-142
-rwxr-xr-xsrc/migration-scripts/interfaces/14-to-152
-rwxr-xr-xsrc/migration-scripts/interfaces/15-to-162
-rw-r--r--src/tests/test_template.py49
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')
+