summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/conf_mode/bcast_relay.py166
-rwxr-xr-xsrc/conf_mode/dhcp_server.py798
-rwxr-xr-xsrc/conf_mode/snmp.py24
-rwxr-xr-xsrc/conf_mode/tftp_server.py35
-rwxr-xr-xsrc/conf_mode/vrrp.py2
-rwxr-xr-xsrc/migration-scripts/dhcp-server/4-to-5121
-rwxr-xr-xsrc/system/on-dhcp-event.sh98
7 files changed, 1156 insertions, 88 deletions
diff --git a/src/conf_mode/bcast_relay.py b/src/conf_mode/bcast_relay.py
index 95f6215b5..8cc948610 100755
--- a/src/conf_mode/bcast_relay.py
+++ b/src/conf_mode/bcast_relay.py
@@ -20,55 +20,104 @@ import sys
import os
import fnmatch
import subprocess
+import jinja2
from vyos.config import Config
from vyos import ConfigError
config_file = r'/etc/default/udp-broadcast-relay'
+config_tmpl = """
+### Autogenerated by bcast_relay.py ###
+
+# UDP broadcast relay configuration for instance {{ id }}
+{%- if description %}
+# Comment: {{ description }}
+{% endif -%}
+DAEMON_ARGS="{% if address %}-s {{ address }} {% endif %}{{ id }} {{ port }} {{ interfaces | join(' ') }}"
+"""
+
+default_config_data = {
+ 'disabled': False,
+ 'instances': []
+}
+
def get_config():
+ relay = default_config_data
conf = Config()
- conf.set_level("service broadcast-relay id")
- relay_id = conf.list_nodes("")
- relays = []
-
- for id in relay_id:
- interface_list = []
- address = conf.return_value("{0} address".format(id))
- description = conf.return_value("{0} description".format(id))
- port = conf.return_value("{0} port".format(id))
-
- # split the interface name listing and form a list
- if conf.exists("{0} interface".format(id)):
- intfs_names = []
- intfs_names = conf.return_values("{0} interface".format(id))
-
- for name in intfs_names:
- interface_list.append(name)
-
- relay = {
- "id": id,
- "address": address,
- "description": description,
- "interfaces" : interface_list,
- "port": port
+ if not conf.exists('service broadcast-relay'):
+ return None
+ else:
+ conf.set_level('service broadcast-relay')
+
+ # Service can be disabled by user
+ if conf.exists('disable'):
+ relay['disabled'] = True
+ return relay
+
+ # Parse configuration of each individual instance
+ if conf.exists('id'):
+ for id in conf.list_nodes('id'):
+ conf.set_level('service broadcast-relay id {0}'.format(id))
+ config = {
+ 'id': id,
+ 'disabled': False,
+ 'address': '',
+ 'description': '',
+ 'interfaces': [],
+ 'port': ''
}
- relays.append(relay)
- return relays
+ # Check if individual broadcast relay service is disabled
+ if conf.exists('disable'):
+ config['disabled'] = True
+
+ # Source IP of forwarded packets, if empty original senders address is used
+ if conf.exists('address'):
+ config['address'] = conf.return_value('address')
+
+ # A description for each individual broadcast relay service
+ if conf.exists('description'):
+ config['description'] = conf.return_value('description')
+
+ # UDP port to listen on for broadcast frames
+ if conf.exists('port'):
+ config['port'] = conf.return_value('port')
+
+ # Network interfaces to listen on for broadcast frames to be relayed
+ if conf.exists('interface'):
+ config['interfaces'] = conf.return_values('interface')
+
+ relay['instances'].append(config)
-def verify(relays):
- for relay in relays:
- if not relay["port"]:
- raise ConfigError("UDP broadcast relay 'id {0}' requires a port number".format(relay["id"]))
+ return relay
- if len(relay["interfaces"]) < 2:
- raise ConfigError("UDP broadcast relay 'id {0}' requires at least 2 interfaces".format(relay["id"]))
+def verify(relay):
+ if relay is None:
+ return None
+
+ if relay['disabled']:
+ return None
+
+ for r in relay['instances']:
+ # we don't have to check this instance when it's disabled
+ if r['disabled']:
+ continue
+
+ # we certainly require a UDP port to listen to
+ if not r['port']:
+ raise ConfigError('UDP broadcast relay "{0}" requires a port number'.format(r['id']))
+
+ # Relaying data without two interface is kinda senseless ...
+ if len(r['interfaces']) < 2:
+ raise ConfigError('UDP broadcast relay "id {0}" requires at least 2 interfaces'.format(r['id']))
return None
-def generate(relays):
- config_header = '### Autogenerated by bcast_relay.py ###\n'
+
+def generate(relay):
+ if relay is None:
+ return None
config_dir = os.path.dirname(config_file)
config_filename = os.path.basename(config_file)
@@ -82,32 +131,43 @@ def generate(relays):
# sort our list
active_configs.sort()
+ # delete old configuration files
for id in active_configs[:]:
- os.unlink(config_file + id)
-
- for relay in relays:
- file = config_file + str(relay["id"])
- interfaces = ' '.join(str(intf) for intf in relay["interfaces"])
- config_args = 'DAEMON_ARGS="{0} {1}"\n'.format(relay["port"], interfaces)
-
- f = open(file, 'w')
- f.write(config_header)
- if relay["description"]:
- f.write('# ' + relay["description"] + '\n')
- f.write(config_args)
- f.close()
+ if os.path.exists(config_file + id):
+ os.unlink(config_file + id)
+
+ # If the service is disabled, we can bail out here
+ if relay['disabled']:
+ print('Warning: UDP broadcast relay service will be deactivated because it is disabled')
+ return None
+
+ for r in relay['instances']:
+ # Skip writing instance config when it's disabled
+ if r['disabled']:
+ continue
+
+ # configuration filename contains instance id
+ file = config_file + str(r['id'])
+ tmpl = jinja2.Template(config_tmpl)
+ config_text = tmpl.render(r)
+ with open(file, 'w') as f:
+ f.write(config_text)
return None
-def apply(relays):
+def apply(relay):
# first stop all running services
- cmd = "sudo systemctl stop udp-broadcast-relay@{1..99}"
- os.system(cmd)
+ os.system('sudo systemctl stop udp-broadcast-relay@{1..99}')
+
+ if (relay is None) or relay['disabled']:
+ return None
# start only required service instances
- for relay in relays:
- cmd = "sudo systemctl start udp-broadcast-relay@{0}".format(relay["id"])
- os.system(cmd)
+ for r in relay['instances']:
+ # Don't start individual instance when it's disabled
+ if r['disabled']:
+ continue
+ os.system('sudo systemctl start udp-broadcast-relay@{0}'.format(r['id']))
return None
diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py
new file mode 100755
index 000000000..1458ed1d0
--- /dev/null
+++ b/src/conf_mode/dhcp_server.py
@@ -0,0 +1,798 @@
+#!/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 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.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.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 {{ 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
+
+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,
+ '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
+
+ # 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:
+ 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) 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.')
+
+ # 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']))
+
+ # 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 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('Stop IP address in DHCP range 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('Start IP address {0} of DHCP range 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('Stop IP address {0} of DHCP range 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('Stop IP address {0} of DHCP range should be greater or equal\n' \
+ 'to the start IP address {1} of this range!'.format(stop, start))
+
+ # Range start address must be unique
+ if start in range_start:
+ raise ConfigError('Conflicting DHCP lease range:\n' \
+ 'Pool start IP address {0} defined multipe times!'.format(range['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 IP address {0} defined multipe times!'.format(range['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 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}\n' \
+ '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 address {0} under static mapping {1}\n' \
+ '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\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('Subnets must be unique! Subnet {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 with {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)
+ 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)
diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py
index 3b47ffc98..b98741913 100755
--- a/src/conf_mode/snmp.py
+++ b/src/conf_mode/snmp.py
@@ -24,12 +24,12 @@ import pwd
import time
import jinja2
-import ipaddress
import random
import binascii
import re
import vyos.version
+import vyos.validate
from vyos.config import Config
from vyos import ConfigError
@@ -65,7 +65,6 @@ access_config_tmpl = """
{% endfor %}
{% endif -%}
rwuser {{ vyos_user }}
-
"""
# SNMPS template - be careful if you edit the template.
@@ -85,7 +84,9 @@ usmUser 1 3 {{ u.engineID }} "{{ u.name }}" "{{ u.name }}" NULL {{ u.authOID }}
{% endif %}
createUser {{ vyos_user }} MD5 "{{ vyos_user_pass }}" DES
+{% if v3_engineid %}
oldEngineID {{ v3_engineid }}
+{%- endif -%}
"""
# SNMPS template - be careful if you edit the template.
@@ -142,8 +143,10 @@ agentaddress unix:/run/snmpd.socket{% if listen_on %}{% for li in listen_on %},{
{% if communities -%}
{% for c in communities %}
{% if c.network -%}
-{% for network in c.network %}
+{% for network in c.network_v4 %}
{{ c.authorization }}community {{ c.name }} {{ network }}
+{% endfor %}
+{% for network in c.network_v6 %}
{{ c.authorization }}community6 {{ c.name }} {{ network }}
{% endfor %}
{% else %}
@@ -271,14 +274,19 @@ def get_config():
community = {
'name': name,
'authorization': 'ro',
- 'network': []
+ 'network_v4': [],
+ 'network_v6': []
}
if conf.exists('community {0} authorization'.format(name)):
community['authorization'] = conf.return_value('community {0} authorization'.format(name))
if conf.exists('community {0} network'.format(name)):
- community['network'] = conf.return_values('community {0} network'.format(name))
+ for addr in conf.return_values('community {0} network'.format(name)):
+ if vyos.validate.is_ipv4(addr):
+ community['network_v4'] = addr
+ else:
+ community['network_v6'] = addr
snmp['communities'].append(community)
@@ -295,14 +303,12 @@ def get_config():
if conf.exists('listen-address {0} port'.format(addr)):
port = conf.return_value('listen-address {0} port'.format(addr))
- if ipaddress.ip_address(addr).version == 4:
+ if vyos.validate.is_ipv4(addr):
# udp:127.0.0.1:161
listen = 'udp:' + addr + ':' + port
- elif ipaddress.ip_address(addr).version == 6:
+ else:
# udp6:[::1]:161
listen = 'udp6:' + '[' + addr + ']' + ':' + port
- else:
- raise ConfigError('Invalid IP address version')
snmp['listen_on'].append(listen)
diff --git a/src/conf_mode/tftp_server.py b/src/conf_mode/tftp_server.py
index 2b4732190..b6cf5c09e 100755
--- a/src/conf_mode/tftp_server.py
+++ b/src/conf_mode/tftp_server.py
@@ -22,8 +22,7 @@ import stat
import pwd
import jinja2
-import ipaddress
-import netifaces
+import vyos.validate
from vyos.config import Config
from vyos import ConfigError
@@ -47,6 +46,7 @@ TFTP_ADDRESS="{% for a in listen_ipv6 -%}[{{ a }}]:{{ port }}{{- " --address " i
{%- endif %}
TFTP_OPTIONS="--secure {% if allow_upload %}--create --umask 000{% endif %}"
+
"""
default_config_data = {
@@ -57,20 +57,6 @@ default_config_data = {
'listen_ipv6': []
}
-# Verify if an IP address is assigned to any interface, IPv4 and IPv6
-def addrok(ipaddr, ipversion):
- # For every available interface on this system
- for interface in netifaces.interfaces():
- # If it has any IPv4 or IPv6 address (depending on ipversion) configured
- if ipversion in netifaces.ifaddresses(interface).keys():
- # For every configured IP address
- for addr in netifaces.ifaddresses(interface)[ipversion]:
- # Check if it matches to the address requested
- if addr['addr'] == ipaddr:
- return True
-
- return False
-
def get_config():
tftpd = default_config_data
conf = Config()
@@ -90,10 +76,9 @@ def get_config():
if conf.exists('listen-address'):
for addr in conf.return_values('listen-address'):
- if (ipaddress.ip_address(addr).version == 4):
+ if vyos.validate.is_ipv4(addr):
tftpd['listen_ipv4'].append(addr)
-
- if (ipaddress.ip_address(addr).version == 6):
+ else:
tftpd['listen_ipv6'].append(addr)
return tftpd
@@ -110,13 +95,13 @@ def verify(tftpd):
if not (tftpd['listen_ipv4'] or tftpd['listen_ipv6']):
raise ConfigError('TFTP server listen address must be configured!')
- for address in tftpd['listen_ipv4']:
- if not addrok(address, netifaces.AF_INET):
- raise ConfigError('TFTP server listen address "{0}" not configured on this system.'.format(address))
+ for addr in tftpd['listen_ipv4']:
+ if not vyos.validate.is_addr_assigned(addr):
+ raise ConfigError('TFTP server IPv4 listen address "{0}" not configured!'.format(addr))
- for address in tftpd['listen_ipv6']:
- if not addrok(address, netifaces.AF_INET6):
- raise ConfigError('TFTP server listen address "{0}" not configured on this system.'.format(address))
+ for addr in tftpd['listen_ipv6']:
+ if not vyos.validate.is_addr_assigned(addr):
+ raise ConfigError('TFTP server IPv6 listen address "{0}" not configured!'.format(addr))
return None
diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py
index d21e3ef40..28633f1b4 100755
--- a/src/conf_mode/vrrp.py
+++ b/src/conf_mode/vrrp.py
@@ -273,7 +273,7 @@ def verify(data):
count = len(_groups) - 1
index = 0
while (index < count):
- if _groups[index]["vrid"] == _groups[index + 1]["vrid"]:
+ if (_groups[index]["vrid"] == _groups[index + 1]["vrid"]) and (_groups[index]["interface"] == _groups[index + 1]["interface"]):
raise ConfigError("VRID {0} is used in groups {1} and {2} that both use interface {3}. Groups on the same interface must use different VRIDs".format(
_groups[index]["vrid"], _groups[index]["name"], _groups[index + 1]["name"], _groups[index]["interface"]))
else:
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
+
+