Page MenuHomeVyOS Platform

No OneTemporary

Size
125 KB
Referenced Files
None
Subscribers
None
diff --git a/interface-definitions/dhcp-server.xml.in b/interface-definitions/dhcp-server.xml.in
index 081f7ed42..8aaeeb29d 100644
--- a/interface-definitions/dhcp-server.xml.in
+++ b/interface-definitions/dhcp-server.xml.in
@@ -1,466 +1,456 @@
<?xml version="1.0"?>
<!-- DHCP server configuration -->
<interfaceDefinition>
<node name="service">
<children>
<node name="dhcp-server" owner="${vyos_conf_scripts_dir}/dhcp_server.py">
<properties>
<help>Dynamic Host Configuration Protocol (DHCP) for DHCP server</help>
<priority>911</priority>
</properties>
<children>
#include <include/generic-disable-node.xml.i>
<leafNode name="dynamic-dns-update">
<properties>
<help>Dynamically update Domain Name System (RFC4702)</help>
<valueless/>
</properties>
</leafNode>
<node name="failover">
<properties>
<help>DHCP failover configuration</help>
</properties>
<children>
#include <include/source-address-ipv4.xml.i>
<leafNode name="remote">
<properties>
<help>IPv4 remote address used for connectio</help>
<valueHelp>
<format>ipv4</format>
<description>IPv4 address of failover peer</description>
</valueHelp>
<constraint>
<validator name="ipv4-address"/>
</constraint>
</properties>
</leafNode>
<leafNode name="name">
<properties>
<help>Peer name used to identify connection</help>
<constraint>
<regex>[-_a-zA-Z0-9.]+</regex>
</constraint>
<constraintErrorMessage>Invalid failover peer name. May only contain letters, numbers and .-_</constraintErrorMessage>
</properties>
</leafNode>
<leafNode name="status">
<properties>
<help>Failover hierarchy</help>
<completionHelp>
<list>primary secondary</list>
</completionHelp>
<valueHelp>
<format>primary</format>
<description>Configure this server to be the primary node</description>
</valueHelp>
<valueHelp>
<format>secondary</format>
<description>Configure this server to be the secondary node</description>
</valueHelp>
<constraint>
<regex>(primary|secondary)</regex>
</constraint>
<constraintErrorMessage>Invalid DHCP failover peer status</constraintErrorMessage>
</properties>
</leafNode>
#include <include/pki/ca-certificate.xml.i>
#include <include/pki/certificate.xml.i>
</children>
</node>
<leafNode name="hostfile-update">
<properties>
<help>Updating /etc/hosts file (per client lease)</help>
<valueless/>
</properties>
</leafNode>
#include <include/listen-address-ipv4.xml.i>
<tagNode name="shared-network-name">
<properties>
<help>Name of DHCP shared network</help>
<constraint>
<regex>[-_a-zA-Z0-9.]+</regex>
</constraint>
<constraintErrorMessage>Invalid shared network name. May only contain letters, numbers and .-_</constraintErrorMessage>
</properties>
<children>
<leafNode name="authoritative">
<properties>
<help>Option to make DHCP server authoritative for this physical network</help>
<valueless/>
</properties>
</leafNode>
#include <include/dhcp/domain-name.xml.i>
#include <include/dhcp/domain-search.xml.i>
#include <include/dhcp/ntp-server.xml.i>
#include <include/generic-description.xml.i>
#include <include/generic-disable-node.xml.i>
#include <include/name-server-ipv4.xml.i>
<tagNode name="subnet">
<properties>
<help>DHCP subnet for shared network</help>
<valueHelp>
<format>ipv4net</format>
<description>IPv4 address and prefix length</description>
</valueHelp>
<constraint>
<validator name="ipv4-prefix"/>
</constraint>
<constraintErrorMessage>Invalid IPv4 subnet definition</constraintErrorMessage>
</properties>
<children>
<leafNode name="bootfile-name">
<properties>
<help>Bootstrap file name</help>
<constraint>
<regex>[[:ascii:]]{1,253}</regex>
</constraint>
</properties>
</leafNode>
<leafNode name="bootfile-server">
<properties>
<help>Server from which the initial boot file is to be loaded</help>
<valueHelp>
<format>ipv4</format>
<description>Bootfile server IPv4 address</description>
</valueHelp>
<valueHelp>
<format>hostname</format>
<description>Bootfile server FQDN</description>
</valueHelp>
<constraint>
<validator name="ipv4-address"/>
<validator name="fqdn"/>
</constraint>
</properties>
</leafNode>
<leafNode name="bootfile-size">
<properties>
<help>Bootstrap file size</help>
<valueHelp>
<format>u32:1-16</format>
<description>Bootstrap file size in 512 byte blocks</description>
</valueHelp>
<constraint>
<validator name="numeric" argument="--range 1-16"/>
</constraint>
</properties>
</leafNode>
#include <include/dhcp/captive-portal.xml.i>
<leafNode name="client-prefix-length">
<properties>
<help>Specifies the clients subnet mask as per RFC 950. If unset, subnet declaration is used.</help>
<valueHelp>
<format>u32:0-32</format>
<description>DHCP client prefix length must be 0 to 32</description>
</valueHelp>
<constraint>
<validator name="numeric" argument="--range 0-32"/>
</constraint>
<constraintErrorMessage>DHCP client prefix length must be 0 to 32</constraintErrorMessage>
</properties>
</leafNode>
<leafNode name="default-router">
<properties>
<help>IP address of default router</help>
<valueHelp>
<format>ipv4</format>
<description>Default router IPv4 address</description>
</valueHelp>
<constraint>
<validator name="ipv4-address"/>
</constraint>
</properties>
</leafNode>
#include <include/dhcp/domain-name.xml.i>
#include <include/dhcp/domain-search.xml.i>
#include <include/generic-description.xml.i>
#include <include/name-server-ipv4.xml.i>
<leafNode name="exclude">
<properties>
<help>IP address to exclude from DHCP lease range</help>
<valueHelp>
<format>ipv4</format>
<description>IPv4 address to exclude from lease range</description>
</valueHelp>
<constraint>
<validator name="ipv4-address"/>
</constraint>
<multi/>
</properties>
</leafNode>
<leafNode name="ip-forwarding">
<properties>
<help>Enable IP forwarding on client</help>
<valueless/>
</properties>
</leafNode>
<leafNode name="lease">
<properties>
<help>Lease timeout in seconds</help>
<valueHelp>
<format>u32</format>
<description>DHCP lease time in seconds</description>
</valueHelp>
<constraint>
<validator name="numeric" argument="--range 0-4294967295"/>
</constraint>
<constraintErrorMessage>DHCP lease time must be between 0 and 4294967295 (49 days)</constraintErrorMessage>
</properties>
<defaultValue>86400</defaultValue>
</leafNode>
#include <include/dhcp/ntp-server.xml.i>
<leafNode name="pop-server">
<properties>
<help>IP address of POP3 server</help>
<valueHelp>
<format>ipv4</format>
<description>POP3 server IPv4 address</description>
</valueHelp>
<constraint>
<validator name="ipv4-address"/>
</constraint>
<multi/>
</properties>
</leafNode>
<leafNode name="server-identifier">
<properties>
<help>Address for DHCP server identifier</help>
<valueHelp>
<format>ipv4</format>
<description>DHCP server identifier IPv4 address</description>
</valueHelp>
<constraint>
<validator name="ipv4-address"/>
</constraint>
</properties>
</leafNode>
<leafNode name="smtp-server">
<properties>
<help>IP address of SMTP server</help>
<valueHelp>
<format>ipv4</format>
<description>SMTP server IPv4 address</description>
</valueHelp>
<constraint>
<validator name="ipv4-address"/>
</constraint>
<multi/>
</properties>
</leafNode>
<tagNode name="range">
<properties>
<help>DHCP lease range</help>
<constraint>
<regex>[-_a-zA-Z0-9.]+</regex>
</constraint>
<constraintErrorMessage>Invalid range name, may only be alphanumeric, dot and hyphen</constraintErrorMessage>
</properties>
<children>
<leafNode name="start">
<properties>
<help>First IP address for DHCP lease range</help>
<valueHelp>
<format>ipv4</format>
<description>IPv4 start address of pool</description>
</valueHelp>
<constraint>
<validator name="ipv4-address"/>
</constraint>
</properties>
</leafNode>
<leafNode name="stop">
<properties>
<help>Last IP address for DHCP lease range</help>
<valueHelp>
<format>ipv4</format>
<description>IPv4 end address of pool</description>
</valueHelp>
<constraint>
<validator name="ipv4-address"/>
</constraint>
</properties>
</leafNode>
</children>
</tagNode>
<tagNode name="static-mapping">
<properties>
- <help>Name of static mapping</help>
+ <help>Hostname for static mapping reservation</help>
<constraint>
- <regex>[-_a-zA-Z0-9.]+</regex>
+ <validator name="fqdn"/>
</constraint>
- <constraintErrorMessage>Invalid static mapping name, may only be alphanumeric, dot and hyphen</constraintErrorMessage>
+ <constraintErrorMessage>Invalid static mapping hostname</constraintErrorMessage>
</properties>
<children>
#include <include/generic-disable-node.xml.i>
<leafNode name="ip-address">
<properties>
<help>Fixed IP address of static mapping</help>
<valueHelp>
<format>ipv4</format>
<description>IPv4 address used in static mapping</description>
</valueHelp>
<constraint>
<validator name="ipv4-address"/>
</constraint>
</properties>
</leafNode>
- <leafNode name="mac-address">
- <properties>
- <help>Media Access Control (MAC) address</help>
- <valueHelp>
- <format>macaddr</format>
- <description>Hardware (MAC) address</description>
- </valueHelp>
- <constraint>
- <validator name="mac-address"/>
- </constraint>
- </properties>
- </leafNode>
+ #include <include/interface/mac.xml.i>
+ #include <include/interface/duid.xml.i>
</children>
</tagNode>
<tagNode name="static-route">
<properties>
<help>Classless static route destination subnet</help>
<valueHelp>
<format>ipv4net</format>
<description>IPv4 address and prefix length</description>
</valueHelp>
<constraint>
<validator name="ipv4-prefix"/>
</constraint>
</properties>
<children>
<leafNode name="next-hop">
<properties>
<help>IP address of router to be used to reach the destination subnet</help>
<valueHelp>
<format>ipv4</format>
<description>IPv4 address of router</description>
</valueHelp>
<constraint>
<validator name="ip-address"/>
</constraint>
</properties>
</leafNode>
</children>
</tagNode >
<leafNode name="ipv6-only-preferred">
<properties>
<help>Disable IPv4 on IPv6 only hosts (RFC 8925)</help>
<valueHelp>
<format>u32</format>
<description>Seconds</description>
</valueHelp>
<constraint>
<validator name="numeric" argument="--range 0-4294967295"/>
</constraint>
<constraintErrorMessage>Seconds must be between 0 and 4294967295 (49 days)</constraintErrorMessage>
</properties>
</leafNode>
<leafNode name="tftp-server-name">
<properties>
<help>TFTP server name</help>
<valueHelp>
<format>ipv4</format>
<description>TFTP server IPv4 address</description>
</valueHelp>
<valueHelp>
<format>hostname</format>
<description>TFTP server FQDN</description>
</valueHelp>
<constraint>
<validator name="ipv4-address"/>
<validator name="fqdn"/>
</constraint>
</properties>
</leafNode>
<leafNode name="time-offset">
<properties>
<help>Client subnet offset in seconds from Coordinated Universal Time (UTC)</help>
<valueHelp>
<format>[-]N</format>
<description>Time offset (number, may be negative)</description>
</valueHelp>
<constraint>
<regex>-?[0-9]+</regex>
</constraint>
<constraintErrorMessage>Invalid time offset value</constraintErrorMessage>
</properties>
</leafNode>
<leafNode name="time-server">
<properties>
<help>IP address of time server</help>
<valueHelp>
<format>ipv4</format>
<description>Time server IPv4 address</description>
</valueHelp>
<constraint>
<validator name="ipv4-address"/>
</constraint>
<multi/>
</properties>
</leafNode>
<leafNode name="time-zone">
<properties>
<help>Time zone to send to clients. Uses RFC4833 options 100 and 101</help>
<completionHelp>
<script>timedatectl list-timezones</script>
</completionHelp>
<constraint>
<validator name="timezone" argument="--validate"/>
</constraint>
</properties>
</leafNode>
<node name="vendor-option">
<properties>
<help>Vendor Specific Options</help>
</properties>
<children>
<node name="ubiquiti">
<properties>
<help>Ubiquiti specific parameters</help>
</properties>
<children>
<leafNode name="unifi-controller">
<properties>
<help>Address of UniFi controller</help>
<valueHelp>
<format>ipv4</format>
<description>IP address of UniFi controller</description>
</valueHelp>
<constraint>
<validator name="ipv4-address"/>
</constraint>
</properties>
</leafNode>
</children>
</node>
</children>
</node>
<leafNode name="wins-server">
<properties>
<help>IP address for Windows Internet Name Service (WINS) server</help>
<valueHelp>
<format>ipv4</format>
<description>WINS server IPv4 address</description>
</valueHelp>
<constraint>
<validator name="ipv4-address"/>
</constraint>
<multi/>
</properties>
</leafNode>
<leafNode name="wpad-url">
<properties>
<help>Web Proxy Autodiscovery (WPAD) URL</help>
</properties>
</leafNode>
</children>
</tagNode>
</children>
</tagNode>
</children>
</node>
</children>
</node>
</interfaceDefinition>
diff --git a/interface-definitions/dhcpv6-server.xml.in b/interface-definitions/dhcpv6-server.xml.in
index b37f79434..10fdbf3f7 100644
--- a/interface-definitions/dhcpv6-server.xml.in
+++ b/interface-definitions/dhcpv6-server.xml.in
@@ -1,386 +1,375 @@
<?xml version="1.0"?>
<interfaceDefinition>
<node name="service">
<children>
<node name="dhcpv6-server" owner="${vyos_conf_scripts_dir}/dhcpv6_server.py">
<properties>
<help>DHCP for IPv6 (DHCPv6) server</help>
<priority>900</priority>
</properties>
<children>
#include <include/generic-disable-node.xml.i>
<node name="global-parameters">
<properties>
<help>Additional global parameters for DHCPv6 server</help>
</properties>
<children>
#include <include/name-server-ipv6.xml.i>
</children>
</node>
<leafNode name="preference">
<properties>
<help>Preference of this DHCPv6 server compared with others</help>
<valueHelp>
<format>u32:0-255</format>
<description>DHCPv6 server preference (0-255)</description>
</valueHelp>
<constraint>
<validator name="numeric" argument="--range 0-255"/>
</constraint>
<constraintErrorMessage>Preference must be between 0 and 255</constraintErrorMessage>
</properties>
</leafNode>
<tagNode name="shared-network-name">
<properties>
<help>DHCPv6 shared network name</help>
<constraint>
<regex>[-_a-zA-Z0-9.]+</regex>
</constraint>
<constraintErrorMessage>Invalid DHCPv6 shared network name. May only contain letters, numbers and .-_</constraintErrorMessage>
</properties>
<children>
#include <include/generic-disable-node.xml.i>
#include <include/generic-description.xml.i>
<leafNode name="interface">
<properties>
<help>Optional interface for this shared network to accept requests from</help>
<completionHelp>
<script>${vyos_completion_dir}/list_interfaces</script>
</completionHelp>
<valueHelp>
<format>txt</format>
<description>Interface name</description>
</valueHelp>
<constraint>
#include <include/constraint/interface-name.xml.i>
</constraint>
</properties>
</leafNode>
<node name="common-options">
<properties>
<help>Common options to distribute to all clients, including stateless clients</help>
</properties>
<children>
<leafNode name="info-refresh-time">
<properties>
<help>Time (in seconds) that stateless clients should wait between refreshing the information they were given</help>
<valueHelp>
<format>u32:1-4294967295</format>
<description>DHCPv6 information refresh time</description>
</valueHelp>
<constraint>
<validator name="numeric" argument="--range 1-4294967295"/>
</constraint>
</properties>
</leafNode>
#include <include/dhcp/domain-search.xml.i>
#include <include/name-server-ipv6.xml.i>
</children>
</node>
<tagNode name="subnet">
<properties>
<help>IPv6 DHCP subnet for this shared network</help>
<valueHelp>
<format>ipv6net</format>
<description>IPv6 address and prefix length</description>
</valueHelp>
<constraint>
<validator name="ipv6-prefix"/>
</constraint>
</properties>
<children>
<node name="address-range">
<properties>
<help>Parameters setting ranges for assigning IPv6 addresses</help>
</properties>
<children>
<leafNode name="prefix">
<properties>
<help>IPv6 prefix defining range of addresses to assign</help>
<valueHelp>
<format>ipv6net</format>
<description>IPv6 address and prefix length</description>
</valueHelp>
<constraint>
<validator name="ipv6-prefix"/>
</constraint>
<multi/>
</properties>
</leafNode>
<tagNode name="start">
<properties>
<help>First in range of consecutive IPv6 addresses to assign</help>
<valueHelp>
<format>ipv6</format>
<description>IPv6 address</description>
</valueHelp>
<constraint>
<validator name="ipv6-address"/>
</constraint>
</properties>
<children>
<leafNode name="stop">
<properties>
<help>Last in range of consecutive IPv6 addresses</help>
<valueHelp>
<format>ipv6</format>
<description>IPv6 address</description>
</valueHelp>
<constraint>
<validator name="ipv6-address"/>
</constraint>
</properties>
</leafNode>
</children>
</tagNode>
</children>
</node>
#include <include/dhcp/captive-portal.xml.i>
#include <include/dhcp/domain-search.xml.i>
<node name="lease-time">
<properties>
<help>Parameters relating to the lease time</help>
</properties>
<children>
<leafNode name="default">
<properties>
<help>Default time (in seconds) that will be assigned to a lease</help>
<valueHelp>
<format>u32:1-4294967295</format>
<description>DHCPv6 valid lifetime</description>
</valueHelp>
<constraint>
<validator name="numeric" argument="--range 1-4294967295"/>
</constraint>
</properties>
</leafNode>
<leafNode name="maximum">
<properties>
<help>Maximum time (in seconds) that will be assigned to a lease</help>
<valueHelp>
<format>u32:1-4294967295</format>
<description>Maximum lease time in seconds</description>
</valueHelp>
<constraint>
<validator name="numeric" argument="--range 1-4294967295"/>
</constraint>
</properties>
</leafNode>
<leafNode name="minimum">
<properties>
<help>Minimum time (in seconds) that will be assigned to a lease</help>
<valueHelp>
<format>u32:1-4294967295</format>
<description>Minimum lease time in seconds</description>
</valueHelp>
<constraint>
<validator name="numeric" argument="--range 1-4294967295"/>
</constraint>
</properties>
</leafNode>
</children>
</node>
#include <include/name-server-ipv6.xml.i>
<leafNode name="nis-domain">
<properties>
<help>NIS domain name for client to use</help>
<constraint>
<regex>[-_a-zA-Z0-9.]+</regex>
</constraint>
<constraintErrorMessage>Invalid NIS domain name</constraintErrorMessage>
</properties>
</leafNode>
<leafNode name="nis-server">
<properties>
<help>IPv6 address of a NIS Server</help>
<valueHelp>
<format>ipv6</format>
<description>IPv6 address of NIS server</description>
</valueHelp>
<constraint>
<validator name="ipv6-address"/>
</constraint>
<multi/>
</properties>
</leafNode>
<leafNode name="nisplus-domain">
<properties>
<help>NIS+ domain name for client to use</help>
<constraint>
<regex>[-_a-zA-Z0-9.]+</regex>
</constraint>
<constraintErrorMessage>Invalid NIS+ domain name. May only contain letters, numbers and .-_</constraintErrorMessage>
</properties>
</leafNode>
<leafNode name="nisplus-server">
<properties>
<help>IPv6 address of a NIS+ Server</help>
<valueHelp>
<format>ipv6</format>
<description>IPv6 address of NIS+ server</description>
</valueHelp>
<constraint>
<validator name="ipv6-address"/>
</constraint>
<multi/>
</properties>
</leafNode>
<node name="prefix-delegation">
<properties>
<help>Parameters relating to IPv6 prefix delegation</help>
</properties>
<children>
<tagNode name="prefix">
<properties>
<help>IPv6 prefix to be used in prefix delegation</help>
<valueHelp>
<format>ipv6</format>
<description>IPv6 prefix used in prefix delegation</description>
</valueHelp>
<constraint>
<validator name="ipv6-address"/>
</constraint>
</properties>
<children>
<leafNode name="prefix-length">
<properties>
<help>Length in bits of prefix</help>
<valueHelp>
<format>u32:32-64</format>
<description>Prefix length (32-64)</description>
</valueHelp>
<constraint>
<validator name="numeric" argument="--range 32-64"/>
</constraint>
<constraintErrorMessage>Prefix length must be between 32 and 64</constraintErrorMessage>
</properties>
</leafNode>
<leafNode name="delegated-length">
<properties>
<help>Length in bits of prefixes to be delegated</help>
<valueHelp>
<format>u32:32-64</format>
<description>Delegated prefix length (32-64)</description>
</valueHelp>
<constraint>
<validator name="numeric" argument="--range 32-96"/>
</constraint>
<constraintErrorMessage>Delegated prefix length must be between 32 and 96</constraintErrorMessage>
</properties>
</leafNode>
</children>
</tagNode>
</children>
</node>
<leafNode name="sip-server">
<properties>
<help>IPv6 address of SIP server</help>
<valueHelp>
<format>ipv6</format>
<description>IPv6 address of SIP server</description>
</valueHelp>
<valueHelp>
<format>hostname</format>
<description>FQDN of SIP server</description>
</valueHelp>
<constraint>
<validator name="ipv6-address"/>
<validator name="fqdn"/>
</constraint>
<multi/>
</properties>
</leafNode>
<leafNode name="sntp-server">
<properties>
<help>IPv6 address of an SNTP server for client to use</help>
<constraint>
<validator name="ipv6-address"/>
</constraint>
<multi/>
</properties>
</leafNode>
<tagNode name="static-mapping">
<properties>
- <help>Name of static mapping</help>
+ <help>Hostname for static mapping reservation</help>
<constraint>
- <regex>[-_a-zA-Z0-9.]+</regex>
+ <validator name="fqdn"/>
</constraint>
- <constraintErrorMessage>Invalid static mapping name. May only contain letters, numbers and .-_</constraintErrorMessage>
+ <constraintErrorMessage>Invalid static mapping hostname</constraintErrorMessage>
</properties>
<children>
#include <include/generic-disable-node.xml.i>
- <leafNode name="identifier">
- <properties>
- <help>Client identifier (DUID) for this static mapping</help>
- <valueHelp>
- <format>h[[:h]...]</format>
- <description>DUID: colon-separated hex list (as used by isc-dhcp option dhcpv6.client-id)</description>
- </valueHelp>
- <constraint>
- <regex>([0-9A-Fa-f]{1,2}[:])*([0-9A-Fa-f]{1,2})</regex>
- </constraint>
- <constraintErrorMessage>Invalid DUID, must be in the format h[[:h]...]</constraintErrorMessage>
- </properties>
- </leafNode>
+ #include <include/interface/mac.xml.i>
+ #include <include/interface/duid.xml.i>
<leafNode name="ipv6-address">
<properties>
<help>Client IPv6 address for this static mapping</help>
<valueHelp>
<format>ipv6</format>
<description>IPv6 address for this static mapping</description>
</valueHelp>
<constraint>
<validator name="ipv6-address"/>
</constraint>
</properties>
</leafNode>
<leafNode name="ipv6-prefix">
<properties>
<help>Client IPv6 prefix for this static mapping</help>
<valueHelp>
<format>ipv6net</format>
<description>IPv6 prefix for this static mapping</description>
</valueHelp>
<constraint>
<validator name="ipv6-prefix"/>
</constraint>
</properties>
</leafNode>
</children>
</tagNode>
<node name="vendor-option">
<properties>
<help>Vendor Specific Options</help>
</properties>
<children>
<node name="cisco">
<properties>
<help>Cisco specific parameters</help>
</properties>
<children>
<leafNode name="tftp-server">
<properties>
<help>TFTP server name</help>
<valueHelp>
<format>ipv6</format>
<description>TFTP server IPv6 address</description>
</valueHelp>
<constraint>
<validator name="ipv6-address"/>
</constraint>
<multi/>
</properties>
</leafNode>
</children>
</node>
</children>
</node>
</children>
</tagNode>
</children>
</tagNode>
</children>
</node>
</children>
</node>
</interfaceDefinition>
diff --git a/interface-definitions/include/version/dhcp-server-version.xml.i b/interface-definitions/include/version/dhcp-server-version.xml.i
index 7c4b5633e..cc84ea8b9 100644
--- a/interface-definitions/include/version/dhcp-server-version.xml.i
+++ b/interface-definitions/include/version/dhcp-server-version.xml.i
@@ -1,3 +1,3 @@
<!-- include start from include/version/dhcp-server-version.xml.i -->
-<syntaxVersion component='dhcp-server' version='7'></syntaxVersion>
+<syntaxVersion component='dhcp-server' version='8'></syntaxVersion>
<!-- include end -->
diff --git a/interface-definitions/include/version/dhcpv6-server-version.xml.i b/interface-definitions/include/version/dhcpv6-server-version.xml.i
index ae4178c90..cb026a54a 100644
--- a/interface-definitions/include/version/dhcpv6-server-version.xml.i
+++ b/interface-definitions/include/version/dhcpv6-server-version.xml.i
@@ -1,3 +1,3 @@
<!-- include start from include/version/dhcpv6-server-version.xml.i -->
-<syntaxVersion component='dhcpv6-server' version='2'></syntaxVersion>
+<syntaxVersion component='dhcpv6-server' version='3'></syntaxVersion>
<!-- include end -->
diff --git a/python/vyos/kea.py b/python/vyos/kea.py
index 4a517da5f..819fe16a9 100644
--- a/python/vyos/kea.py
+++ b/python/vyos/kea.py
@@ -1,328 +1,339 @@
# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
import json
import os
import socket
from datetime import datetime
from vyos.template import is_ipv6
from vyos.template import isc_static_route
from vyos.template import netmask_from_cidr
from vyos.utils.dict import dict_search_args
from vyos.utils.file import file_permissions
from vyos.utils.file import read_file
from vyos.utils.process import cmd
kea4_options = {
'name_server': 'domain-name-servers',
'domain_name': 'domain-name',
'domain_search': 'domain-search',
'ntp_server': 'ntp-servers',
'pop_server': 'pop-server',
'smtp_server': 'smtp-server',
'time_server': 'time-servers',
'wins_server': 'netbios-name-servers',
'default_router': 'routers',
'server_identifier': 'dhcp-server-identifier',
'tftp_server_name': 'tftp-server-name',
'bootfile_size': 'boot-size',
'time_offset': 'time-offset',
'wpad_url': 'wpad-url',
'ipv6_only_preferred': 'v6-only-preferred',
'captive_portal': 'v4-captive-portal'
}
kea6_options = {
'info_refresh_time': 'information-refresh-time',
'name_server': 'dns-servers',
'domain_search': 'domain-search',
'nis_domain': 'nis-domain-name',
'nis_server': 'nis-servers',
'nisplus_domain': 'nisp-domain-name',
'nisplus_server': 'nisp-servers',
'sntp_server': 'sntp-servers',
'captive_portal': 'v6-captive-portal'
}
def kea_parse_options(config):
options = []
for node, option_name in kea4_options.items():
if node not in config:
continue
value = ", ".join(config[node]) if isinstance(config[node], list) else config[node]
options.append({'name': option_name, 'data': value})
if 'client_prefix_length' in config:
options.append({'name': 'subnet-mask', 'data': netmask_from_cidr('0.0.0.0/' + config['client_prefix_length'])})
if 'ip_forwarding' in config:
options.append({'name': 'ip-forwarding', 'data': "true"})
if 'static_route' in config:
default_route = ''
if 'default_router' in config:
default_route = isc_static_route('0.0.0.0/0', config['default_router'])
routes = [isc_static_route(route, route_options['next_hop']) for route, route_options in config['static_route'].items()]
options.append({'name': 'rfc3442-static-route', 'data': ", ".join(routes if not default_route else routes + [default_route])})
options.append({'name': 'windows-static-route', 'data': ", ".join(routes)})
if 'time_zone' in config:
with open("/usr/share/zoneinfo/" + config['time_zone'], "rb") as f:
tz_string = f.read().split(b"\n")[-2].decode("utf-8")
options.append({'name': 'pcode', 'data': tz_string})
options.append({'name': 'tcode', 'data': config['time_zone']})
return options
def kea_parse_subnet(subnet, config):
out = {'subnet': subnet}
options = kea_parse_options(config)
if 'bootfile_name' in config:
out['boot-file-name'] = config['bootfile_name']
if 'bootfile_server' in config:
out['next-server'] = config['bootfile_server']
if 'lease' in config:
out['valid-lifetime'] = int(config['lease'])
out['max-valid-lifetime'] = int(config['lease'])
if 'range' in config:
pools = []
for num, range_config in config['range'].items():
start, stop = range_config['start'], range_config['stop']
pools.append({'pool': f'{start} - {stop}'})
out['pools'] = pools
if 'static_mapping' in config:
reservations = []
for host, host_config in config['static_mapping'].items():
if 'disable' in host_config:
continue
- obj = {
- 'hw-address': host_config['mac_address']
+ reservation = {
+ 'hostname': host,
}
+ if 'mac' in host_config:
+ reservation['hw-address'] = host_config['mac']
+
+ if 'duid' in host_config:
+ reservation['duid'] = host_config['duid']
+
if 'ip_address' in host_config:
- obj['ip-address'] = host_config['ip_address']
+ reservation['ip-address'] = host_config['ip_address']
- reservations.append(obj)
+ reservations.append(reservation)
out['reservations'] = reservations
unifi_controller = dict_search_args(config, 'vendor_option', 'ubiquiti', 'unifi_controller')
if unifi_controller:
options.append({
'name': 'unifi-controller',
'data': unifi_controller,
'space': 'ubnt'
})
if options:
out['option-data'] = options
return out
def kea6_parse_options(config):
options = []
if 'common_options' in config:
common_opt = config['common_options']
for node, option_name in kea6_options.items():
if node not in common_opt:
continue
value = ", ".join(common_opt[node]) if isinstance(common_opt[node], list) else common_opt[node]
options.append({'name': option_name, 'data': value})
for node, option_name in kea6_options.items():
if node not in config:
continue
value = ", ".join(config[node]) if isinstance(config[node], list) else config[node]
options.append({'name': option_name, 'data': value})
if 'sip_server' in config:
sip_servers = config['sip_server']
addrs = []
hosts = []
for server in sip_servers:
if is_ipv6(server):
addrs.append(server)
else:
hosts.append(server)
if addrs:
options.append({'name': 'sip-server-addr', 'data': ", ".join(addrs)})
-
+
if hosts:
options.append({'name': 'sip-server-dns', 'data': ", ".join(hosts)})
cisco_tftp = dict_search_args(config, 'vendor_option', 'cisco', 'tftp-server')
if cisco_tftp:
options.append({'name': 'tftp-servers', 'code': 2, 'space': 'cisco', 'data': cisco_tftp})
return options
def kea6_parse_subnet(subnet, config):
out = {'subnet': subnet}
options = kea6_parse_options(config)
if 'address_range' in config:
addr_range = config['address_range']
pools = []
if 'prefix' in addr_range:
for prefix in addr_range['prefix']:
pools.append({'pool': prefix})
if 'start' in addr_range:
for start, range_conf in addr_range['start'].items():
stop = range_conf['stop']
pools.append({'pool': f'{start} - {stop}'})
out['pools'] = pools
if 'prefix_delegation' in config:
pd_pools = []
if 'prefix' in config['prefix_delegation']:
for prefix, pd_conf in config['prefix_delegation']['prefix'].items():
pd_pools.append({
'prefix': prefix,
'prefix-len': int(pd_conf['prefix_length']),
'delegated-len': int(pd_conf['delegated_length'])
})
out['pd-pools'] = pd_pools
if 'lease_time' in config:
if 'default' in config['lease_time']:
out['valid-lifetime'] = int(config['lease_time']['default'])
if 'maximum' in config['lease_time']:
out['max-valid-lifetime'] = int(config['lease_time']['maximum'])
if 'minimum' in config['lease_time']:
out['min-valid-lifetime'] = int(config['lease_time']['minimum'])
if 'static_mapping' in config:
reservations = []
for host, host_config in config['static_mapping'].items():
if 'disable' in host_config:
continue
- reservation = {}
+ reservation = {
+ 'hostname': host
+ }
+
+ if 'mac' in host_config:
+ reservation['hw-address'] = host_config['mac']
- if 'identifier' in host_config:
- reservation['duid'] = host_config['identifier']
+ if 'duid' in host_config:
+ reservation['duid'] = host_config['duid']
if 'ipv6_address' in host_config:
reservation['ip-addresses'] = [ host_config['ipv6_address'] ]
if 'ipv6_prefix' in host_config:
reservation['prefixes'] = [ host_config['ipv6_prefix'] ]
reservations.append(reservation)
out['reservations'] = reservations
if options:
out['option-data'] = options
return out
def kea_parse_leases(lease_path):
contents = read_file(lease_path)
lines = contents.split("\n")
output = []
if len(lines) < 2:
return output
headers = lines[0].split(",")
for line in lines[1:]:
line_out = dict(zip(headers, line.split(",")))
lifetime = int(line_out['valid_lifetime'])
expiry = int(line_out['expire'])
line_out['start_timestamp'] = datetime.utcfromtimestamp(expiry - lifetime)
line_out['expire_timestamp'] = datetime.utcfromtimestamp(expiry) if expiry else None
output.append(line_out)
return output
def _ctrl_socket_command(path, command, args=None):
if not os.path.exists(path):
return None
if file_permissions(path) != '0775':
cmd(f'sudo chmod 775 {path}')
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
sock.connect(path)
payload = {'command': command}
if args:
payload['arguments'] = args
sock.send(bytes(json.dumps(payload), 'utf-8'))
result = b''
while True:
data = sock.recv(4096)
result += data
if len(data) < 4096:
break
return json.loads(result.decode('utf-8'))
def kea_get_active_config(inet):
ctrl_socket = f'/run/kea/dhcp{inet}-ctrl-socket'
config = _ctrl_socket_command(ctrl_socket, 'config-get')
-
+
if not config or 'result' not in config or config['result'] != 0:
return None
return config
def kea_get_pool_from_subnet_id(config, inet, subnet_id):
shared_networks = dict_search_args(config, 'arguments', f'Dhcp{inet}', 'shared-networks')
if not shared_networks:
return None
for network in shared_networks:
if f'subnet{inet}' not in network:
continue
for subnet in network[f'subnet{inet}']:
if 'id' in subnet and int(subnet['id']) == int(subnet_id):
return network['name']
return None
diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py
index 9f6e05ff3..bf0c09965 100755
--- a/smoketest/scripts/cli/test_service_dhcp-server.py
+++ b/smoketest/scripts/cli/test_service_dhcp-server.py
@@ -1,626 +1,631 @@
#!/usr/bin/env python3
#
# Copyright (C) 2020-2021 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 unittest
from json import loads
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
-from vyos.utils.dict import dict_search_recursive
from vyos.utils.process import process_named_running
from vyos.utils.file import read_file
-from vyos.template import address_from_cidr
from vyos.template import inc_ip
from vyos.template import dec_ip
-from vyos.template import netmask_from_cidr
PROCESS_NAME = 'kea-dhcp4'
CTRL_PROCESS_NAME = 'kea-ctrl-agent'
KEA4_CONF = '/run/kea/kea-dhcp4.conf'
KEA4_CTRL = '/run/kea/dhcp4-ctrl-socket'
base_path = ['service', 'dhcp-server']
subnet = '192.0.2.0/25'
router = inc_ip(subnet, 1)
dns_1 = inc_ip(subnet, 2)
dns_2 = inc_ip(subnet, 3)
domain_name = 'vyos.net'
class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
@classmethod
def setUpClass(cls):
super(TestServiceDHCPServer, cls).setUpClass()
+ # Clear out current configuration to allow running this test on a live system
+ cls.cli_delete(cls, base_path)
cidr_mask = subnet.split('/')[-1]
cls.cli_set(cls, ['interfaces', 'dummy', 'dum8765', 'address', f'{router}/{cidr_mask}'])
@classmethod
def tearDownClass(cls):
cls.cli_delete(cls, ['interfaces', 'dummy', 'dum8765'])
super(TestServiceDHCPServer, cls).tearDownClass()
def tearDown(self):
self.cli_delete(base_path)
self.cli_commit()
def walk_path(self, obj, path):
current = obj
for i, key in enumerate(path):
if isinstance(key, str):
self.assertTrue(isinstance(current, dict), msg=f'Failed path: {path}')
self.assertTrue(key in current, msg=f'Failed path: {path}')
elif isinstance(key, int):
self.assertTrue(isinstance(current, list), msg=f'Failed path: {path}')
self.assertTrue(0 <= key < len(current), msg=f'Failed path: {path}')
else:
assert False, "Invalid type"
current = current[key]
return current
def verify_config_object(self, obj, path, value):
base_obj = self.walk_path(obj, path)
self.assertTrue(isinstance(base_obj, list))
self.assertTrue(any(True for v in base_obj if v == value))
def verify_config_value(self, obj, path, key, value):
base_obj = self.walk_path(obj, path)
if isinstance(base_obj, list):
self.assertTrue(any(True for v in base_obj if key in v and v[key] == value))
elif isinstance(base_obj, dict):
self.assertTrue(key in base_obj)
self.assertEqual(base_obj[key], value)
def test_dhcp_single_pool_range(self):
shared_net_name = 'SMOKE-1'
range_0_start = inc_ip(subnet, 10)
range_0_stop = inc_ip(subnet, 20)
range_1_start = inc_ip(subnet, 40)
range_1_stop = inc_ip(subnet, 50)
pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
# we use the first subnet IP address as default gateway
self.cli_set(pool + ['default-router', router])
self.cli_set(pool + ['name-server', dns_1])
self.cli_set(pool + ['name-server', dns_2])
self.cli_set(pool + ['domain-name', domain_name])
# check validate() - No DHCP address range or active static-mapping set
with self.assertRaises(ConfigSessionError):
self.cli_commit()
self.cli_set(pool + ['range', '0', 'start', range_0_start])
self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
self.cli_set(pool + ['range', '1', 'start', range_1_start])
self.cli_set(pool + ['range', '1', 'stop', range_1_stop])
# commit changes
self.cli_commit()
config = read_file(KEA4_CONF)
obj = loads(config)
self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400)
self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400)
# Verify options
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
{'name': 'domain-name', 'data': domain_name})
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
{'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'})
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
{'name': 'routers', 'data': router})
# Verify pools
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
{'pool': f'{range_0_start} - {range_0_stop}'})
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
{'pool': f'{range_1_start} - {range_1_stop}'})
# Check for running process
self.assertTrue(process_named_running(PROCESS_NAME))
def test_dhcp_single_pool_options(self):
shared_net_name = 'SMOKE-0815'
range_0_start = inc_ip(subnet, 10)
range_0_stop = inc_ip(subnet, 20)
smtp_server = '1.2.3.4'
time_server = '4.3.2.1'
tftp_server = 'tftp.vyos.io'
search_domains = ['foo.vyos.net', 'bar.vyos.net']
bootfile_name = 'vyos'
bootfile_server = '192.0.2.1'
wpad = 'http://wpad.vyos.io/foo/bar'
server_identifier = bootfile_server
ipv6_only_preferred = '300'
pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
# we use the first subnet IP address as default gateway
self.cli_set(pool + ['default-router', router])
self.cli_set(pool + ['name-server', dns_1])
self.cli_set(pool + ['name-server', dns_2])
self.cli_set(pool + ['domain-name', domain_name])
self.cli_set(pool + ['ip-forwarding'])
self.cli_set(pool + ['smtp-server', smtp_server])
self.cli_set(pool + ['pop-server', smtp_server])
self.cli_set(pool + ['time-server', time_server])
self.cli_set(pool + ['tftp-server-name', tftp_server])
for search in search_domains:
self.cli_set(pool + ['domain-search', search])
self.cli_set(pool + ['bootfile-name', bootfile_name])
self.cli_set(pool + ['bootfile-server', bootfile_server])
self.cli_set(pool + ['wpad-url', wpad])
self.cli_set(pool + ['server-identifier', server_identifier])
self.cli_set(pool + ['static-route', '10.0.0.0/24', 'next-hop', '192.0.2.1'])
self.cli_set(pool + ['ipv6-only-preferred', ipv6_only_preferred])
self.cli_set(pool + ['time-zone', 'Europe/London'])
# check validate() - No DHCP address range or active static-mapping set
with self.assertRaises(ConfigSessionError):
self.cli_commit()
self.cli_set(pool + ['range', '0', 'start', range_0_start])
self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
# commit changes
self.cli_commit()
config = read_file(KEA4_CONF)
obj = loads(config)
self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'boot-file-name', bootfile_name)
self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'next-server', bootfile_server)
self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400)
self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400)
# Verify options
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
{'name': 'domain-name', 'data': domain_name})
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
{'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'})
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
{'name': 'domain-search', 'data': ', '.join(search_domains)})
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
{'name': 'pop-server', 'data': smtp_server})
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
{'name': 'smtp-server', 'data': smtp_server})
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
{'name': 'time-servers', 'data': time_server})
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
{'name': 'routers', 'data': router})
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
{'name': 'dhcp-server-identifier', 'data': server_identifier})
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
{'name': 'tftp-server-name', 'data': tftp_server})
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
{'name': 'wpad-url', 'data': wpad})
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
{'name': 'rfc3442-static-route', 'data': '24,10,0,0,192,0,2,1, 0,192,0,2,1'})
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
{'name': 'windows-static-route', 'data': '24,10,0,0,192,0,2,1'})
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
{'name': 'v6-only-preferred', 'data': ipv6_only_preferred})
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
{'name': 'ip-forwarding', 'data': "true"})
# Time zone
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
{'name': 'pcode', 'data': 'GMT0BST,M3.5.0/1,M10.5.0'})
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
{'name': 'tcode', 'data': 'Europe/London'})
# Verify pools
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
{'pool': f'{range_0_start} - {range_0_stop}'})
# Check for running process
self.assertTrue(process_named_running(PROCESS_NAME))
def test_dhcp_single_pool_static_mapping(self):
shared_net_name = 'SMOKE-2'
domain_name = 'private'
pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
# we use the first subnet IP address as default gateway
self.cli_set(pool + ['default-router', router])
self.cli_set(pool + ['name-server', dns_1])
self.cli_set(pool + ['name-server', dns_2])
self.cli_set(pool + ['domain-name', domain_name])
# check validate() - No DHCP address range or active static-mapping set
with self.assertRaises(ConfigSessionError):
self.cli_commit()
client_base = 10
for client in ['client1', 'client2', 'client3']:
mac = '00:50:00:00:00:{}'.format(client_base)
- self.cli_set(pool + ['static-mapping', client, 'mac-address', mac])
+ self.cli_set(pool + ['static-mapping', client, 'mac', mac])
self.cli_set(pool + ['static-mapping', client, 'ip-address', inc_ip(subnet, client_base)])
client_base += 1
+ # cannot have both mac-address and duid set
+ with self.assertRaises(ConfigSessionError):
+ self.cli_set(pool + ['static-mapping', 'client1', 'duid', '00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:11'])
+ self.cli_commit()
+ self.cli_delete(pool + ['static-mapping', 'client1', 'duid'])
+
# commit changes
self.cli_commit()
config = read_file(KEA4_CONF)
obj = loads(config)
self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400)
self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400)
# Verify options
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
{'name': 'domain-name', 'data': domain_name})
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
{'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'})
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
{'name': 'routers', 'data': router})
client_base = 10
for client in ['client1', 'client2', 'client3']:
mac = '00:50:00:00:00:{}'.format(client_base)
ip = inc_ip(subnet, client_base)
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'reservations'],
- {'hw-address': mac, 'ip-address': ip})
+ {'hostname': client, 'hw-address': mac, 'ip-address': ip})
client_base += 1
# Check for running process
self.assertTrue(process_named_running(PROCESS_NAME))
def test_dhcp_multiple_pools(self):
lease_time = '14400'
for network in ['0', '1', '2', '3']:
shared_net_name = f'VyOS-SMOKETEST-{network}'
subnet = f'192.0.{network}.0/24'
router = inc_ip(subnet, 1)
dns_1 = inc_ip(subnet, 2)
range_0_start = inc_ip(subnet, 10)
range_0_stop = inc_ip(subnet, 20)
range_1_start = inc_ip(subnet, 30)
range_1_stop = inc_ip(subnet, 40)
pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
# we use the first subnet IP address as default gateway
self.cli_set(pool + ['default-router', router])
self.cli_set(pool + ['name-server', dns_1])
self.cli_set(pool + ['domain-name', domain_name])
self.cli_set(pool + ['lease', lease_time])
self.cli_set(pool + ['range', '0', 'start', range_0_start])
self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
self.cli_set(pool + ['range', '1', 'start', range_1_start])
self.cli_set(pool + ['range', '1', 'stop', range_1_stop])
client_base = 60
for client in ['client1', 'client2', 'client3', 'client4']:
mac = '02:50:00:00:00:{}'.format(client_base)
- self.cli_set(pool + ['static-mapping', client, 'mac-address', mac])
+ self.cli_set(pool + ['static-mapping', client, 'mac', mac])
self.cli_set(pool + ['static-mapping', client, 'ip-address', inc_ip(subnet, client_base)])
client_base += 1
# commit changes
self.cli_commit()
config = read_file(KEA4_CONF)
obj = loads(config)
for network in ['0', '1', '2', '3']:
shared_net_name = f'VyOS-SMOKETEST-{network}'
subnet = f'192.0.{network}.0/24'
router = inc_ip(subnet, 1)
dns_1 = inc_ip(subnet, 2)
range_0_start = inc_ip(subnet, 10)
range_0_stop = inc_ip(subnet, 20)
range_1_start = inc_ip(subnet, 30)
range_1_stop = inc_ip(subnet, 40)
self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'subnet', subnet)
self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'valid-lifetime', int(lease_time))
self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'max-valid-lifetime', int(lease_time))
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'],
{'name': 'domain-name', 'data': domain_name})
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'],
{'name': 'domain-name-servers', 'data': dns_1})
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'],
{'name': 'routers', 'data': router})
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'pools'],
{'pool': f'{range_0_start} - {range_0_stop}'})
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'pools'],
{'pool': f'{range_1_start} - {range_1_stop}'})
client_base = 60
for client in ['client1', 'client2', 'client3', 'client4']:
mac = '02:50:00:00:00:{}'.format(client_base)
ip = inc_ip(subnet, client_base)
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'reservations'],
- {'hw-address': mac, 'ip-address': ip})
+ {'hostname': client, 'hw-address': mac, 'ip-address': ip})
client_base += 1
# Check for running process
self.assertTrue(process_named_running(PROCESS_NAME))
def test_dhcp_exclude_not_in_range(self):
# T3180: verify else path when slicing DHCP ranges and exclude address
# is not part of the DHCP range
range_0_start = inc_ip(subnet, 10)
range_0_stop = inc_ip(subnet, 20)
pool = base_path + ['shared-network-name', 'EXCLUDE-TEST', 'subnet', subnet]
self.cli_set(pool + ['default-router', router])
self.cli_set(pool + ['exclude', router])
self.cli_set(pool + ['range', '0', 'start', range_0_start])
self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
# commit changes
self.cli_commit()
config = read_file(KEA4_CONF)
obj = loads(config)
self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'EXCLUDE-TEST')
self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
# Verify options
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
{'name': 'routers', 'data': router})
# Verify pools
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
{'pool': f'{range_0_start} - {range_0_stop}'})
# Check for running process
self.assertTrue(process_named_running(PROCESS_NAME))
def test_dhcp_exclude_in_range(self):
# T3180: verify else path when slicing DHCP ranges and exclude address
# is not part of the DHCP range
range_0_start = inc_ip(subnet, 10)
range_0_stop = inc_ip(subnet, 100)
# the DHCP exclude addresse is blanked out of the range which is done
# by slicing one range into two ranges
exclude_addr = inc_ip(range_0_start, 20)
range_0_stop_excl = dec_ip(exclude_addr, 1)
range_0_start_excl = inc_ip(exclude_addr, 1)
pool = base_path + ['shared-network-name', 'EXCLUDE-TEST-2', 'subnet', subnet]
self.cli_set(pool + ['default-router', router])
self.cli_set(pool + ['exclude', exclude_addr])
self.cli_set(pool + ['range', '0', 'start', range_0_start])
self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
# commit changes
self.cli_commit()
config = read_file(KEA4_CONF)
obj = loads(config)
self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'EXCLUDE-TEST-2')
self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
# Verify options
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
{'name': 'routers', 'data': router})
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
{'pool': f'{range_0_start} - {range_0_stop_excl}'})
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
{'pool': f'{range_0_start_excl} - {range_0_stop}'})
# Check for running process
self.assertTrue(process_named_running(PROCESS_NAME))
def test_dhcp_relay_server(self):
# Listen on specific address and return DHCP leases from a non
# directly connected pool
self.cli_set(base_path + ['listen-address', router])
relay_subnet = '10.0.0.0/16'
relay_router = inc_ip(relay_subnet, 1)
range_0_start = '10.0.1.0'
range_0_stop = '10.0.250.255'
pool = base_path + ['shared-network-name', 'RELAY', 'subnet', relay_subnet]
self.cli_set(pool + ['default-router', relay_router])
self.cli_set(pool + ['range', '0', 'start', range_0_start])
self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
# commit changes
self.cli_commit()
config = read_file(KEA4_CONF)
obj = loads(config)
self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'RELAY')
self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', relay_subnet)
# Verify options
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
{'name': 'routers', 'data': relay_router})
# Verify pools
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
{'pool': f'{range_0_start} - {range_0_stop}'})
# Check for running process
self.assertTrue(process_named_running(PROCESS_NAME))
def test_dhcp_failover(self):
shared_net_name = 'FAILOVER'
failover_name = 'VyOS-Failover'
range_0_start = inc_ip(subnet, 10)
range_0_stop = inc_ip(subnet, 20)
pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
# we use the first subnet IP address as default gateway
self.cli_set(pool + ['default-router', router])
# check validate() - No DHCP address range or active static-mapping set
with self.assertRaises(ConfigSessionError):
self.cli_commit()
self.cli_set(pool + ['range', '0', 'start', range_0_start])
self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
# failover
failover_local = router
failover_remote = inc_ip(router, 1)
self.cli_set(base_path + ['failover', 'source-address', failover_local])
self.cli_set(base_path + ['failover', 'name', failover_name])
self.cli_set(base_path + ['failover', 'remote', failover_remote])
self.cli_set(base_path + ['failover', 'status', 'primary'])
# commit changes
self.cli_commit()
config = read_file(KEA4_CONF)
obj = loads(config)
# Verify failover
self.verify_config_value(obj, ['Dhcp4', 'control-socket'], 'socket-name', KEA4_CTRL)
self.verify_config_object(
obj,
['Dhcp4', 'hooks-libraries', 0, 'parameters', 'high-availability', 0, 'peers'],
{'name': os.uname()[1], 'url': f'http://{failover_local}:647/', 'role': 'primary', 'auto-failover': True})
self.verify_config_object(
obj,
['Dhcp4', 'hooks-libraries', 0, 'parameters', 'high-availability', 0, 'peers'],
{'name': failover_name, 'url': f'http://{failover_remote}:647/', 'role': 'standby', 'auto-failover': True})
self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
# Verify options
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
{'name': 'routers', 'data': router})
# Verify pools
self.verify_config_object(
obj,
['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
{'pool': f'{range_0_start} - {range_0_stop}'})
# Check for running process
self.assertTrue(process_named_running(PROCESS_NAME))
self.assertTrue(process_named_running(CTRL_PROCESS_NAME))
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 175a67537..f163cc69a 100755
--- a/smoketest/scripts/cli/test_service_dhcpv6-server.py
+++ b/smoketest/scripts/cli/test_service_dhcpv6-server.py
@@ -1,264 +1,273 @@
#!/usr/bin/env python3
#
# Copyright (C) 2020-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/>.
import unittest
from json import loads
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 = 'kea-dhcp6'
KEA6_CONF = '/run/kea/kea-dhcp6.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()
+ # Clear out current configuration to allow running this test on a live system
+ cls.cli_delete(cls, base_path)
+
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 walk_path(self, obj, path):
current = obj
for i, key in enumerate(path):
if isinstance(key, str):
self.assertTrue(isinstance(current, dict), msg=f'Failed path: {path}')
self.assertTrue(key in current, msg=f'Failed path: {path}')
elif isinstance(key, int):
self.assertTrue(isinstance(current, list), msg=f'Failed path: {path}')
self.assertTrue(0 <= key < len(current), msg=f'Failed path: {path}')
else:
assert False, "Invalid type"
current = current[key]
return current
def verify_config_object(self, obj, path, value):
base_obj = self.walk_path(obj, path)
self.assertTrue(isinstance(base_obj, list))
self.assertTrue(any(True for v in base_obj if v == value))
def verify_config_value(self, obj, path, key, value):
base_obj = self.walk_path(obj, path)
if isinstance(base_obj, list):
self.assertTrue(any(True for v in base_obj if key in v and v[key] == value))
elif isinstance(base_obj, dict):
self.assertTrue(key in base_obj)
self.assertEqual(base_obj[key], value)
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])
+ duid = f'00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:{client_base:02}'
+ self.cli_set(pool + ['static-mapping', client, 'duid', duid])
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
+ # cannot have both mac-address and duid set
+ with self.assertRaises(ConfigSessionError):
+ self.cli_set(pool + ['static-mapping', 'client1', 'mac', '00:50:00:00:00:11'])
+ self.cli_commit()
+ self.cli_delete(pool + ['static-mapping', 'client1', 'mac'])
+
# commit changes
self.cli_commit()
config = read_file(KEA6_CONF)
obj = loads(config)
self.verify_config_value(obj, ['Dhcp6', 'shared-networks'], 'name', shared_net_name)
self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'subnet', subnet)
self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'valid-lifetime', int(lease_time))
self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'min-valid-lifetime', int(min_lease_time))
self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'max-valid-lifetime', int(max_lease_time))
# Verify options
self.verify_config_object(
obj,
['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'],
{'name': 'dns-servers', 'data': f'{dns_1}, {dns_2}'})
self.verify_config_object(
obj,
['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'],
{'name': 'domain-search', 'data': ", ".join(search_domains)})
self.verify_config_object(
obj,
['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'],
{'name': 'nis-domain-name', 'data': domain})
self.verify_config_object(
obj,
['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'],
{'name': 'nis-servers', 'data': ", ".join(nis_servers)})
self.verify_config_object(
obj,
['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'],
{'name': 'nisp-domain-name', 'data': domain})
self.verify_config_object(
obj,
['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'],
{'name': 'nisp-servers', 'data': ", ".join(nis_servers)})
self.verify_config_object(
obj,
['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'],
{'name': 'sntp-servers', 'data': sntp_server})
self.verify_config_object(
obj,
['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'],
{'name': 'sip-server-dns', 'data': sip_server})
# Verify pools
self.verify_config_object(
obj,
['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'pools'],
{'pool': f'{range_start} - {range_stop}'})
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)
+ duid = f'00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:{client_base:02}'
ip = inc_ip(subnet, client_base)
prefix = inc_ip(subnet, client_base << 64) + '/64'
self.verify_config_object(
obj,
['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'reservations'],
- {'duid': cid, 'ip-addresses': [ip], 'prefixes': [prefix]})
+ {'hostname': client, 'duid': duid, 'ip-addresses': [ip], 'prefixes': [prefix]})
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_len = '64'
prefix_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', 'prefix', delegate_start, 'delegated-length', delegate_len])
self.cli_set(pool + ['prefix-delegation', 'prefix', delegate_start, 'prefix-length', prefix_len])
# commit changes
self.cli_commit()
config = read_file(KEA6_CONF)
obj = loads(config)
self.verify_config_value(obj, ['Dhcp6', 'shared-networks'], 'name', shared_net_name)
self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'subnet', subnet)
# Verify pools
self.verify_config_object(
obj,
['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'pools'],
{'pool': f'{range_start} - {range_stop}'})
self.verify_config_object(
obj,
['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'pd-pools'],
{'prefix': delegate_start, 'prefix-len': int(prefix_len), 'delegated-len': int(delegate_len)})
# 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(KEA6_CONF)
obj = loads(config)
self.verify_config_value(obj, ['Dhcp6', 'shared-networks'], 'name', shared_net_name)
self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'subnet', subnet)
self.verify_config_object(
obj,
['Dhcp6', 'option-data'],
{'name': 'dns-servers', "code": 23, "space": "dhcp6", "csv-format": True, 'data': f'{ns_global_1}, {ns_global_2}'})
# Check for running process
self.assertTrue(process_named_running(PROCESS_NAME))
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py
index e06a3c8a8..c1308cda7 100755
--- a/src/conf_mode/dhcp_server.py
+++ b/src/conf_mode/dhcp_server.py
@@ -1,376 +1,376 @@
#!/usr/bin/env python3
#
# Copyright (C) 2018-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/>.
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.config import Config
from vyos.pki import wrap_certificate
from vyos.pki import wrap_private_key
from vyos.template import render
from vyos.utils.dict import dict_search
from vyos.utils.dict import dict_search_args
from vyos.utils.file import write_file
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()
ctrl_config_file = '/run/kea/kea-ctrl-agent.conf'
ctrl_socket = '/run/kea/dhcp4-ctrl-socket'
config_file = '/run/kea/kea-dhcp4.conf'
lease_file = '/config/dhcp4.leases'
systemd_override = r'/run/systemd/system/kea-ctrl-agent.service.d/10-override.conf'
ca_cert_file = '/run/kea/kea-failover-ca.pem'
cert_file = '/run/kea/kea-failover.pem'
cert_key_file = '/run/kea/kea-failover-key.pem'
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 dict_search('failover.certificate', dhcp):
- dhcp['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True)
+ dhcp['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True)
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
# A shared-network requires a subnet definition
for network, network_config in dhcp['shared_network_name'].items():
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():
# 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!')
# 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)
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 ('mac' not in mapping_config and 'duid' not in mapping_config) or \
+ ('mac' in mapping_config and 'duid' in mapping_config):
+ raise ConfigError(f'Either MAC address or Client identifier (DUID) is required for '
+ f'static mapping "{mapping}" within shared-network "{network}, {subnet}"!')
# 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 'failover' in dhcp:
for key in ['name', 'remote', 'source_address', 'status']:
if key not in dhcp['failover']:
tmp = key.replace('_', '-')
raise ConfigError(f'DHCP failover requires "{tmp}" to be specified!')
if len({'certificate', 'ca_certificate'} & set(dhcp['failover'])) == 1:
raise ConfigError(f'DHCP secured failover requires both certificate and CA certificate')
if 'certificate' in dhcp['failover']:
cert_name = dhcp['failover']['certificate']
if cert_name not in dhcp['pki']['certificate']:
raise ConfigError(f'Invalid certificate specified for DHCP failover')
if not dict_search_args(dhcp['pki']['certificate'], cert_name, 'certificate'):
raise ConfigError(f'Invalid certificate specified for DHCP failover')
if not dict_search_args(dhcp['pki']['certificate'], cert_name, 'private', 'key'):
raise ConfigError(f'Missing private key on certificate specified for DHCP failover')
if 'ca_certificate' in dhcp['failover']:
ca_cert_name = dhcp['failover']['ca_certificate']
if ca_cert_name not in dhcp['pki']['ca']:
raise ConfigError(f'Invalid CA certificate specified for DHCP failover')
if not dict_search_args(dhcp['pki']['ca'], ca_cert_name, 'certificate'):
raise ConfigError(f'Invalid CA certificate specified for DHCP failover')
for address in (dict_search('listen_address', dhcp) or []):
if is_addr_assigned(address):
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
dhcp['lease_file'] = lease_file
dhcp['machine'] = os.uname().machine
if not os.path.exists(lease_file):
write_file(lease_file, '', user='_kea', group='vyattacfg', mode=0o755)
for f in [cert_file, cert_key_file, ca_cert_file]:
if os.path.exists(f):
os.unlink(f)
if 'failover' in dhcp:
if 'certificate' in dhcp['failover']:
cert_name = dhcp['failover']['certificate']
cert_data = dhcp['pki']['certificate'][cert_name]['certificate']
key_data = dhcp['pki']['certificate'][cert_name]['private']['key']
write_file(cert_file, wrap_certificate(cert_data), user='_kea', mode=0o600)
write_file(cert_key_file, wrap_private_key(key_data), user='_kea', mode=0o600)
dhcp['failover']['cert_file'] = cert_file
dhcp['failover']['cert_key_file'] = cert_key_file
if 'ca_certificate' in dhcp['failover']:
ca_cert_name = dhcp['failover']['ca_certificate']
ca_cert_data = dhcp['pki']['ca'][ca_cert_name]['certificate']
write_file(ca_cert_file, wrap_certificate(ca_cert_data), user='_kea', mode=0o600)
dhcp['failover']['ca_cert_file'] = ca_cert_file
render(systemd_override, 'dhcp-server/10-override.conf.j2', dhcp)
render(ctrl_config_file, 'dhcp-server/kea-ctrl-agent.conf.j2', dhcp)
render(config_file, 'dhcp-server/kea-dhcp4.conf.j2', dhcp)
return None
def apply(dhcp):
services = ['kea-ctrl-agent', 'kea-dhcp4-server', 'kea-dhcp-ddns-server']
if not dhcp or 'disable' in dhcp:
for service in services:
call(f'systemctl stop {service}.service')
if os.path.exists(config_file):
os.unlink(config_file)
return None
for service in services:
action = 'restart'
if service == 'kea-dhcp-ddns-server' and 'dynamic_dns_update' not in dhcp:
action = 'stop'
if service == 'kea-ctrl-agent' and 'failover' not in dhcp:
action = 'stop'
call(f'systemctl {action} {service}.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/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py
index b01f510e5..f9da3d84a 100755
--- a/src/conf_mode/dhcpv6_server.py
+++ b/src/conf_mode/dhcpv6_server.py
@@ -1,208 +1,213 @@
#!/usr/bin/env python3
#
# Copyright (C) 2018-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/>.
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.file import write_file
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/kea/kea-dhcp6.conf'
ctrl_socket = '/run/kea/dhcp6-ctrl-socket'
lease_file = '/config/dhcp6.leases'
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 'prefix' not in subnet_config['prefix_delegation']:
raise ConfigError('prefix-delegation prefix not defined!')
for prefix, prefix_config in subnet_config['prefix_delegation']['prefix'].items():
if 'delegated_length' not in prefix_config:
raise ConfigError(f'Delegated IPv6 prefix length for "{prefix}" '\
f'must be configured')
if 'prefix_length' not in prefix_config:
raise ConfigError('Length of delegated IPv6 prefix must be configured')
if prefix_config['prefix_length'] > prefix_config['delegated_length']:
raise ConfigError('Length of delegated IPv6 prefix must be within parent prefix')
# 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 ('mac' not in mapping_config and 'duid' not in mapping_config) or \
+ ('mac' in mapping_config and 'duid' in mapping_config):
+ raise ConfigError(f'Either MAC address or Client identifier (DUID) is required for '
+ f'static mapping "{mapping}" within shared-network "{network}, {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
dhcpv6['lease_file'] = lease_file
dhcpv6['machine'] = os.uname().machine
if not os.path.exists(lease_file):
write_file(lease_file, '', user='_kea', group='vyattacfg', mode=0o755)
render(config_file, 'dhcp-server/kea-dhcp6.conf.j2', dhcpv6)
return None
def apply(dhcpv6):
# bail out early - looks like removal from running config
service_name = 'kea-dhcp6-server.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/migration-scripts/dhcp-server/7-to-8 b/src/migration-scripts/dhcp-server/7-to-8
new file mode 100755
index 000000000..151aa6d7b
--- /dev/null
+++ b/src/migration-scripts/dhcp-server/7-to-8
@@ -0,0 +1,65 @@
+#!/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/>.
+
+# T3316:
+# - Adjust hostname to have valid FQDN characters only (underscores aren't allowed anymore)
+# - Rename "service dhcp-server shared-network-name ... static-mapping <hostname> mac-address ..."
+# to "service dhcp-server shared-network-name ... static-mapping <hostname> mac ..."
+
+import sys
+import re
+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()
+
+base = ['service', 'dhcp-server', 'shared-network-name']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
+
+for network in config.list_nodes(base):
+ # Run this for every specified 'subnet'
+ if config.exists(base + [network, 'subnet']):
+ for subnet in config.list_nodes(base + [network, 'subnet']):
+ base_subnet = base + [network, 'subnet', subnet]
+ if config.exists(base_subnet + ['static-mapping']):
+ for hostname in config.list_nodes(base_subnet + ['static-mapping']):
+ base_mapping = base_subnet + ['static-mapping', hostname]
+
+ # Rename the 'mac-address' node to 'mac'
+ if config.exists(base_mapping + ['mac-address']):
+ config.rename(base_mapping + ['mac-address'], 'mac')
+
+ # Adjust hostname to have valid FQDN characters only
+ new_hostname = re.sub(r'[^a-zA-Z0-9-.]', '-', hostname)
+ if new_hostname != hostname:
+ config.rename(base_mapping, new_hostname)
+
+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/dhcpv6-server/2-to-3 b/src/migration-scripts/dhcpv6-server/2-to-3
new file mode 100755
index 000000000..f4bdc1d1e
--- /dev/null
+++ b/src/migration-scripts/dhcpv6-server/2-to-3
@@ -0,0 +1,78 @@
+#!/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/>.
+
+# T3316:
+# - Adjust hostname to have valid FQDN characters only (underscores aren't allowed anymore)
+# - Adjust duid (old identifier) to comply with duid format
+# - Rename "service dhcpv6-server shared-network-name ... static-mapping <hostname> identifier ..."
+# to "service dhcpv6-server shared-network-name ... static-mapping <hostname> duid ..."
+# - Rename "service dhcpv6-server shared-network-name ... static-mapping <hostname> mac-address ..."
+# to "service dhcpv6-server shared-network-name ... static-mapping <hostname> mac ..."
+
+import sys
+import re
+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()
+
+base = ['service', 'dhcpv6-server', 'shared-network-name']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
+
+for network in config.list_nodes(base):
+ # Run this for every specified 'subnet'
+ if config.exists(base + [network, 'subnet']):
+ for subnet in config.list_nodes(base + [network, 'subnet']):
+ base_subnet = base + [network, 'subnet', subnet]
+ if config.exists(base_subnet + ['static-mapping']):
+ for hostname in config.list_nodes(base_subnet + ['static-mapping']):
+ base_mapping = base_subnet + ['static-mapping', hostname]
+ if config.exists(base_mapping + ['identifier']):
+
+ # Adjust duid to comply with duid format (a:3:b:04:... => 0a:03:0b:04:...)
+ duid = config.return_value(base_mapping + ['identifier'])
+ new_duid = ':'.join(x.rjust(2,'0') for x in duid.split(':'))
+ if new_duid != duid:
+ config.set(base_mapping + ['identifier'], new_duid)
+
+ # Rename the 'identifier' node to 'duid'
+ config.rename(base_mapping + ['identifier'], 'duid')
+
+ # Rename the 'mac-address' node to 'mac'
+ if config.exists(base_mapping + ['mac-address']):
+ config.rename(base_mapping + ['mac-address'], 'mac')
+
+ # Adjust hostname to have valid FQDN characters only
+ new_hostname = re.sub(r'[^a-zA-Z0-9-.]', '-', hostname)
+ if new_hostname != hostname:
+ config.rename(base_mapping, new_hostname)
+
+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)

File Metadata

Mime Type
text/x-diff
Expires
Fri, Nov 6, 6:18 AM (3 h, 57 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
4652694
Default Alt Text
(125 KB)

Event Timeline