Page Menu
Home
VyOS Platform
Search
Configure Global Search
Log In
Files
F38930287
dhcp_server.py
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
37 KB
Referenced Files
None
Subscribers
None
dhcp_server.py
View Options
#!/usr/bin/env python3
#
# Copyright (C) 2018 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
sys
import
os
import
ipaddress
import
jinja2
import
socket
import
struct
import
vyos.validate
from
vyos.config
import
Config
from
vyos
import
ConfigError
config_file
=
r'/etc/dhcp/dhcpd.conf'
lease_file
=
r'/config/dhcpd.leases'
daemon_config_file
=
r'/etc/default/isc-dhcp-server'
# Please be careful if you edit the template.
config_tmpl
=
"""
### Autogenerated by dhcp_server.py ###
# For options please consult the following website:
# https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html
#
# log-facility local7;
{% if hostfile_update %}
on release {
set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name);
set ClientIp = binary-to-ascii(10, 8, ".",leased-address);
set ClientMac = binary-to-ascii(16, 8, ":",substring(hardware, 1, 6));
set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!");
execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", ClientName, ClientIp, ClientMac, ClientDomain);
}
on expiry {
set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name);
set ClientIp = binary-to-ascii(10, 8, ".",leased-address);
set ClientMac = binary-to-ascii(16, 8, ":",substring(hardware, 1, 6));
set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!");
execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", ClientName, ClientIp, ClientMac, ClientDomain);
}
{% endif %}
{%- if host_decl_name %}
use-host-decl-names on;
{%- endif %}
ddns-update-style {% if ddns_enable -%} interim {%- else -%} none {%- endif %};
{% if static_route -%}
option rfc3442-static-route code 121 = array of integer 8;
option windows-static-route code 249 = array of integer 8;
{%- endif %}
{% if wpad -%}
option wpad-url code 252 = text;
{% endif %}
{%- if global_parameters %}
# The following {{ global_parameters | length }} line(s) were added as global-parameters in the CLI and have not been validated
{%- for param in global_parameters %}
{{ param }}
{%- endfor -%}
{%- endif %}
# Failover configuration
{% for network in shared_network %}
{%- if not network.disabled -%}
{%- for subnet in network.subnet %}
{%- if subnet.failover_name -%}
failover peer "{{ subnet.failover_name }}" {
{%- if subnet.failover_status == 'primary' %}
primary;
mclt 1800;
split 128;
{%- elif subnet.failover_status == 'secondary' %}
secondary;
{%- endif %}
address {{ subnet.failover_local_addr }};
port 520;
peer address {{ subnet.failover_peer_addr }};
peer port 520;
max-response-delay 30;
max-unacked-updates 10;
load balance max seconds 3;
}
{% endif -%}
{% endfor -%}
{% endif -%}
{% endfor %}
# Shared network configration(s)
{% for network in shared_network %}
{%- if not network.disabled -%}
shared-network {{ network.name }} {
{%- if network.authoritative %}
authoritative;
{%- endif %}
{%- if network.network_parameters %}
# The following {{ network.network_parameters | length }} line(s) were added as shared-network-parameters in the CLI and have not been validated
{%- for param in network.network_parameters %}
{{ param }}
{%- endfor %}
{%- endif %}
{%- for subnet in network.subnet %}
subnet {{ subnet.address }} netmask {{ subnet.netmask }} {
{%- if subnet.dns_server %}
option domain-name-servers {{ subnet.dns_server | join(', ') }};
{%- endif %}
{%- if subnet.domain_search %}
option domain-search {{ subnet.domain_search | join(', ') }};
{%- endif %}
{%- if subnet.ntp_server %}
option ntp-servers {{ subnet.ntp_server | join(', ') }};
{%- endif %}
{%- if subnet.pop_server %}
option pop-server {{ subnet.pop_server | join(', ') }};
{%- endif %}
{%- if subnet.smtp_server %}
option smtp-server {{ subnet.smtp_server | join(', ') }};
{%- endif %}
{%- if subnet.time_server %}
option time-servers {{ subnet.time_server | join(', ') }};
{%- endif %}
{%- if subnet.wins_server %}
option netbios-name-servers {{ subnet.wins_server | join(', ') }};
{%- endif %}
{%- if subnet.static_route %}
option rfc3442-static-route {{ subnet.static_route }};
option windows-static-route {{ subnet.static_route }};
{%- endif %}
{%- if subnet.ip_forwarding %}
option ip-forwarding true;
{%- endif -%}
{%- if subnet.default_router %}
option routers {{ subnet.default_router }};
{%- endif -%}
{%- if subnet.server_identifier %}
option dhcp-server-identifier {{ subnet.server_identifier }};
{%- endif -%}
{%- if subnet.domain_name %}
option domain-name "{{ subnet.domain_name }}";
{%- endif -%}
{%- if subnet.subnet_parameters %}
# The following {{ subnet.subnet_parameters | length }} line(s) were added as subnet-parameters in the CLI and have not been validated
{%- for param in subnet.subnet_parameters %}
{{ param }}
{%- endfor -%}
{%- endif %}
{%- if subnet.tftp_server %}
option tftp-server-name "{{ subnet.tftp_server }}";
{%- endif -%}
{%- if subnet.bootfile_name %}
option bootfile-name "{{ subnet.bootfile_name }}";
filename "{{ subnet.bootfile_name }}";
{%- endif -%}
{%- if subnet.bootfile_server %}
next-server {{ subnet.bootfile_server }};
{%- endif -%}
{%- if subnet.time_offset %}
option time-offset {{ subnet.time_offset }};
{%- endif -%}
{%- if subnet.wpad_url %}
option wpad-url "{{ subnet.wpad_url }}";
{%- endif -%}
{%- if subnet.client_prefix_length %}
option subnet-mask {{ subnet.client_prefix_length }};
{%- endif -%}
{% if subnet.lease %}
default-lease-time {{ subnet.lease }};
max-lease-time {{ subnet.lease }};
{%- endif -%}
{%- for host in subnet.static_mapping %}
{% if not host.disabled -%}
host {% if host_decl_name -%} {{ host.name }} {%- else -%} {{ network.name }}_{{ host.name }} {%- endif %} {
{%- if host.ip_address %}
fixed-address {{ host.ip_address }};
{%- endif %}
hardware ethernet {{ host.mac_address }};
{%- if host.static_parameters %}
# The following {{ host.static_parameters | length }} line(s) were added as static-mapping-parameters in the CLI and have not been validated
{%- for param in host.static_parameters %}
{{ param }}
{%- endfor -%}
{%- endif %}
}
{%- endif %}
{%- endfor %}
{%- if subnet.failover_name %}
pool {
failover peer "{{ subnet.failover_name }}";
deny dynamic bootp clients;
{%- for range in subnet.range %}
range {{ range.start }} {{ range.stop }};
{%- endfor %}
}
{%- else %}
{%- for range in subnet.range %}
range {{ range.start }} {{ range.stop }};
{%- endfor %}
{%- endif %}
}
{%- endfor %}
on commit {
set shared-networkname = "{{ network.name }}";
{% if hostfile_update -%}
set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name);
set ClientIp = binary-to-ascii(10, 8, ".", leased-address);
set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 6));
set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!");
execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "commit", ClientName, ClientIp, ClientMac, ClientDomain);
{%- endif %}
}
}
{%- endif %}
{% endfor %}
"""
daemon_tmpl
=
"""
### Autogenerated by dhcp_server.py ###
# sourced by /etc/init.d/isc-dhcp-server
DHCPD_CONF=/etc/dhcp/dhcpd.conf
DHCPD_PID=/var/run/dhcpd.pid
OPTIONS="-4 -lf {{ lease_file }}"
INTERFACES=""
"""
default_config_data
=
{
'lease_file'
:
lease_file
,
'disabled'
:
False
,
'ddns_enable'
:
False
,
'global_parameters'
:
[],
'hostfile_update'
:
False
,
'host_decl_name'
:
False
,
'static_route'
:
False
,
'wpad'
:
False
,
'shared_network'
:
[],
}
def
get_config
():
dhcp
=
default_config_data
conf
=
Config
()
if
not
conf
.
exists
(
'service dhcp-server'
):
return
None
else
:
conf
.
set_level
(
'service dhcp-server'
)
# check for global disable of DHCP service
if
conf
.
exists
(
'disable'
):
dhcp
[
'disabled'
]
=
True
# check for global dynamic DNS upste
if
conf
.
exists
(
'dynamic-dns-update'
):
dhcp
[
'ddns_enable'
]
=
True
# HACKS AND TRICKS
#
# check for global 'raw' ISC DHCP parameters configured by users
# actually this is a bad idea in general to pass raw parameters from any user
if
conf
.
exists
(
'global-parameters'
):
dhcp
[
'global_parameters'
]
=
conf
.
return_values
(
'global-parameters'
)
# check for global DHCP server updating /etc/host per lease
if
conf
.
exists
(
'hostfile-update'
):
dhcp
[
'hostfile_update'
]
=
True
# If enabled every host declaration within that scope, the name provided
# for the host declaration will be supplied to the client as its hostname.
if
conf
.
exists
(
'host-decl-name'
):
dhcp
[
'host_decl_name'
]
=
True
# check for multiple, shared networks served with DHCP addresses
if
conf
.
exists
(
'shared-network-name'
):
for
network
in
conf
.
list_nodes
(
'shared-network-name'
):
conf
.
set_level
(
'service dhcp-server shared-network-name {0}'
.
format
(
network
))
config
=
{
'name'
:
network
,
'authoritative'
:
False
,
'description'
:
''
,
'disabled'
:
False
,
'network_parameters'
:
[],
'subnet'
:
[]
}
# check if DHCP server should be authoritative on this network
if
conf
.
exists
(
'authoritative'
):
config
[
'authoritative'
]
=
True
# A description for this given network
if
conf
.
exists
(
'description'
):
config
[
'description'
]
=
conf
.
return_value
(
'description'
)
# If disabled, the shared-network configuration becomes inactive in
# the running DHCP server instance
if
conf
.
exists
(
'disable'
):
config
[
'disabled'
]
=
True
# HACKS AND TRICKS
#
# check for 'raw' ISC DHCP parameters configured by users
# actually this is a bad idea in general to pass raw parameters
# from any user
#
# deprecate this and issue a warning like we do for DNS forwarding?
if
conf
.
exists
(
'shared-network-parameters'
):
config
[
'network_parameters'
]
=
conf
.
return_values
(
'shared-network-parameters'
)
# check for multiple subnet configurations in a shared network
# config segment
if
conf
.
exists
(
'subnet'
):
for
net
in
conf
.
list_nodes
(
'subnet'
):
conf
.
set_level
(
'service dhcp-server shared-network-name {0} subnet {1}'
.
format
(
network
,
net
))
subnet
=
{
'network'
:
net
,
'address'
:
str
(
ipaddress
.
ip_network
(
net
)
.
network_address
),
'netmask'
:
str
(
ipaddress
.
ip_network
(
net
)
.
netmask
),
'bootfile_name'
:
''
,
'bootfile_server'
:
''
,
'client_prefix_length'
:
''
,
'default_router'
:
''
,
'dns_server'
:
[],
'domain_name'
:
''
,
'domain_search'
:
[],
'exclude'
:
[],
'failover_local_addr'
:
''
,
'failover_name'
:
''
,
'failover_peer_addr'
:
''
,
'failover_status'
:
''
,
'ip_forwarding'
:
False
,
'lease'
:
'86400'
,
'ntp_server'
:
[],
'pop_server'
:
[],
'server_identifier'
:
''
,
'smtp_server'
:
[],
'range'
:
[],
'static_mapping'
:
[],
'static_subnet'
:
''
,
'static_router'
:
''
,
'static_route'
:
''
,
'subnet_parameters'
:
[],
'tftp_server'
:
''
,
'time_offset'
:
''
,
'time_server'
:
[],
'wins_server'
:
[],
'wpad_url'
:
''
}
# Used to identify a bootstrap file
if
conf
.
exists
(
'bootfile-name'
):
subnet
[
'bootfile_name'
]
=
conf
.
return_value
(
'bootfile-name'
)
# Specify host address of the server from which the initial boot file
# (specified above) is to be loaded. Should be a numeric IP address or
# domain name.
if
conf
.
exists
(
'bootfile-server'
):
subnet
[
'bootfile_server'
]
=
conf
.
return_value
(
'bootfile-server'
)
# The subnet mask option specifies the client's subnet mask as per RFC 950. If no subnet
# mask option is provided anywhere in scope, as a last resort dhcpd will use the subnet
# mask from the subnet declaration for the network on which an address is being assigned.
if
conf
.
exists
(
'client-prefix-length'
):
# snippet borrowed from https://stackoverflow.com/questions/33750233/convert-cidr-to-subnet-mask-in-python
host_bits
=
32
-
int
(
conf
.
return_value
(
'client-prefix-length'
))
subnet
[
'client_prefix_length'
]
=
socket
.
inet_ntoa
(
struct
.
pack
(
'!I'
,
(
1
<<
32
)
-
(
1
<<
host_bits
)))
# Default router IP address on the client's subnet
if
conf
.
exists
(
'default-router'
):
subnet
[
'default_router'
]
=
conf
.
return_value
(
'default-router'
)
# Specifies a list of Domain Name System (STD 13, RFC 1035) name servers available to
# the client. Servers should be listed in order of preference.
if
conf
.
exists
(
'dns-server'
):
subnet
[
'dns_server'
]
=
conf
.
return_values
(
'dns-server'
)
# Option specifies the domain name that client should use when resolving hostnames
# via the Domain Name System.
if
conf
.
exists
(
'domain-name'
):
subnet
[
'domain_name'
]
=
conf
.
return_value
(
'domain-name'
)
# The domain-search option specifies a 'search list' of Domain Names to be used
# by the client to locate not-fully-qualified domain names.
if
conf
.
exists
(
'domain-search'
):
for
domain
in
conf
.
return_values
(
'domain-search'
):
subnet
[
'domain_search'
]
.
append
(
'"'
+
domain
+
'"'
)
# IP address (local) for failover peer to connect
if
conf
.
exists
(
'failover local-address'
):
subnet
[
'failover_local_addr'
]
=
conf
.
return_value
(
'failover local-address'
)
# DHCP failover peer name
if
conf
.
exists
(
'failover name'
):
subnet
[
'failover_name'
]
=
conf
.
return_value
(
'failover name'
)
# IP address (remote) of failover peer
if
conf
.
exists
(
'failover peer-address'
):
subnet
[
'failover_peer_addr'
]
=
conf
.
return_value
(
'failover peer-address'
)
# DHCP failover peer status (primary|secondary)
if
conf
.
exists
(
'failover status'
):
subnet
[
'failover_status'
]
=
conf
.
return_value
(
'failover status'
)
# Option specifies whether the client should configure its IP layer for packet
# forwarding
if
conf
.
exists
(
'ip-forwarding'
):
subnet
[
'ip_forwarding'
]
=
True
# Time should be the length in seconds that will be assigned to a lease if the
# client requesting the lease does not ask for a specific expiration time
if
conf
.
exists
(
'lease'
):
subnet
[
'lease'
]
=
conf
.
return_value
(
'lease'
)
# Specifies a list of IP addresses indicating NTP (RFC 5905) servers available
# to the client.
if
conf
.
exists
(
'ntp-server'
):
subnet
[
'ntp_server'
]
=
conf
.
return_values
(
'ntp-server'
)
# POP3 server option specifies a list of POP3 servers available to the client.
# Servers should be listed in order of preference.
if
conf
.
exists
(
'pop-server'
):
subnet
[
'pop_server'
]
=
conf
.
return_values
(
'pop-server'
)
# DHCP servers include this option in the DHCPOFFER in order to allow the client
# to distinguish between lease offers. DHCP clients use the contents of the
# 'server identifier' field as the destination address for any DHCP messages
# unicast to the DHCP server
if
conf
.
exists
(
'server-identifier'
):
subnet
[
'server_identifier'
]
=
conf
.
return_value
(
'server-identifier'
)
# SMTP server option specifies a list of SMTP servers available to the client.
# Servers should be listed in order of preference.
if
conf
.
exists
(
'smtp-server'
):
subnet
[
'smtp_server'
]
=
conf
.
return_values
(
'smtp-server'
)
# For any subnet on which addresses will be assigned dynamically, there must be at
# least one range statement. The range statement gives the lowest and highest IP
# addresses in a range. All IP addresses in the range should be in the subnet in
# which the range statement is declared.
if
conf
.
exists
(
'range'
):
for
range
in
conf
.
list_nodes
(
'range'
):
range
=
{
'start'
:
conf
.
return_value
(
'range {0} start'
.
format
(
range
)),
'stop'
:
conf
.
return_value
(
'range {0} stop'
.
format
(
range
))
}
subnet
[
'range'
]
.
append
(
range
)
# IP address that needs to be excluded from DHCP lease range
if
conf
.
exists
(
'exclude'
):
# We have no need to store the exclude addresses. Exclude addresses
# are recalculated into several ranges
exclude
=
[]
subnet
[
'exclude'
]
=
conf
.
return_values
(
'exclude'
)
for
addr
in
subnet
[
'exclude'
]:
exclude
.
append
(
ipaddress
.
ip_address
(
addr
))
# sort excluded IP addresses ascending
exclude
=
sorted
(
exclude
)
# calculate multipe ranges based on the excluded IP addresses
output
=
[]
for
range
in
subnet
[
'range'
]:
range_start
=
range
[
'start'
]
range_stop
=
range
[
'stop'
]
for
i
in
exclude
:
# Excluded IP address must be in out specified range
if
(
i
>=
ipaddress
.
ip_address
(
range_start
))
and
(
i
<=
ipaddress
.
ip_address
(
range_stop
)):
# Build up new IP address range ending one IP address before
# our exclude address
range
=
{
'start'
:
str
(
range_start
),
'stop'
:
str
(
i
-
1
)
}
# Our next IP address range will start one address after
# our exclude address
range_start
=
i
+
1
output
.
append
(
range
)
# Take care of last IP address range spanning from the last exclude
# address (+1) to the end of the initial configured range
if
i
is
exclude
[
-
1
]:
last
=
{
'start'
:
str
(
i
+
1
),
'stop'
:
str
(
range_stop
)
}
output
.
append
(
last
)
else
:
# IP address not inside search range, take range is it is
output
.
append
(
range
)
# We successfully build up a new list containing several IP address
# ranges, replace IP address range in our dictionary
subnet
[
'range'
]
=
output
# Static DHCP leases
if
conf
.
exists
(
'static-mapping'
):
for
mapping
in
conf
.
list_nodes
(
'static-mapping'
):
conf
.
set_level
(
'service dhcp-server shared-network-name {0} subnet {1} static-mapping {2}'
.
format
(
network
,
net
,
mapping
))
mapping
=
{
'name'
:
mapping
,
'disabled'
:
False
,
'ip_address'
:
''
,
'mac_address'
:
''
,
'static_parameters'
:
[]
}
# This static lease is disabled
if
conf
.
exists
(
'disable'
):
mapping
[
'disabled'
]
=
True
# IP address used for this DHCP client
if
conf
.
exists
(
'ip-address'
):
mapping
[
'ip_address'
]
=
conf
.
return_value
(
'ip-address'
)
# MAC address of requesting DHCP client
if
conf
.
exists
(
'mac-address'
):
mapping
[
'mac_address'
]
=
conf
.
return_value
(
'mac-address'
)
# HACKS AND TRICKS
#
# check for 'raw' ISC DHCP parameters configured by users
# actually this is a bad idea in general to pass raw parameters
# from any user
#
# deprecate this and issue a warning like we do for DNS forwarding?
if
conf
.
exists
(
'static-mapping-parameters'
):
mapping
[
'static_parameters'
]
=
conf
.
return_values
(
'static-mapping-parameters'
)
# append static-mapping configuration to subnet list
subnet
[
'static_mapping'
]
.
append
(
mapping
)
# Reset config level to matching hirachy
conf
.
set_level
(
'service dhcp-server shared-network-name {0} subnet {1}'
.
format
(
network
,
net
))
# This option specifies a list of static routes that the client should install in its routing
# cache. If multiple routes to the same destination are specified, they are listed in descending
# order of priority.
if
conf
.
exists
(
'static-route destination-subnet'
):
subnet
[
'static_subnet'
]
=
conf
.
return_value
(
'static-route destination-subnet'
)
# Required for global config section
dhcp
[
'static_route'
]
=
True
if
conf
.
exists
(
'static-route router'
):
subnet
[
'static_router'
]
=
conf
.
return_value
(
'static-route router'
)
if
subnet
[
'static_router'
]
and
subnet
[
'static_subnet'
]:
# https://ercpe.de/blog/pushing-static-routes-with-isc-dhcp-server
# Option format is:
# <netmask>, <network-byte1>, <network-byte2>, <network-byte3>, <router-byte1>, <router-byte2>, <router-byte3>
# where bytes with the value 0 are omitted.
net
=
ipaddress
.
ip_network
(
subnet
[
'static_subnet'
])
# add netmask
string
=
str
(
net
.
prefixlen
)
+
','
# add network bytes
bytes
=
str
(
net
.
network_address
)
.
split
(
'.'
)
for
b
in
bytes
:
if
b
!=
'0'
:
string
+=
b
+
','
# add router bytes
bytes
=
subnet
[
'static_router'
]
.
split
(
'.'
)
for
b
in
bytes
:
string
+=
b
if
b
is
not
bytes
[
-
1
]:
string
+=
','
subnet
[
'static_route'
]
=
string
# HACKS AND TRICKS
#
# check for 'raw' ISC DHCP parameters configured by users
# actually this is a bad idea in general to pass raw parameters
# from any user
#
# deprecate this and issue a warning like we do for DNS forwarding?
if
conf
.
exists
(
'subnet-parameters'
):
subnet
[
'subnet_parameters'
]
=
conf
.
return_values
(
'subnet-parameters'
)
# This option is used to identify a TFTP server and, if supported by the client, should have
# the same effect as the server-name declaration. BOOTP clients are unlikely to support this
# option. Some DHCP clients will support it, and others actually require it.
if
conf
.
exists
(
'tftp-server-name'
):
subnet
[
'tftp_server'
]
=
conf
.
return_value
(
'tftp-server-name'
)
# The time-offset option specifies the offset of the client’s subnet in seconds from
# Coordinated Universal Time (UTC).
if
conf
.
exists
(
'time-offset'
):
subnet
[
'time_offset'
]
=
conf
.
return_value
(
'time-offset'
)
# The time-server option specifies a list of RFC 868 time servers available to the client.
# Servers should be listed in order of preference.
if
conf
.
exists
(
'time-server'
):
subnet
[
'time_server'
]
=
conf
.
return_values
(
'time-server'
)
# The NetBIOS name server (NBNS) option specifies a list of RFC 1001/1002 NBNS name servers
# listed in order of preference. NetBIOS Name Service is currently more commonly referred to
# as WINS. WINS servers can be specified using the netbios-name-servers option.
if
conf
.
exists
(
'wins-server'
):
subnet
[
'wins_server'
]
=
conf
.
return_values
(
'wins-server'
)
# URL for Web Proxy Autodiscovery Protocol
if
conf
.
exists
(
'wpad-url'
):
subnet
[
'wpad_url'
]
=
conf
.
return_value
(
'wpad-url'
)
# Required for global config section
dhcp
[
'wpad'
]
=
True
# append subnet configuration to shared network subnet list
config
[
'subnet'
]
.
append
(
subnet
)
# append shared network configuration to config dictionary
dhcp
[
'shared_network'
]
.
append
(
config
)
return
dhcp
def
verify
(
dhcp
):
if
(
dhcp
is
None
)
or
(
dhcp
[
'disabled'
]
is
True
):
return
None
# If DHCP is enabled we need one share-network
if
len
(
dhcp
[
'shared_network'
])
==
0
:
raise
ConfigError
(
'No DHCP shared networks configured.
\n
'
\
'At least one DHCP shared network must be configured.'
)
# Inspect shared-network/subnet
failover_names
=
[]
listen_ok
=
False
subnets
=
[]
# A shared-network requires a subnet definition
for
network
in
dhcp
[
'shared_network'
]:
if
len
(
network
[
'subnet'
])
==
0
:
raise
ConfigError
(
'No DHCP lease subnets configured for {0}. At least one
\n
'
\
'lease subnet must be configured for each shared network.'
.
format
(
network
[
'name'
]))
for
subnet
in
network
[
'subnet'
]:
# Subnet static route declaration requires destination and router
if
subnet
[
'static_subnet'
]
or
subnet
[
'static_router'
]:
if
not
(
subnet
[
'static_subnet'
]
and
subnet
[
'static_router'
]):
raise
ConfigError
(
'Please specify missing DHCP static-route parameter(s):
\n
'
\
'destination-subnet | router'
)
# Failover requires all 4 parameters set
if
subnet
[
'failover_local_addr'
]
or
subnet
[
'failover_peer_addr'
]
or
subnet
[
'failover_name'
]
or
subnet
[
'failover_status'
]:
if
not
(
subnet
[
'failover_local_addr'
]
and
subnet
[
'failover_peer_addr'
]
and
subnet
[
'failover_name'
]
and
subnet
[
'failover_status'
]):
raise
ConfigError
(
'Please specify missing DHCP failover parameter(s):
\n
'
\
'local-address | peer-address | name | status'
)
# Failover names must be uniquie
if
subnet
[
'failover_name'
]
in
failover_names
:
raise
ConfigError
(
'Failover names must be unique:
\n
'
\
'{0} has already been configured!'
.
format
(
subnet
[
'failover_name'
]))
else
:
failover_names
.
append
(
subnet
[
'failover_name'
])
# Failover requires start/stop ranges for pool
if
(
len
(
subnet
[
'range'
])
==
0
):
raise
ConfigError
(
'At least one start-stop range must be configured for {0}
\n
'
\
'to set up DHCP failover!'
.
format
(
subnet
[
'network'
]))
# Check if DHCP address range is inside configured subnet declaration
range_start
=
[]
range_stop
=
[]
for
range
in
subnet
[
'range'
]:
start
=
range
[
'start'
]
stop
=
range
[
'stop'
]
# DHCP stop IP required after start IP
if
start
and
not
stop
:
raise
ConfigError
(
'DHCP range stop address for start {0} is not defined!'
.
format
(
start
))
# Start address must be inside network
if
not
ipaddress
.
ip_address
(
start
)
in
ipaddress
.
ip_network
(
subnet
[
'network'
]):
raise
ConfigError
(
'DHCP range start address {0} is not in subnet {1}
\n
'
\
'specified for shared network {2}!'
.
format
(
start
,
subnet
[
'network'
],
network
[
'name'
]))
# Stop address must be inside network
if
not
ipaddress
.
ip_address
(
stop
)
in
ipaddress
.
ip_network
(
subnet
[
'network'
]):
raise
ConfigError
(
'DHCP range stop address {0} is not in subnet {1}
\n
'
\
'specified for shared network {2}!'
.
format
(
stop
,
subnet
[
'network'
],
network
[
'name'
]))
# Stop address must be greater or equal to start address
if
not
ipaddress
.
ip_address
(
stop
)
>=
ipaddress
.
ip_address
(
start
):
raise
ConfigError
(
'DHCP range stop address {0} must be greater or equal
\n
'
\
'to the range start address {1}!'
.
format
(
stop
,
start
))
# Range start address must be unique
if
start
in
range_start
:
raise
ConfigError
(
'Conflicting DHCP lease range:
\n
'
\
'Pool start address {0} defined multipe times!'
.
format
(
start
))
else
:
range_start
.
append
(
start
)
# Range stop address must be unique
if
stop
in
range_stop
:
raise
ConfigError
(
'Conflicting DHCP lease range:
\n
'
\
'Pool stop address {0} defined multipe times!'
.
format
(
stop
))
else
:
range_stop
.
append
(
stop
)
# Exclude addresses must be in bound
for
exclude
in
subnet
[
'exclude'
]:
if
not
ipaddress
.
ip_address
(
exclude
)
in
ipaddress
.
ip_network
(
subnet
[
'network'
]):
raise
ConfigError
(
'Exclude IP address {0} is outside of the DHCP lease network {1}
\n
'
\
'under shared network {2}!'
.
format
(
exclude
,
subnet
[
'network'
],
network
[
'name'
]))
# At least one DHCP address range or static-mapping required
active_mapping
=
False
if
(
len
(
subnet
[
'range'
])
==
0
):
for
mapping
in
subnet
[
'static_mapping'
]:
# we need at least one active mapping
if
(
not
active_mapping
)
and
(
not
mapping
[
'disabled'
]):
active_mapping
=
True
else
:
active_mapping
=
True
if
not
active_mapping
:
raise
ConfigError
(
'No DHCP address range or active static-mapping set
\n
'
\
'for subnet {0}!'
.
format
(
subnet
[
'network'
]))
# Static mappings require just a MAC address (will use an IP from the dynamic pool if IP is not set)
for
mapping
in
subnet
[
'static_mapping'
]:
if
mapping
[
'ip_address'
]:
# Static IP address must be in bound
if
not
ipaddress
.
ip_address
(
mapping
[
'ip_address'
])
in
ipaddress
.
ip_network
(
subnet
[
'network'
]):
raise
ConfigError
(
'DHCP static lease IP address {0} for static mapping {1}
\n
'
\
'in shared network {2} is outside DHCP lease subnet {3}!'
\
.
format
(
mapping
[
'ip_address'
],
mapping
[
'name'
],
network
[
'name'
],
subnet
[
'network'
]))
# Static mapping requires MAC address
if
not
mapping
[
'mac_address'
]:
raise
ConfigError
(
'DHCP static lease MAC address not specified for static mapping
\n
'
\
'{0} under shared network name {1}!'
.
format
(
mapping
[
'name'
],
network
[
'name'
]))
# There must be one subnet connected to a listen interface.
# This only counts if the network itself is not disabled!
if
not
network
[
'disabled'
]:
if
vyos
.
validate
.
is_subnet_connected
(
subnet
[
'network'
],
primary
=
True
):
listen_ok
=
True
# Subnets must be non overlapping
if
subnet
[
'network'
]
in
subnets
:
raise
ConfigError
(
'DHCP subnets must be unique! Subnet {0} defined multiple times!'
.
format
(
subnet
[
'network'
]))
else
:
subnets
.
append
(
subnet
[
'network'
])
# Check for overlapping subnets
net
=
ipaddress
.
ip_network
(
subnet
[
'network'
])
for
n
in
subnets
:
net2
=
ipaddress
.
ip_network
(
n
)
if
(
net
!=
net2
):
if
net
.
overlaps
(
net2
):
raise
ConfigError
(
'DHCP conflicting subnet ranges: {0} overlaps {1}'
.
format
(
net
,
net2
))
if
not
listen_ok
:
raise
ConfigError
(
'None of the DHCP lease subnets are inside any configured subnet on
\n
'
\
'broadcast interfaces. At least one lease subnet must be set such that
\n
'
\
'DHCP server listens on a one broadcast interface!'
)
return
None
def
generate
(
dhcp
):
if
dhcp
is
None
:
return
None
if
dhcp
[
'disabled'
]
is
True
:
print
(
'Warning: DHCP server will be deactivated because it is disabled'
)
return
None
tmpl
=
jinja2
.
Template
(
config_tmpl
)
config_text
=
tmpl
.
render
(
dhcp
)
# Please see: https://phabricator.vyos.net/T1129 for quoting of the raw parameters
# we can pass to ISC DHCPd
config_text
=
config_text
.
replace
(
"""
,
'"'
)
with
open
(
config_file
,
'w'
)
as
f
:
f
.
write
(
config_text
)
tmpl
=
jinja2
.
Template
(
daemon_tmpl
)
config_text
=
tmpl
.
render
(
dhcp
)
with
open
(
daemon_config_file
,
'w'
)
as
f
:
f
.
write
(
config_text
)
return
None
def
apply
(
dhcp
):
if
(
dhcp
is
None
)
or
dhcp
[
'disabled'
]:
# DHCP server is removed in the commit
os
.
system
(
'sudo systemctl stop isc-dhcp-server.service'
)
if
os
.
path
.
exists
(
config_file
):
os
.
unlink
(
config_file
)
if
os
.
path
.
exists
(
daemon_config_file
):
os
.
unlink
(
daemon_config_file
)
else
:
# If our file holding DHCP leases does yet not exist - create it
if
not
os
.
path
.
exists
(
lease_file
):
os
.
mknod
(
lease_file
)
os
.
system
(
'sudo systemctl restart isc-dhcp-server.service'
)
return
None
if
__name__
==
'__main__'
:
try
:
c
=
get_config
()
verify
(
c
)
generate
(
c
)
apply
(
c
)
except
ConfigError
as
e
:
print
(
e
)
sys
.
exit
(
1
)
File Metadata
Details
Attached
Mime Type
text/x-script.python
Expires
Tue, Dec 16, 4:00 AM (1 d, 12 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3076296
Default Alt Text
dhcp_server.py (37 KB)
Attached To
Mode
rVYOSONEX vyos-1x
Attached
Detach File
Event Timeline
Log In to Comment