From 77c1b3457439889846380c5fd5da30cd11e253d9 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Thu, 16 Aug 2018 21:04:28 +0200
Subject: T778: T782: dhcp-server: XML and Python rewrite

This commit changes in addtion the DHCP server config syntax as defined
in "T782: Cleanup dhcp-server configuration".

Replace boolean parameter from the folowing nodes and make it valueless.
This requires a migration script which is tracked with this task

* set service dhcp-server shared-network-name <xyz> subnet 172.31.0.0/24
  ip-forwarding enable (true|false)
* set service dhcp-server shared-network-name <xyz> authoritative (true|false)
* set service dhcp-server disabled (true|false)
* set service dhcp-server dynamic-dns-update enable (true|fals)
* set service dhcp-server hostfile-update (enable|disable)

Replace the nested start/stop ip address from "subnet 172.31.0.0/24 start
172.31.0.101 stop 172.31.0.149" to "subnet 172.31.0.0/24 range <foo> start" and
"subnet 172.31.0.0/24 range <foo> stop" where foo can be any character or number.

In addition the vyatta-cfg-dhcp-server package used it's own init/config file
for service startup. This has been migrated to the vanilla Debian files.

Copy 'on-dhcp-event.sh' from vyatta-cfg-shcp-server package commit 4749e648bca6.
---
 src/conf_mode/dhcp_server.py             | 773 +++++++++++++++++++++++++++++++
 src/migration-scripts/dhcp-server/4-to-5 | 121 +++++
 src/system/on-dhcp-event.sh              |  98 ++++
 3 files changed, 992 insertions(+)
 create mode 100755 src/conf_mode/dhcp_server.py
 create mode 100755 src/migration-scripts/dhcp-server/4-to-5
 create mode 100755 src/system/on-dhcp-event.sh

(limited to 'src')

diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py
new file mode 100755
index 000000000..6dc8bf2bb
--- /dev/null
+++ b/src/conf_mode/dhcp_server.py
@@ -0,0 +1,773 @@
+#!/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 netifaces
+
+from vyos.config import Config
+from vyos import ConfigError
+
+config_file = r'/etc/dhcp/dhcpd.conf'
+daemon_config_file = r'/etc/default/isc-dhcp-server'
+
+# Please be careful if you edit the template.
+config_tmpl = """
+### Autogenerated by dhcp_server.py ###
+
+log-facility local7;
+
+{% if hostfile_update %}
+on commit {
+    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);
+}
+
+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 %}
+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 static_route -%}
+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.disable -%}
+{%- 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 configrations
+{% for network in shared_network %}
+{%- if not network.disable -%}
+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.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.disable -%}
+        host {{ network.name }}_{{ host.name }} {
+            fixed-address {{ host.ip_address }};
+            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 %}
+        {%- for range in subnet.range %}
+        range {{ range.start }} {{ range.stop }};
+        {%- endfor %}
+    }
+    {%- endfor %}
+    on commit { set shared-networkname = "{{ network.name }}"; }
+}
+{%- endif %}
+{% endfor %}
+"""
+
+daemon_tmpl = """
+### Autogenerated by dhcp_server.py ###
+
+# sourced by /etc/init.d/isc-dhcp-server
+
+# Path to dhcpd's config file (default: /etc/dhcp/dhcpd.conf).
+DHCPD_CONF=/etc/dhcp/dhcpd.conf
+
+# Path to dhcpd's PID file (default: /var/run/dhcpd.pid).
+DHCPD_PID=/var/run/dhcpd.pid
+
+# Additional options to start dhcpd with.
+#       Don't use options -cf or -pf here; use DHCPD_CONF/ DHCPD_PID instead
+OPTIONS="-lf /config/dhcpd.leases"
+
+# On what interfaces should the DHCP server (dhcpd) serve DHCP requests?
+#       Separate multiple interfaces with spaces, e.g. "eth0 eth1".
+INTERFACES=""
+"""
+
+default_config_data = {
+    'disable': False,
+    'ddns_enable': False,
+    'global_parameters': [],
+    'hostfile_update': 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['disable'] = 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
+
+    # 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': '',
+                'disable': 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['disable'] = 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,
+                                'disable': False,
+                                'ip_address': '',
+                                'mac_address': '',
+                                'static_parameters': []
+                            }
+
+                            # This static lease is disabled
+                            if conf.exists('disable'):
+                                mapping['disable'] = 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:
+                            if b != '0':
+                                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'):
+                        config['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:
+        return None
+
+    # If DHCP is enabled we need one share-network
+    if len(dhcp['shared_network']) == 0:
+        raise ConfigError('No DHCP shared networks configured. At least one DHCP shared network must be configured.')
+
+    # 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 DHCP lease subnet must be configured for each shared network.'.format(network['name']))
+
+    # Inspect our subnet configuration
+    failover_names = []
+    listen_ok = False
+    subnets = []
+    for network in dhcp['shared_network']:
+        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 the missing DHCP static-route parameter: 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 set one or more of the missing DHCP failover parameters: local-address | peer-address | name | status')
+
+                # Failover names must be uniquie
+                if subnet['failover_name'] in failover_names:
+                    raise ConfigError('Failover names should be unique: "{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('Atleast one start-stop range must be configured for $subnet to set up DHCP failover.')
+
+            # Check if DHCP address range is inside configured subnet declaration
+            range_start = []
+            range_stop = []
+            for range in subnet['range']:
+                # DHCP stop IP required after start IP
+                if range['start'] and not range['stop']:
+                    raise ConfigError('DHCP range stop IP not defined for range start IP "{0}".'.format(range['start']))
+
+                # Start address must be inside network
+                if not ipaddress.ip_address(range['start']) in ipaddress.ip_network(subnet['network']):
+                    raise ConfigError('DHCP range start IP "{0}" is not in subnet "{1}" specified in network "{2}."'.format(range['start'], subnet['network'], network['name']))
+
+                # Stop address must be inside network
+                if not ipaddress.ip_address(range['stop']) in ipaddress.ip_network(subnet['network']):
+                    raise ConfigError('DHCP range stop IP "{0}" is not in  subnet "{1}" specified in network "{2}."'.format(range['stop'], subnet['network'], network['name']))
+
+                # Stop address must be greater or equal to start address
+                if not ipaddress.ip_address(range['stop']) >= ipaddress.ip_address(range['start']):
+                    raise ConfigError('DHCP range stop IP "{0}" should be an address greater or equal to the start address "{1}".'.format(range['stop'], range['start']))
+
+                # Range start address must be unique
+                if range['start'] in range_start:
+                    raise ConfigError('Conflicting DHCP lease ranges: Pool start address "{0}" defined multipe times'.format(range['start']))
+                else:
+                    range_start.append(range['start'])
+
+                # Range stop address must be unique
+                if range['stop'] in range_stop:
+                    raise ConfigError('Conflicting DHCP lease ranges: Pool stop address "{0}" defined multipe times'.format(range['stop']))
+                else:
+                    range_stop.append(range['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 "{0}" is outside of the DHCP lease network "{1}" 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['disable']):
+                        active_mapping = True
+            else:
+                active_mapping = True
+
+            if not active_mapping:
+                raise ConfigError('No DHCP address range or active static-mapping set for subnet "{0}".'.format(subnet['network']))
+
+            # Static IP address mappings require both an IP address and MAC address
+            for mapping in subnet['static_mapping']:
+                # Static IP address must be configured
+                if not mapping['ip_address']:
+                    raise ConfigError('No static lease IP address specified for static mapping "{0}" under shared network name "{1}".'.format(mapping['name'], network['name']))
+
+                # Static IP address must be in bound
+                if not ipaddress.ip_address(mapping['ip_address']) in ipaddress.ip_network(subnet['network']):
+                    raise ConfigError('Static DHCP lease IP "{0}" under static mapping "{1}" in shared network "{2}" is outside DHCP lease network "{3}".'.format(mapping['ip_address'], mapping['name'], network['name'], subnet['network'], ))
+
+                # Static mapping requires MAC address
+                if not mapping['mac_address']:
+                     raise ConfigError('No static lease MAC address specified for static mapping "{0}" under shared network name "{1}".'.format(mapping['name'], network['name']))
+
+            #
+            # There must be one subnet connected to a real interface
+            #
+            for interface in netifaces.interfaces():
+                # Retrieve IP address of network interface
+                ip = netifaces.ifaddresses(interface)[netifaces.AF_INET][0]['addr']
+                if ipaddress.ip_address(ip) in ipaddress.ip_network(subnet['network']):
+                    listen_ok = True
+
+            #
+            # Subnets must be non overlapping
+            #
+            if subnet['network'] in subnets:
+                raise ConfigError('Subnets must be unique! "{0}" defined multiple times.'.format(subnet))
+            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.compare_networks(net2) != 0):
+                    if net.overlaps(net2):
+                        raise ConfigError('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 broadcast interfaces. At least one lease subnet must be set such that DHCP server listens on a one broadcast interface')
+
+    return None
+
+def generate(dhcp):
+    if dhcp is None:
+        return None
+
+    tmpl = jinja2.Template(config_tmpl)
+    config_text = tmpl.render(dhcp)
+    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 not None:
+        os.system('sudo systemctl restart isc-dhcp-server.service')
+    else:
+        # DHCP server is removed in the commit
+        os.system('sudo systemctl stop isc-dhcp-server.service')
+        os.unlink(config_file)
+        os.unlink(daemon_config_file)
+
+    return None
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        sys.exit(1)
diff --git a/src/migration-scripts/dhcp-server/4-to-5 b/src/migration-scripts/dhcp-server/4-to-5
new file mode 100755
index 000000000..8b973d608
--- /dev/null
+++ b/src/migration-scripts/dhcp-server/4-to-5
@@ -0,0 +1,121 @@
+#!/usr/bin/env python3
+
+# Removes boolean operator from:
+#   - "set service dhcp-server shared-network-name <xyz> subnet 172.31.0.0/24 ip-forwarding enable (true|false)"
+#   - "set service dhcp-server shared-network-name <xyz> authoritative (true|false)"
+#   - "set service dhcp-server disabled (true|false)"
+
+import sys
+
+from vyos.configtree import ConfigTree
+
+if (len(sys.argv) < 1):
+    print("Must specify file name!")
+    sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+    config_file = f.read()
+
+config = ConfigTree(config_file)
+
+if not config.exists(['service', 'dhcp-server']):
+    # Nothing to do
+    sys.exit(0)
+else:
+    base = ['service', 'dhcp-server']
+    # Make node "set service dhcp-server dynamic-dns-update enable (true|false)" valueless
+    if config.exists(base + ['dynamic-dns-update']):
+        bool_val = config.return_value(base + ['dynamic-dns-update', 'enable'])
+
+        # Delete the node with the old syntax
+        config.delete(base + ['dynamic-dns-update'])
+        if str(bool_val) == 'true':
+            # Enable dynamic-dns-update with new syntax
+            config.set(base + ['dynamic-dns-update'], value=None)
+
+    # Make node "set service dhcp-server disabled (true|false)" valueless
+    if config.exists(base + ['disabled']):
+        bool_val = config.return_value(base + ['disabled'])
+
+        # Delete the node with the old syntax
+        config.delete(base + ['disabled'])
+        if str(bool_val) == 'true':
+            # Now disable DHCP server with the new syntax
+            config.set(base + ['disable'], value=None)
+
+    # Make node "set service dhcp-server hostfile-update (enable|disable) valueless
+    if config.exists(base + ['hostfile-update']):
+        bool_val = config.return_value(base + ['hostfile-update'])
+
+        # Delete the node with the old syntax incl. all subnodes
+        config.delete(base + ['hostfile-update'])
+        if str(bool_val) == 'enable':
+            # Enable hostfile update with new syntax
+            config.set(base + ['hostfile-update'], value=None)
+
+    # Run this for every instance if 'shared-network-name'
+    for network in config.list_nodes(base + ['shared-network-name']):
+        base_network = base + ['shared-network-name', network]
+        # format as tag node to avoid loading problems
+        config.set_tag(base + ['shared-network-name'])
+
+        # Run this for every specified 'subnet'
+        for subnet in config.list_nodes(base_network + ['subnet']):
+            base_subnet = base_network + ['subnet', subnet]
+            # format as tag node to avoid loading problems
+            config.set_tag(base_network + ['subnet'])
+
+            # Make node "set service dhcp-server shared-network-name <xyz> subnet 172.31.0.0/24 ip-forwarding enable" valueless
+            if config.exists(base_subnet + ['ip-forwarding', 'enable']):
+                bool_val = config.return_value(base_subnet + ['ip-forwarding', 'enable'])
+                # Delete the node with the old syntax
+                config.delete(base_subnet + ['ip-forwarding'])
+                if str(bool_val) == 'true':
+                    # Recreate node with new syntax
+                    config.set(base_subnet + ['ip-forwarding'], value=None)
+
+            # Rename node "set service dhcp-server shared-network-name <xyz> subnet 172.31.0.0/24 start <172.16.0.4> stop <172.16.0.9>
+            if config.exists(base_subnet + ['start']):
+                # This is the new "range" id for DHCP lease ranges
+                r_id = 0
+                for range in config.list_nodes(base_subnet + ['start']):
+                    range_start = range
+                    range_stop = config.return_value(base_subnet + ['start', range_start, 'stop'])
+
+                    # Delete the node with the old syntax
+                    config.delete(base_subnet + ['start', range_start])
+
+                    # Create the node for the new syntax
+                    # Note: range is a tag node, counter is its child, not a value
+                    config.set(base_subnet + ['range', r_id])
+                    config.set(base_subnet + ['range', r_id, 'start'], value=range_start)
+                    config.set(base_subnet + ['range', r_id, 'stop'], value=range_stop)
+
+                    # format as tag node to avoid loading problems
+                    config.set_tag(base_subnet + ['range'])
+
+                    # increment range id for possible next range definition
+                    r_id += 1
+
+                # Delete the node with the old syntax
+                config.delete(['service', 'dhcp-server', 'shared-network-name', network, 'subnet', subnet, 'start'])
+
+
+        # Make node "set service dhcp-server shared-network-name <xyz> authoritative" valueless
+        if config.exists(['service', 'dhcp-server', 'shared-network-name', network, 'authoritative']):
+            bool_val = config.return_value(['service', 'dhcp-server', 'shared-network-name', network, 'authoritative'])
+            # Delete the node with the old syntax
+            config.delete(['service', 'dhcp-server', 'shared-network-name', network, 'authoritative'])
+            if str(bool_val) == 'true':
+                # Recreate node with new syntax
+                config.set(['service', 'dhcp-server', 'shared-network-name', network, 'authoritative'])
+
+    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))
+
+    sys.exit(1)
diff --git a/src/system/on-dhcp-event.sh b/src/system/on-dhcp-event.sh
new file mode 100755
index 000000000..d671bffd6
--- /dev/null
+++ b/src/system/on-dhcp-event.sh
@@ -0,0 +1,98 @@
+#!/bin/bash
+
+# This script came from ubnt.com forum user "bradd" in the following post
+# http://community.ubnt.com/t5/EdgeMAX/Automatic-DNS-resolution-of-DHCP-client-names/td-p/651311
+# It has been modified by Ubiquiti to update the /etc/host file
+# instead of adding to the CLI.
+# Thanks to forum user "itsmarcos" for bug fix & improvements
+# Thanks to forum user "ruudboon" for multiple domain fix
+# Thanks to forum user "chibby85" for expire patch and static-mapping
+
+if [ $# -lt 5 ]; then
+  echo Invalid args
+  logger -s -t on-dhcp-event "Invalid args \"$@\""
+  exit 1
+fi
+
+action=$1
+client_name=$2
+client_ip=$3
+client_mac=$4
+domain=$5
+file=/etc/hosts
+changes=0
+
+if [ "$domain" == "..YYZ!" ]; then
+    client_fqdn_name=$client_name
+    client_search_expr=$client_name
+else
+    client_fqdn_name=$client_name.$domain
+    client_search_expr="$client_name\\.$domain"
+fi
+
+case "$action" in
+  commit) # add mapping for new lease
+    echo "- new lease event, setting static mapping for host "\
+         "$client_fqdn_name (MAC=$client_mac, IP=$client_ip)"
+    #
+    # grep fails miserably with \t in the search expression.
+    # In the following line one <Ctrl-V> <TAB> is used after $client_search_expr
+    # followed by a single space
+    grep -q " $client_search_expr	 #on-dhcp-event " $file
+    if [ $? == 0 ]; then
+       echo pattern found, removing
+       wc1=`cat $file | wc -l`
+       sudo sed -i "/ $client_search_expr\t #on-dhcp-event /d" $file
+       wc2=`cat $file | wc -l`
+       if [ "$wc1" -eq "$wc2" ]; then
+         echo No change
+       fi
+    else
+       echo pattern NOT found
+    fi
+
+    # check if hostname already exists (e.g. a static host mapping)
+    # if so don't overwrite
+    grep -q " $client_search_expr	 " $file
+    if [ $? == 0 ]; then
+       echo host $client_fqdn_name already exists, exiting
+       exit 1
+    fi
+
+    line="$client_ip\t $client_fqdn_name\t #on-dhcp-event $client_mac"
+    sudo sh -c "echo -e '$line' >> $file"
+    ((changes++))
+    echo Entry was added
+    ;;
+
+  release) # delete mapping for released address
+    echo "- lease release event, deleting static mapping for host $client_fqdn_name"
+    wc1=`cat $file | wc -l`
+    sudo sed -i "/ $client_search_expr\t #on-dhcp-event /d" $file
+    wc2=`cat $file | wc -l`
+    if [ "$wc1" -eq "$wc2" ]; then
+      echo No change
+    else
+      echo Entry was removed
+      ((changes++))
+    fi
+    ;;
+
+  *)
+    logger -s -t on-dhcp-event "Invalid command \"$1\""
+    exit 1;
+    ;;
+esac
+
+if [ $changes -gt 0 ]; then
+  echo Success
+  pid=`pgrep pdns_recursor`
+  if [ -n "$pid" ]; then
+     sudo rec_control reload-zones
+  fi
+else
+  echo No changes made
+fi
+exit 0
+
+
-- 
cgit v1.2.3