Page Menu
Home
VyOS Platform
Search
Configure Global Search
Log In
Files
F119062166
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
125 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
rVYOSONEX vyos-1x
Attached
Detach File
Event Timeline
Log In to Comment