From d4c10902026b76a6372cf738fc98860b29b39759 Mon Sep 17 00:00:00 2001
From: Daniil Baturin <daniil@baturin.org>
Date: Wed, 16 May 2018 16:01:38 +0200
Subject: T644: remove prefixing from all scripts and update environment
 variables with VyOS paths.

---
 src/completion/list_dumpable_interfaces.py    |  14 ++
 src/completion/list_interfaces.py             |   8 +
 src/conf_mode/bcast_relay.py                  | 124 +++++++++++++
 src/conf_mode/dns_forwarding.py               | 213 ++++++++++++++++++++++
 src/conf_mode/host_name.py                    |  96 ++++++++++
 src/conf_mode/lldp.py                         | 218 ++++++++++++++++++++++
 src/conf_mode/mdns_repeater.py                |  93 ++++++++++
 src/conf_mode/ntp.py                          | 161 +++++++++++++++++
 src/conf_mode/ssh.py                          | 249 ++++++++++++++++++++++++++
 src/conf_mode/task_scheduler.py               | 148 +++++++++++++++
 src/conf_mode/vyos_config_bcast_relay.py      | 124 -------------
 src/conf_mode/vyos_config_dns_forwarding.py   | 213 ----------------------
 src/conf_mode/vyos_config_host_name.py        |  96 ----------
 src/conf_mode/vyos_config_lldp.py             | 218 ----------------------
 src/conf_mode/vyos_config_mdns_repeater.py    |  93 ----------
 src/conf_mode/vyos_config_ntp.py              | 161 -----------------
 src/conf_mode/vyos_config_ssh.py              | 249 --------------------------
 src/conf_mode/vyos_update_crontab.py          | 148 ---------------
 src/op_mode/dns_forwarding_restart.sh         |   8 +
 src/op_mode/dns_forwarding_statistics.py      |  24 +++
 src/op_mode/version.py                        | 123 +++++++++++++
 src/op_mode/vyos_dns_forwarding_statistics.py |  24 ---
 src/op_mode/vyos_list_dumpable_interfaces.py  |  14 --
 src/op_mode/vyos_list_interfaces.py           |   8 -
 src/op_mode/vyos_restart_dns_forwarding.sh    |   8 -
 src/op_mode/vyos_show_version.py              | 123 -------------
 src/tests/test_vyos_update_crontab.py         |  20 +--
 27 files changed, 1489 insertions(+), 1489 deletions(-)
 create mode 100755 src/completion/list_dumpable_interfaces.py
 create mode 100755 src/completion/list_interfaces.py
 create mode 100755 src/conf_mode/bcast_relay.py
 create mode 100755 src/conf_mode/dns_forwarding.py
 create mode 100755 src/conf_mode/host_name.py
 create mode 100644 src/conf_mode/lldp.py
 create mode 100755 src/conf_mode/mdns_repeater.py
 create mode 100755 src/conf_mode/ntp.py
 create mode 100755 src/conf_mode/ssh.py
 create mode 100755 src/conf_mode/task_scheduler.py
 delete mode 100755 src/conf_mode/vyos_config_bcast_relay.py
 delete mode 100755 src/conf_mode/vyos_config_dns_forwarding.py
 delete mode 100755 src/conf_mode/vyos_config_host_name.py
 delete mode 100644 src/conf_mode/vyos_config_lldp.py
 delete mode 100755 src/conf_mode/vyos_config_mdns_repeater.py
 delete mode 100755 src/conf_mode/vyos_config_ntp.py
 delete mode 100755 src/conf_mode/vyos_config_ssh.py
 delete mode 100755 src/conf_mode/vyos_update_crontab.py
 create mode 100755 src/op_mode/dns_forwarding_restart.sh
 create mode 100755 src/op_mode/dns_forwarding_statistics.py
 create mode 100755 src/op_mode/version.py
 delete mode 100755 src/op_mode/vyos_dns_forwarding_statistics.py
 delete mode 100755 src/op_mode/vyos_list_dumpable_interfaces.py
 delete mode 100755 src/op_mode/vyos_list_interfaces.py
 delete mode 100755 src/op_mode/vyos_restart_dns_forwarding.sh
 delete mode 100755 src/op_mode/vyos_show_version.py

(limited to 'src')

diff --git a/src/completion/list_dumpable_interfaces.py b/src/completion/list_dumpable_interfaces.py
new file mode 100755
index 000000000..53ee89633
--- /dev/null
+++ b/src/completion/list_dumpable_interfaces.py
@@ -0,0 +1,14 @@
+#!/usr/bin/env python3
+
+# Extract the list of interfaces available for traffic dumps from tcpdump -D
+
+import re
+import subprocess
+
+if __name__ == '__main__':
+    out = subprocess.check_output(['/usr/sbin/tcpdump', '-D']).decode().strip()
+    out = out.split("\n")
+
+    intfs = " ".join(map(lambda s: re.search(r'\d+\.(\S+)\s', s).group(1), out))
+
+    print(intfs)
diff --git a/src/completion/list_interfaces.py b/src/completion/list_interfaces.py
new file mode 100755
index 000000000..59c9dffad
--- /dev/null
+++ b/src/completion/list_interfaces.py
@@ -0,0 +1,8 @@
+#!/usr/bin/env python3
+
+import netifaces
+
+if __name__ == '__main__':
+    interfaces = netifaces.interfaces()
+
+    print(" ".join(interfaces))
diff --git a/src/conf_mode/bcast_relay.py b/src/conf_mode/bcast_relay.py
new file mode 100755
index 000000000..785690d9c
--- /dev/null
+++ b/src/conf_mode/bcast_relay.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2017 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 fnmatch
+import time
+import subprocess
+
+from vyos.config import Config
+from vyos import ConfigError
+
+config_file = r'/etc/default/udp-broadcast-relay'
+
+def get_config():
+    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
+            }
+        relays.append(relay)
+
+    return relays
+
+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"]))
+
+        if len(relay["interfaces"]) < 2:
+            raise ConfigError("UDP broadcast relay 'id {0}' requires at least 2 interfaces".format(relay["id"]))
+
+    return None
+
+def generate(relays):
+    config_header = '### Autogenerated by {0} on {tm} ###\n'.format(os.path.basename(__file__),
+                     tm=time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()))
+
+    config_dir = os.path.dirname(config_file)
+    config_filename = os.path.basename(config_file)
+    active_configs = []
+
+    for config in fnmatch.filter(os.listdir(config_dir), config_filename + '*'):
+        # determine prefix length to identify service instance
+        prefix_len = len(config_filename)
+        active_configs.append(config[prefix_len:])
+
+    # sort our list
+    active_configs.sort()
+
+    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()
+
+    return None
+
+def apply(relays):
+    # first stop all running services
+    cmd = "sudo systemctl stop udp-broadcast-relay@{1..99}"
+    os.system(cmd)
+
+    # start only required service instances
+    for relay in relays:
+        cmd = "sudo systemctl start udp-broadcast-relay@{0}".format(relay["id"])
+        os.system(cmd)
+
+    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/dns_forwarding.py b/src/conf_mode/dns_forwarding.py
new file mode 100755
index 000000000..be48cde60
--- /dev/null
+++ b/src/conf_mode/dns_forwarding.py
@@ -0,0 +1,213 @@
+#!/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 netifaces
+import jinja2
+
+from vyos.config import Config
+from vyos import ConfigError
+
+config_file = r'/etc/powerdns/recursor.conf'
+
+# XXX: pdns recursor doesn't like whitespace near entry separators,
+# especially in the semicolon-separated lists of name servers.
+# Please be careful if you edit the template.
+config_tmpl = """
+### Autogenerated by vyos-config-dns-forwarding.py ###
+
+# Non-configurable defaults
+daemon=yes
+threads=1
+allow-from=0.0.0.0/0
+log-common-errors=yes
+
+# cache-size
+max-cache-entries={{ cache_size }}
+
+# ignore-hosts-file
+export-etc-hosts={{ export_hosts_file }}
+
+# listen-on
+local-address={{ listen_on | join(',') }}
+
+# domain ... server ...
+{% if domains -%}
+
+forward-zones={% for d in domains %}
+{{ d.name }}={{ d.servers | join(";") }}
+{%- if loop.first %}, {% endif %}
+{% endfor %}
+
+{% endif %}
+
+# name-server
+forward-zones-recurse=.={{ name_servers | join(';') }}
+
+"""
+
+default_config_data = {
+    'cache_size' : 10000,
+    'export_hosts_file': 'yes',
+    'listen_on': [],
+    'interfaces': [],
+    'name_servers': [],
+    'domains': []
+}
+
+
+# borrowed from: https://github.com/donjajo/py-world/blob/master/resolvconfReader.py, THX!
+def get_resolvers(file):
+    resolvers = []
+    try:
+        with open(file, 'r') as resolvconf:
+            for line in resolvconf.readlines():
+                line = line.split('#',1)[0];
+                line = line.rstrip();
+                if 'nameserver' in line:
+                    resolvers.append(line.split()[1])
+        return resolvers
+    except IOError:
+        return []
+
+def get_config():
+    dns = default_config_data
+    conf = Config()
+    if not conf.exists('service dns forwarding'):
+        return None
+    else:
+        conf.set_level('service dns forwarding')
+
+    if conf.exists('cache-size'):
+        cache_size = conf.return_value('cache-size')
+        dns['cache_size'] = cache_size
+
+    if conf.exists('domain'):
+        for node in conf.list_nodes('domain'):
+            server = conf.return_values("domain {0} server".format(node))
+            domain = {
+                "name": node,
+                "servers": server
+            }
+            dns['domains'].append(domain)
+
+    if conf.exists('ignore-hosts-file'):
+        dns.setdefault('export_hosts_file', "no")
+
+    if conf.exists('name-server'):
+        name_servers = conf.return_values('name-server')
+        dns['name_servers'] = dns['name_servers'] + name_servers
+
+    if conf.exists('system'):
+        conf.set_level('system')
+        system_name_servers = []
+        system_name_servers = conf.return_values('name-server')
+        if not system_name_servers:
+            print("DNS forwarding warning: No name-servers set under 'system name-server'\n")
+        else:
+            dns['name_servers'] = dns['name_servers'] + system_name_servers
+        conf.set_level('service dns forwarding')
+
+    ## Hacks and tricks
+
+    # The old VyOS syntax that comes from dnsmasq was "listen-on $interface".
+    # pdns wants addresses instead, so we emulate it by looking up all addresses
+    # of a given interface and writing them to the config
+    if conf.exists('listen-on'):
+        interfaces = conf.return_values('listen-on')
+
+        listen4 = []
+        listen6 = []
+        for interface in interfaces:
+            addrs = netifaces.ifaddresses(interface)
+            for ip4 in addrs[netifaces.AF_INET]:
+                listen4.append(ip4['addr'])
+
+            for ip6 in addrs[netifaces.AF_INET6]:
+                listen6.append(ip6['addr'])
+
+        dns['listen_on'] = listen4 + listen6
+
+        # Save interfaces in the dict for the reference
+        dns['interfaces'] = interfaces
+
+    # Add name servers received from DHCP
+    if conf.exists('dhcp'):
+        interfaces = []
+        interfaces = conf.return_values('dhcp')
+        for interface in interfaces:
+            dhcp_resolvers = get_resolvers("/etc/resolv.conf.dhclient-new-{0}".format(interface))
+            if dhcp_resolvers:
+                dns['name_servers'] = dns['name_servers'] + dhcp_resolvers
+
+    return dns
+
+def verify(dns):
+    # bail out early - looks like removal from running config
+    if dns is None:
+        return None
+
+    if not dns['interfaces']:
+        raise ConfigError('Error: DNS forwarding requires a configured listen interface!')
+
+    for interface in dns['interfaces']:
+        try:
+            netifaces.ifaddresses(interface)[netifaces.AF_INET]
+        except KeyError as e:
+            raise ConfigError('Error: Interface {0} has no IP address assigned'.format(interface))
+
+    if dns['domains']:
+        for domain in dns['domains']:
+            if not domain['servers']:
+                raise ConfigError('Error: No server configured for domain {0}'.format(domain['name']))
+
+    return None
+
+def generate(dns):
+    # bail out early - looks like removal from running config
+    if dns is None:
+        return None
+
+    tmpl = jinja2.Template(config_tmpl, trim_blocks=True)
+
+    config_text = tmpl.render(dns)
+    with open(config_file, 'w') as f:
+        f.write(config_text)
+    return None
+
+def apply(dns):
+    if dns is not None:
+        os.system("systemctl restart pdns-recursor")
+    else:
+        # DNS forwarding is removed in the commit
+        os.system("systemctl stop pdns-recursor")
+        os.unlink(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/conf_mode/host_name.py b/src/conf_mode/host_name.py
new file mode 100755
index 000000000..2a245b211
--- /dev/null
+++ b/src/conf_mode/host_name.py
@@ -0,0 +1,96 @@
+#!/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 os
+import re
+import sys
+import subprocess
+
+from vyos.config import Config
+from vyos.util import ConfigError
+
+hostname_regex = re.compile("^[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]$")
+
+def get_config():
+    conf = Config()
+
+    hostname = conf.return_value("system host-name")
+    domain = conf.return_value("system domain-name")
+
+    # No one likes fixups, but we really don't want VyOS fail to boot
+    # if hostname is not in the config
+    if not hostname:
+        hostname = "vyos"
+
+    if domain:
+        fqdn = "{0}.{1}".format(hostname, domain)
+    else:
+        fqdn = hostname
+
+    return {"hostname": hostname, "domain": domain, "fqdn": fqdn}
+
+def verify(config):
+    # check for invalid host
+	
+    # pattern $VAR(@) "^[[:alnum:]][-.[:alnum:]]*[[:alnum:]]$" ; "invalid host name $VAR(@)"
+    if not hostname_regex.match(config["hostname"]):
+        raise ConfigError('Invalid host name ' + config["hostname"])
+
+    # pattern $VAR(@) "^.{1,63}$" ; "invalid host-name length"
+    length = len(config["hostname"])
+    if length < 1 or length > 63:
+        raise ConfigError('Invalid host-name length, must be less than 63 characters')
+
+    return None
+
+
+def generate(config):
+    # read the hosts file
+    with open('/etc/hosts', 'r') as f:
+        hosts = f.read()
+
+    # get the current hostname
+    old_hostname = subprocess.check_output(['hostname']).decode().strip()
+
+    # replace the local host line
+    hosts = re.sub(r"(127.0.1.1\s+{0}.*)".format(old_hostname), r"127.0.1.1\t{0} # VyOS entry\n".format(config["fqdn"]), hosts)
+
+    with open('/etc/hosts', 'w') as f:
+        f.write(hosts)
+
+    return None
+
+
+def apply(config):
+    os.system("hostnamectl set-hostname {0}".format(config["fqdn"]))
+
+    # restart services that use the hostname
+    os.system("systemctl restart rsyslog.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/lldp.py b/src/conf_mode/lldp.py
new file mode 100644
index 000000000..ba7e9cb13
--- /dev/null
+++ b/src/conf_mode/lldp.py
@@ -0,0 +1,218 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2017 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 re
+import sys
+
+from vyos.config import Config
+from vyos.util import ConfigError
+
+
+
+def get_options(config):
+    options ={}
+    config.set_level('service lldp')
+    options['listen_vlan'] = config.exists('listen-vlan')
+
+    options["addr"] = config.return_value('management-address')
+
+    snmp = config.exists('snmp enable')
+    options["snmp"] = snmp
+    if snmp:
+        config.set_level('')
+        options["sys_snmp"] = config.exists('service snmp')
+        config.set_level('service lldp')
+
+    config.set_level('service lldp legacy-protocols')
+    options["cdp"] = config.exists("cdp")
+    options["edp"] = config.exists("edp")
+    options["fdp"] = config.exists("fdp")
+    options["sonmp"] = config.exists("sonmp")
+    return options
+
+
+def get_interface_list(config):
+    config.set_level('service lldp')
+    intfs_names = config.list_nodes('interface')
+    if len(intfs_names) < 0:
+        return 0
+    interface_list = []
+    for name in intfs_names:
+        config.set_level("service lldp interface {0}".format(name))
+        disable = config.exists('disable')
+        intf = {
+            "name": name,
+            "disable": disable
+        }
+        interface_list.append(intf)
+    return interface_list
+
+
+def get_location_intf(config, name):
+    path = "service lldp interface {0}".format(name)
+    config.set_level(path)
+    if config.exists("location"):
+        return 0
+    config.set_level("{} location".format(path))
+    civic_based = {}
+    elin = None
+    coordinate_based = {}
+
+    if config.exists('civic-based'):
+        config.set_level("{} location civic-based".format(path))
+        cc = config.return_value("country-code")
+        civic_based["country_code"] = cc
+        civic_based["ca_type"] = []
+        ca_types_names = config.list_nodes('ca-type')
+        if ca_types_names:
+            for ca_types_name in ca_types_names:
+                config.set_level("{0} location civic-based ca-type {1}".format(path, ca_types_name))
+                ca_val = config.return_value('ca-value')
+                ca_type = {
+                    "name": ca_types_name,
+                    "ca_val": ca_val
+                }
+                civic_based["ca_type"].append(ca_type)
+
+    elif config.exists("elin"):
+        elin = config.return_value("elin")
+
+    elif config.exists("coordinate-based"):
+        config.set_level("{} location coordinate-based".format(path))
+        alt = config.return_value("altitude")
+        lat = config.return_value("latitude")
+        long = config.return_value("longitude")
+        datum = config.return_value("datum")
+        coordinate_based["altitude"] = alt
+        coordinate_based["latitude"] = lat
+        coordinate_based["longitude"] = long
+        coordinate_based["datum"] = datum
+
+    intf = {
+        "name": name,
+        "civic_based": civic_based,
+        "elin": elin,
+        "coordinate_based": coordinate_based
+
+    }
+    return intf
+
+
+def get_location(config):
+    config.set_level('service lldp')
+    intfs_names = config.list_nodes('interface')
+    if len(intfs_names) < 0:
+        return 0
+    if config.exists("disable"):
+        return 0
+    intfs_location = []
+    for name in intfs_names:
+        intf = get_location_intf(config, name)
+        intfs_location.append(intf)
+    return intfs_location
+
+
+def get_config():
+    conf = Config()
+    options = get_options(conf)
+    interface_list = get_interface_list(conf)
+    location = get_location(conf)
+    lldp = {"options": options, "interface_list": interface_list, "location": location}
+    return lldp
+
+
+def verify(lldp):
+
+    # check location
+    for location in lldp["location"]:
+
+        # check civic-based
+        if len(location["civic_based"]) > 0:
+            if len(location["coordinate_based"]) > 0 or location["elin"]:
+                raise ConfigError("Can only configure 1 location type for interface {0}".format(location["name"]))
+
+            # check country-code
+            if not location["civic_based"]["country_code"]:
+                raise ConfigError("Invalid location for interface {0}: must configure the country code".format(location["name"]))
+
+            if not re.match(r"^[a-zA-Z]{2}$", location["civic_based"]["country_code"]):
+                raise ConfigError("Invalid location for interface {0}: country-code must be 2 characters".format(location["name"]))
+            # check ca-type
+            if len(location["civic_based"]["ca_type"]) < 0:
+                raise ConfigError("Invalid location for interface {0}: must define at least 1 ca-type".format(location["name"]))
+
+            for ca_type in location["civic_based"]["ca_type"]:
+                if not int(ca_type["name"]) in range(0, 129):
+                    raise ConfigError("Invalid location for interface {0}: ca-type must between 0-128".format(location["name"]))
+
+                if not ca_type["ca_val"]:
+                    raise ConfigError("Invalid location for interface {0}: must configure the ca-value for ca-type {1}".format(location["name"],ca_type["name"]))
+
+        # check coordinate-based
+        elif len(location["coordinate_based"]) > 0:
+            # check longitude and latitude
+            if not location["coordinate_based"]["longitude"]:
+                raise ConfigError("Must define longitude for interface {0}".format(location["name"]))
+
+            if not location["coordinate_based"]["latitude"]:
+                raise ConfigError("Must define latitude for interface {0}".format(location["name"]))
+
+            if not re.match(r"^(\d+)(\.\d+)?[nNsS]$", location["coordinate_based"]["latitude"]):
+                raise ConfigError("Invalid location for interface {0}: latitude should be a number followed by S or N".format(location["name"]))
+
+            if not re.match(r"^(\d+)(\.\d+)?[eEwW]$", location["coordinate_based"]["longitude"]):
+                raise ConfigError("Invalid location for interface {0}: longitude should be a number followed by E or W".format(location["name"]))
+
+            # check altitude and datum if exist
+            if location["coordinate_based"]["altitude"]:
+                if not re.match(r"^[-+0-9\.]+$", location["coordinate_based"]["altitude"]):
+                    raise ConfigError("Invalid location for interface {0}: altitude should be a positive or negative number".format(location["name"]))
+
+            if location["coordinate_based"]["datum"]:
+                if not re.match(r"^(WGS84|NAD83|MLLW)$", location["coordinate_based"]["datum"]):
+                    raise ConfigError("Invalid location for interface {0}: datum should be WGS84, NAD83, or MLLW".format(location["name"]))
+
+        # check elin
+        elif len(location["elin"]) > 0:
+            if not re.match(r"^[0-9]{10,25}$", location["elin"]):
+                raise ConfigError("Invalid location for interface {0}: ELIN number must be between 10-25 numbers".format(location["name"]))
+
+    # check options
+    if lldp["options"]["snmp"]:
+        if not lldp["options"]["sys_snmp"]:
+            raise ConfigError("SNMP must be configured to enable LLDP SNMP")
+
+
+def generate(config):
+    pass
+
+
+def apply(config):
+    pass
+
+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/mdns_repeater.py b/src/conf_mode/mdns_repeater.py
new file mode 100755
index 000000000..e648fd64f
--- /dev/null
+++ b/src/conf_mode/mdns_repeater.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2017 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 netifaces
+import time
+
+from vyos.config import Config
+from vyos import ConfigError
+
+config_file = r'/etc/default/mdns-repeater'
+
+def get_config():
+    interface_list = []
+
+    conf = Config()
+    conf.set_level('service mdns repeater')
+    if not conf.exists(''):
+        return interface_list
+
+    if conf.exists('interface'):
+        intfs_names = []
+        intfs_names = conf.return_values('interface')
+
+        for name in intfs_names:
+            interface_list.append(name)
+
+    return interface_list
+
+def verify(mdns):
+    # '0' interfaces are possible, think of service deletion. Only '1' is not supported!
+    if len(mdns) == 1:
+        raise ConfigError('At least 2 interfaces must be specified but %d given!' % len(mdns))
+
+    # For mdns-repeater to work it is essential that the interfaces
+    # have an IP address assigned
+    for intf in mdns:
+        try:
+            netifaces.ifaddresses(intf)[netifaces.AF_INET]
+        except KeyError as e:
+            raise ConfigError('No IP address configured for interface "%s"!' % intf)
+
+    return None
+
+def generate(mdns):
+    config_header = '### Autogenerated by vyos-update-mdns-repeater.py on {tm} ###\n'.format(tm=time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()))
+    if len(mdns) > 0:
+        config_args = 'DAEMON_ARGS="' + ' '.join(str(e) for e in mdns) + '"\n'
+    else:
+        config_args = 'DAEMON_ARGS=""\n'
+
+    # write new configuration file
+    f = open(config_file, 'w')
+    f.write(config_header)
+    f.write(config_args)
+    f.close()
+
+    return None
+
+def apply(mdns):
+    if len(mdns) == 0:
+        cmd = "sudo systemctl stop mdns-repeater"
+    else:
+        cmd = "sudo systemctl restart mdns-repeater"
+
+    os.system(cmd)
+    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/ntp.py b/src/conf_mode/ntp.py
new file mode 100755
index 000000000..8be12e44e
--- /dev/null
+++ b/src/conf_mode/ntp.py
@@ -0,0 +1,161 @@
+#!/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 jinja2
+import ipaddress
+
+from vyos.config import Config
+from vyos import ConfigError
+
+config_file = r'/etc/ntp.conf'
+
+# Please be careful if you edit the template.
+config_tmpl = """
+### Autogenerated by vyos-config-ntp.py ###
+
+#
+# Non-configurable defaults
+#
+driftfile /var/lib/ntp/ntp.drift
+# By default, only allow ntpd to query time sources, ignore any incoming requests
+restrict default ignore
+# Local users have unrestricted access, allowing reconfiguration via ntpdc
+restrict 127.0.0.1
+restrict -6 ::1
+
+
+#
+# Configurable section
+#
+
+{% if servers -%}
+{% for s in servers -%}
+# Server configuration for: {{ s.name }}
+server {{ s.name }} iburst {{ s.options | join(" ") }}
+
+{% endfor -%}
+{% endif %}
+
+{% if allowed_networks -%}
+{% for n in allowed_networks -%}
+# Client configuration for network: {{ n.network }}
+restrict {{ n.address }} mask {{ n.netmask }} nomodify notrap nopeer
+
+{% endfor -%}
+{% endif %}
+
+"""
+
+default_config_data = {
+    'servers': [],
+    'allowed_networks': []
+}
+
+def get_config():
+    ntp = default_config_data
+    conf = Config()
+    if not conf.exists('system ntp'):
+        return None
+    else:
+        conf.set_level('system ntp')
+
+    if conf.exists('allow-clients address'):
+        networks = conf.return_values('allow-clients address')
+        for n in networks:
+            addr = ipaddress.ip_network(n)
+            net = {
+                "network" : n,
+                "address" : addr.network_address,
+                "netmask" : addr.netmask
+            }
+
+            ntp['allowed_networks'].append(net)
+
+    if conf.exists('server'):
+        for node in conf.list_nodes('server'):
+            options = []
+            server = {
+                "name": node,
+                "options": []
+            }
+            if conf.exists('server {0} dynamic'.format(node)):
+                options.append('dynamic')
+            if conf.exists('server {0} noselect'.format(node)):
+                options.append('noselect')
+            if conf.exists('server {0} preempt'.format(node)):
+                options.append('preempt')
+            if conf.exists('server {0} prefer'.format(node)):
+                options.append('prefer')
+
+            server['options'] = options
+            ntp['servers'].append(server)
+
+    return ntp
+
+def verify(ntp):
+    # bail out early - looks like removal from running config
+    if ntp is None:
+        return None
+
+    # Configuring allowed clients without a server makes no sense
+    if len(ntp['allowed_networks']) and not len(ntp['servers']):
+        raise ConfigError('NTP server not configured')
+
+    for n in ntp['allowed_networks']:
+        try:
+            addr = ipaddress.ip_network( n['network'] )
+            break
+        except ValueError:
+            raise ConfigError("{0} does not appear to be a valid IPv4 or IPv6 network, check host bits!".format(n['network']))
+
+    return None
+
+def generate(ntp):
+    # bail out early - looks like removal from running config
+    if ntp is None:
+        return None
+
+    tmpl = jinja2.Template(config_tmpl)
+    config_text = tmpl.render(ntp)
+    with open(config_file, 'w') as f:
+        f.write(config_text)
+
+    return None
+
+def apply(ntp):
+    if ntp is not None:
+        os.system('sudo /usr/sbin/invoke-rc.d ntp force-reload')
+    else:
+        # NTP suuport is removed in the commit
+        os.system('sudo /usr/sbin/invoke-rc.d ntp stop')
+        os.unlink(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/conf_mode/ssh.py b/src/conf_mode/ssh.py
new file mode 100755
index 000000000..a4857bba9
--- /dev/null
+++ b/src/conf_mode/ssh.py
@@ -0,0 +1,249 @@
+#!/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 jinja2
+
+from vyos.config import Config
+from vyos import ConfigError
+
+config_file = r'/etc/ssh/sshd_config'
+
+# Please be careful if you edit the template.
+config_tmpl = """
+
+### Autogenerated by vyos-config-ssh.py ###
+
+# Non-configurable defaults
+Protocol 2
+HostKey /etc/ssh/ssh_host_rsa_key
+HostKey /etc/ssh/ssh_host_dsa_key
+HostKey /etc/ssh/ssh_host_ecdsa_key
+HostKey /etc/ssh/ssh_host_ed25519_key
+UsePrivilegeSeparation yes
+KeyRegenerationInterval 3600
+ServerKeyBits 1024
+SyslogFacility AUTH
+LoginGraceTime 120
+StrictModes yes
+RSAAuthentication yes
+PubkeyAuthentication yes
+IgnoreRhosts yes
+RhostsRSAAuthentication no
+HostbasedAuthentication no
+PermitEmptyPasswords no
+ChallengeResponseAuthentication no
+X11Forwarding yes
+X11DisplayOffset 10
+PrintMotd no
+PrintLastLog yes
+TCPKeepAlive yes
+Banner /etc/issue.net
+Subsystem sftp /usr/lib/openssh/sftp-server
+UsePAM yes
+HostKey /etc/ssh/ssh_host_key
+PermitRootLogin no
+
+# Specifies whether sshd should look up the remote host name,
+# and to check that the resolved host name for the remote IP
+# address maps back to the very same IP address.
+UseDNS {{ host_validation }}
+
+# Specifies the port number that sshd listens on.  The default is 22.
+# Multiple options of this type are permitted.
+Port {{ port }}
+
+# Gives the verbosity level that is used when logging messages from sshd
+LogLevel {{ log_level }}
+
+# Specifies whether password authentication is allowed
+PasswordAuthentication {{ password_authentication }}
+
+{% if listen_on -%}
+# Specifies the local addresses sshd should listen on
+{% for a in listen_on -%}
+ListenAddress {{ a }}
+{% endfor -%}
+{% endif %}
+
+{% if ciphers -%}
+# Specifies the ciphers allowed. Multiple ciphers must be comma-separated.
+#
+# NOTE: As of now, there is no 'multi' node for 'ciphers', thus we have only one :/
+Ciphers {{ ciphers | join(",") }}
+{% endif %}
+
+{% if mac -%}
+# Specifies the available MAC (message authentication code) algorithms. The MAC
+# algorithm is used for data integrity protection. Multiple algorithms must be
+# comma-separated.
+#
+# NOTE: As of now, there is no 'multi' node for 'mac', thus we have only one :/
+MACs {{ mac | join(",") }}
+{% endif %}
+
+{% if key_exchange -%}
+# Specifies the available KEX (Key Exchange) algorithms. Multiple algorithms must
+# be comma-separated.
+#
+# NOTE: As of now, there is no 'multi' node for 'key-exchange', thus we have only one :/
+KexAlgorithms {{ key_exchange | join(",") }}
+{% endif %}
+
+{% if allow_users -%}
+# This keyword can be followed by a list of user name patterns, separated by spaces.
+# If specified, login is allowed only for user names that match one of the patterns. 
+# Only user names are valid, a numerical user ID is not recognized.
+AllowUsers {{ allow_users | join(" ") }}
+{% endif %}
+
+{% if allow_groups -%}
+# This keyword can be followed by a list of group name patterns, separated by spaces.
+# If specified, login is allowed only for users whose primary group or supplementary
+# group list matches one of the patterns. Only group names are valid, a numerical group
+# ID is not recognized.
+AllowGroups {{ allow_groups | join(" ") }}
+{% endif %}
+
+{% if deny_users -%}
+# This keyword can be followed by a list of user name patterns, separated by spaces.
+# Login is disallowed for user names that match one of the patterns. Only user names
+# are valid, a numerical user ID is not	recognized.
+DenyUsers {{ deny_users | join(" ") }}
+{% endif %}
+
+{% if deny_groups -%}
+# This keyword can be followed by a list of group name patterns, separated by spaces.
+# Login is disallowed for users whose primary group or supplementary group list matches
+# one of the patterns. Only group names are valid, a numerical group ID is not recognized.
+DenyGroups {{ deny_groups | join(" ") }}
+{% endif %}
+"""
+
+default_config_data = {
+    'port' : '22',
+    'log_level': 'INFO',
+    'password_authentication': 'yes',
+    'host_validation': 'yes'
+}
+
+def get_config():
+    ssh = default_config_data
+    conf = Config()
+    if not conf.exists('service ssh'):
+        return None
+    else:
+        conf.set_level('service ssh')
+
+    if conf.exists('access-control allow user'):
+        allow_users = conf.return_values('access-control allow user')
+        ssh.setdefault('allow_users', allow_users)
+
+    if conf.exists('access-control allow group'):
+        allow_groups = conf.return_values('access-control allow group')
+        ssh.setdefault('allow_groups', allow_groups)
+
+    if conf.exists('access-control deny user'):
+        deny_users = conf.return_values('access-control deny user')
+        ssh.setdefault('deny_users', deny_users)
+
+    if conf.exists('access-control deny group'):
+        deny_groups = conf.return_values('access-control deny group')
+        ssh.setdefault('deny_groups', deny_groups)
+
+    if conf.exists('ciphers'):
+        ciphers = conf.return_values('ciphers')
+        ssh.setdefault('ciphers', ciphers)
+
+    if conf.exists('disable-host-validation'):
+        ssh['host_validation'] = 'no'
+
+    if conf.exists('disable-password-authentication'):
+        ssh['password_authentication'] = 'no'
+
+    if conf.exists('key-exchange'):
+        kex = conf.return_values('key-exchange')
+        ssh.setdefault('key_exchange', kex)
+
+    if conf.exists('listen-address'):
+        # We can listen on both IPv4 and IPv6 addresses
+        # Maybe there could be a check in the future if the configured IP address
+        # is configured on this system at all?
+        addresses = conf.return_values('listen-address')
+        listen = []
+
+        for addr in addresses:
+            listen.append(addr)
+
+        ssh.setdefault('listen_on', listen)
+
+    if conf.exists('loglevel'):
+        ssh['log_level'] = conf.return_value('loglevel')
+
+    if conf.exists('mac'):
+        mac = conf.return_values('mac')
+        ssh.setdefault('mac', mac)
+
+    if conf.exists('port'):
+        port = conf.return_value('port')
+        ssh.setdefault('port', port)
+
+    return ssh
+
+def verify(ssh):
+    if ssh is None:
+        return None
+
+    if 'loglevel' in ssh.keys():
+        allowed_loglevel = 'QUIET, FATAL, ERROR, INFO, VERBOSE'
+        if not ssh['loglevel'] in allowed_loglevel:
+            raise ConfigError('loglevel must be one of "{0}"\n'.format(allowed_loglevel))
+
+    return None
+
+def generate(ssh):
+    if ssh is None:
+        return None
+
+    tmpl = jinja2.Template(config_tmpl)
+    config_text = tmpl.render(ssh)
+    with open(config_file, 'w') as f:
+        f.write(config_text)
+    return None
+
+def apply(ssh):
+    if ssh is not None and 'port' in ssh.keys():
+        os.system("sudo systemctl restart ssh")
+    else:
+        # SSH access is removed in the commit
+        os.system("sudo systemctl stop ssh")
+        os.unlink(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/conf_mode/task_scheduler.py b/src/conf_mode/task_scheduler.py
new file mode 100755
index 000000000..c19b88007
--- /dev/null
+++ b/src/conf_mode/task_scheduler.py
@@ -0,0 +1,148 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2017 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+#
+
+import os
+import re
+import sys
+
+from vyos.config import Config
+from vyos import ConfigError
+
+
+crontab_file = "/etc/cron.d/vyos-crontab"
+
+
+def format_task(minute="*", hour="*", day="*", dayofweek="*", month="*", user="root", rawspec=None, command=""):
+    fmt_full = "{minute} {hour} {day} {month} {dayofweek} {user} {command}\n"
+    fmt_raw = "{spec} {user} {command}\n"
+
+    if rawspec is None:
+        s = fmt_full.format(minute=minute, hour=hour, day=day,
+                            dayofweek=dayofweek, month=month, command=command, user=user)
+    else:
+        s = fmt_raw.format(spec=rawspec, user=user, command=command)
+
+    return s
+
+def split_interval(s):
+    result = re.search(r"(\d+)([mdh]?)", s)
+    value = int(result.group(1))
+    suffix = result.group(2)
+    return( (value, suffix) )
+
+def make_command(executable, arguments):
+    if arguments:
+        return("{0} {1}".format(executable, arguments))
+    else:
+        return(executable)
+
+def get_config():
+    conf = Config()
+    conf.set_level("system task-scheduler task")
+    task_names = conf.list_nodes("")
+    tasks = []
+
+    for name in task_names:
+        interval = conf.return_value("{0} interval".format(name))
+        spec = conf.return_value("{0} crontab-spec".format(name))
+        executable = conf.return_value("{0} executable path".format(name))
+        args = conf.return_value("{0} executable arguments".format(name))
+        task = {
+                "name": name,
+                "interval": interval,
+                "spec": spec,
+                "executable": executable,
+                "args": args
+              }
+        tasks.append(task)
+
+    return tasks
+
+def verify(tasks):
+    for task in tasks:
+        if not task["interval"] and not task["spec"]:
+            raise ConfigError("Invalid task {0}: must define either interval or crontab-spec".format(task["name"]))
+
+        if task["interval"]:
+            if task["spec"]:
+                raise ConfigError("Invalid task {0}: cannot use interval and crontab-spec at the same time".format(task["name"]))
+ 
+            if not re.match(r"^\d+[mdh]?$", task["interval"]):
+                raise(ConfigError("Invalid interval {0} in task {1}: interval should be a number optionally followed by m, h, or d".format(task["name"], task["interval"])))
+            else:
+                # Check if values are within allowed range
+                value, suffix = split_interval(task["interval"])
+
+                if not suffix or suffix == "m":
+                    if value > 60:
+                        raise ConfigError("Invalid task {0}: interval in minutes must not exceed 60".format(task["name"]))
+                elif suffix == "h":
+                    if value > 24:
+                        raise ConfigError("Invalid task {0}: interval in hours must not exceed 24".format(task["name"]))
+                elif suffix == "d":
+                    if value > 31:
+                        raise ConfigError("Invalid task {0}: interval in days must not exceed 31".format(task["name"]))
+
+        if not task["executable"]:
+            raise ConfigError("Invalid task {0}: executable is not defined".format(task["name"]))
+        else:
+            # Check if executable exists and is executable
+            if not (os.path.isfile(task["executable"]) and os.access(task["executable"], os.X_OK)):
+                raise ConfigError("Invalid task {0}: file {1} does not exist or is not executable".format(task["name"], task["executable"]))
+
+def generate(tasks):
+    crontab_header = "### Generated by vyos-update-crontab.py ###\n"
+    if len(tasks) == 0:
+        if os.path.exists(crontab_file):
+            os.remove(crontab_file)
+        else:
+            pass
+    else:
+        crontab_lines = []
+        for task in tasks:
+            command = make_command(task["executable"], task["args"])
+            if task["spec"]:
+                line = format_task(command=command, rawspec=task["spec"])
+            else:
+                value, suffix = split_interval(task["interval"])
+                if not suffix or suffix == "m":
+                    line = format_task(command=command, minute="*/{0}".format(value))
+                elif suffix == "h":
+                    line = format_task(command=command, minute="0", hour="*/{0}".format(value))
+                elif suffix == "d":
+                    line = format_task(command=command, minute="0", hour="0", day="*/{0}".format(value))
+            crontab_lines.append(line)
+
+        with open(crontab_file, 'w') as f:
+            f.write(crontab_header)
+            f.writelines(crontab_lines)
+
+def apply(config):
+    # No daemon restarts etc. needed for cron
+    pass
+
+
+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/vyos_config_bcast_relay.py b/src/conf_mode/vyos_config_bcast_relay.py
deleted file mode 100755
index 785690d9c..000000000
--- a/src/conf_mode/vyos_config_bcast_relay.py
+++ /dev/null
@@ -1,124 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2017 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 fnmatch
-import time
-import subprocess
-
-from vyos.config import Config
-from vyos import ConfigError
-
-config_file = r'/etc/default/udp-broadcast-relay'
-
-def get_config():
-    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
-            }
-        relays.append(relay)
-
-    return relays
-
-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"]))
-
-        if len(relay["interfaces"]) < 2:
-            raise ConfigError("UDP broadcast relay 'id {0}' requires at least 2 interfaces".format(relay["id"]))
-
-    return None
-
-def generate(relays):
-    config_header = '### Autogenerated by {0} on {tm} ###\n'.format(os.path.basename(__file__),
-                     tm=time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()))
-
-    config_dir = os.path.dirname(config_file)
-    config_filename = os.path.basename(config_file)
-    active_configs = []
-
-    for config in fnmatch.filter(os.listdir(config_dir), config_filename + '*'):
-        # determine prefix length to identify service instance
-        prefix_len = len(config_filename)
-        active_configs.append(config[prefix_len:])
-
-    # sort our list
-    active_configs.sort()
-
-    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()
-
-    return None
-
-def apply(relays):
-    # first stop all running services
-    cmd = "sudo systemctl stop udp-broadcast-relay@{1..99}"
-    os.system(cmd)
-
-    # start only required service instances
-    for relay in relays:
-        cmd = "sudo systemctl start udp-broadcast-relay@{0}".format(relay["id"])
-        os.system(cmd)
-
-    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/vyos_config_dns_forwarding.py b/src/conf_mode/vyos_config_dns_forwarding.py
deleted file mode 100755
index be48cde60..000000000
--- a/src/conf_mode/vyos_config_dns_forwarding.py
+++ /dev/null
@@ -1,213 +0,0 @@
-#!/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 netifaces
-import jinja2
-
-from vyos.config import Config
-from vyos import ConfigError
-
-config_file = r'/etc/powerdns/recursor.conf'
-
-# XXX: pdns recursor doesn't like whitespace near entry separators,
-# especially in the semicolon-separated lists of name servers.
-# Please be careful if you edit the template.
-config_tmpl = """
-### Autogenerated by vyos-config-dns-forwarding.py ###
-
-# Non-configurable defaults
-daemon=yes
-threads=1
-allow-from=0.0.0.0/0
-log-common-errors=yes
-
-# cache-size
-max-cache-entries={{ cache_size }}
-
-# ignore-hosts-file
-export-etc-hosts={{ export_hosts_file }}
-
-# listen-on
-local-address={{ listen_on | join(',') }}
-
-# domain ... server ...
-{% if domains -%}
-
-forward-zones={% for d in domains %}
-{{ d.name }}={{ d.servers | join(";") }}
-{%- if loop.first %}, {% endif %}
-{% endfor %}
-
-{% endif %}
-
-# name-server
-forward-zones-recurse=.={{ name_servers | join(';') }}
-
-"""
-
-default_config_data = {
-    'cache_size' : 10000,
-    'export_hosts_file': 'yes',
-    'listen_on': [],
-    'interfaces': [],
-    'name_servers': [],
-    'domains': []
-}
-
-
-# borrowed from: https://github.com/donjajo/py-world/blob/master/resolvconfReader.py, THX!
-def get_resolvers(file):
-    resolvers = []
-    try:
-        with open(file, 'r') as resolvconf:
-            for line in resolvconf.readlines():
-                line = line.split('#',1)[0];
-                line = line.rstrip();
-                if 'nameserver' in line:
-                    resolvers.append(line.split()[1])
-        return resolvers
-    except IOError:
-        return []
-
-def get_config():
-    dns = default_config_data
-    conf = Config()
-    if not conf.exists('service dns forwarding'):
-        return None
-    else:
-        conf.set_level('service dns forwarding')
-
-    if conf.exists('cache-size'):
-        cache_size = conf.return_value('cache-size')
-        dns['cache_size'] = cache_size
-
-    if conf.exists('domain'):
-        for node in conf.list_nodes('domain'):
-            server = conf.return_values("domain {0} server".format(node))
-            domain = {
-                "name": node,
-                "servers": server
-            }
-            dns['domains'].append(domain)
-
-    if conf.exists('ignore-hosts-file'):
-        dns.setdefault('export_hosts_file', "no")
-
-    if conf.exists('name-server'):
-        name_servers = conf.return_values('name-server')
-        dns['name_servers'] = dns['name_servers'] + name_servers
-
-    if conf.exists('system'):
-        conf.set_level('system')
-        system_name_servers = []
-        system_name_servers = conf.return_values('name-server')
-        if not system_name_servers:
-            print("DNS forwarding warning: No name-servers set under 'system name-server'\n")
-        else:
-            dns['name_servers'] = dns['name_servers'] + system_name_servers
-        conf.set_level('service dns forwarding')
-
-    ## Hacks and tricks
-
-    # The old VyOS syntax that comes from dnsmasq was "listen-on $interface".
-    # pdns wants addresses instead, so we emulate it by looking up all addresses
-    # of a given interface and writing them to the config
-    if conf.exists('listen-on'):
-        interfaces = conf.return_values('listen-on')
-
-        listen4 = []
-        listen6 = []
-        for interface in interfaces:
-            addrs = netifaces.ifaddresses(interface)
-            for ip4 in addrs[netifaces.AF_INET]:
-                listen4.append(ip4['addr'])
-
-            for ip6 in addrs[netifaces.AF_INET6]:
-                listen6.append(ip6['addr'])
-
-        dns['listen_on'] = listen4 + listen6
-
-        # Save interfaces in the dict for the reference
-        dns['interfaces'] = interfaces
-
-    # Add name servers received from DHCP
-    if conf.exists('dhcp'):
-        interfaces = []
-        interfaces = conf.return_values('dhcp')
-        for interface in interfaces:
-            dhcp_resolvers = get_resolvers("/etc/resolv.conf.dhclient-new-{0}".format(interface))
-            if dhcp_resolvers:
-                dns['name_servers'] = dns['name_servers'] + dhcp_resolvers
-
-    return dns
-
-def verify(dns):
-    # bail out early - looks like removal from running config
-    if dns is None:
-        return None
-
-    if not dns['interfaces']:
-        raise ConfigError('Error: DNS forwarding requires a configured listen interface!')
-
-    for interface in dns['interfaces']:
-        try:
-            netifaces.ifaddresses(interface)[netifaces.AF_INET]
-        except KeyError as e:
-            raise ConfigError('Error: Interface {0} has no IP address assigned'.format(interface))
-
-    if dns['domains']:
-        for domain in dns['domains']:
-            if not domain['servers']:
-                raise ConfigError('Error: No server configured for domain {0}'.format(domain['name']))
-
-    return None
-
-def generate(dns):
-    # bail out early - looks like removal from running config
-    if dns is None:
-        return None
-
-    tmpl = jinja2.Template(config_tmpl, trim_blocks=True)
-
-    config_text = tmpl.render(dns)
-    with open(config_file, 'w') as f:
-        f.write(config_text)
-    return None
-
-def apply(dns):
-    if dns is not None:
-        os.system("systemctl restart pdns-recursor")
-    else:
-        # DNS forwarding is removed in the commit
-        os.system("systemctl stop pdns-recursor")
-        os.unlink(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/conf_mode/vyos_config_host_name.py b/src/conf_mode/vyos_config_host_name.py
deleted file mode 100755
index 2a245b211..000000000
--- a/src/conf_mode/vyos_config_host_name.py
+++ /dev/null
@@ -1,96 +0,0 @@
-#!/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 os
-import re
-import sys
-import subprocess
-
-from vyos.config import Config
-from vyos.util import ConfigError
-
-hostname_regex = re.compile("^[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]$")
-
-def get_config():
-    conf = Config()
-
-    hostname = conf.return_value("system host-name")
-    domain = conf.return_value("system domain-name")
-
-    # No one likes fixups, but we really don't want VyOS fail to boot
-    # if hostname is not in the config
-    if not hostname:
-        hostname = "vyos"
-
-    if domain:
-        fqdn = "{0}.{1}".format(hostname, domain)
-    else:
-        fqdn = hostname
-
-    return {"hostname": hostname, "domain": domain, "fqdn": fqdn}
-
-def verify(config):
-    # check for invalid host
-	
-    # pattern $VAR(@) "^[[:alnum:]][-.[:alnum:]]*[[:alnum:]]$" ; "invalid host name $VAR(@)"
-    if not hostname_regex.match(config["hostname"]):
-        raise ConfigError('Invalid host name ' + config["hostname"])
-
-    # pattern $VAR(@) "^.{1,63}$" ; "invalid host-name length"
-    length = len(config["hostname"])
-    if length < 1 or length > 63:
-        raise ConfigError('Invalid host-name length, must be less than 63 characters')
-
-    return None
-
-
-def generate(config):
-    # read the hosts file
-    with open('/etc/hosts', 'r') as f:
-        hosts = f.read()
-
-    # get the current hostname
-    old_hostname = subprocess.check_output(['hostname']).decode().strip()
-
-    # replace the local host line
-    hosts = re.sub(r"(127.0.1.1\s+{0}.*)".format(old_hostname), r"127.0.1.1\t{0} # VyOS entry\n".format(config["fqdn"]), hosts)
-
-    with open('/etc/hosts', 'w') as f:
-        f.write(hosts)
-
-    return None
-
-
-def apply(config):
-    os.system("hostnamectl set-hostname {0}".format(config["fqdn"]))
-
-    # restart services that use the hostname
-    os.system("systemctl restart rsyslog.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/vyos_config_lldp.py b/src/conf_mode/vyos_config_lldp.py
deleted file mode 100644
index ba7e9cb13..000000000
--- a/src/conf_mode/vyos_config_lldp.py
+++ /dev/null
@@ -1,218 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2017 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 re
-import sys
-
-from vyos.config import Config
-from vyos.util import ConfigError
-
-
-
-def get_options(config):
-    options ={}
-    config.set_level('service lldp')
-    options['listen_vlan'] = config.exists('listen-vlan')
-
-    options["addr"] = config.return_value('management-address')
-
-    snmp = config.exists('snmp enable')
-    options["snmp"] = snmp
-    if snmp:
-        config.set_level('')
-        options["sys_snmp"] = config.exists('service snmp')
-        config.set_level('service lldp')
-
-    config.set_level('service lldp legacy-protocols')
-    options["cdp"] = config.exists("cdp")
-    options["edp"] = config.exists("edp")
-    options["fdp"] = config.exists("fdp")
-    options["sonmp"] = config.exists("sonmp")
-    return options
-
-
-def get_interface_list(config):
-    config.set_level('service lldp')
-    intfs_names = config.list_nodes('interface')
-    if len(intfs_names) < 0:
-        return 0
-    interface_list = []
-    for name in intfs_names:
-        config.set_level("service lldp interface {0}".format(name))
-        disable = config.exists('disable')
-        intf = {
-            "name": name,
-            "disable": disable
-        }
-        interface_list.append(intf)
-    return interface_list
-
-
-def get_location_intf(config, name):
-    path = "service lldp interface {0}".format(name)
-    config.set_level(path)
-    if config.exists("location"):
-        return 0
-    config.set_level("{} location".format(path))
-    civic_based = {}
-    elin = None
-    coordinate_based = {}
-
-    if config.exists('civic-based'):
-        config.set_level("{} location civic-based".format(path))
-        cc = config.return_value("country-code")
-        civic_based["country_code"] = cc
-        civic_based["ca_type"] = []
-        ca_types_names = config.list_nodes('ca-type')
-        if ca_types_names:
-            for ca_types_name in ca_types_names:
-                config.set_level("{0} location civic-based ca-type {1}".format(path, ca_types_name))
-                ca_val = config.return_value('ca-value')
-                ca_type = {
-                    "name": ca_types_name,
-                    "ca_val": ca_val
-                }
-                civic_based["ca_type"].append(ca_type)
-
-    elif config.exists("elin"):
-        elin = config.return_value("elin")
-
-    elif config.exists("coordinate-based"):
-        config.set_level("{} location coordinate-based".format(path))
-        alt = config.return_value("altitude")
-        lat = config.return_value("latitude")
-        long = config.return_value("longitude")
-        datum = config.return_value("datum")
-        coordinate_based["altitude"] = alt
-        coordinate_based["latitude"] = lat
-        coordinate_based["longitude"] = long
-        coordinate_based["datum"] = datum
-
-    intf = {
-        "name": name,
-        "civic_based": civic_based,
-        "elin": elin,
-        "coordinate_based": coordinate_based
-
-    }
-    return intf
-
-
-def get_location(config):
-    config.set_level('service lldp')
-    intfs_names = config.list_nodes('interface')
-    if len(intfs_names) < 0:
-        return 0
-    if config.exists("disable"):
-        return 0
-    intfs_location = []
-    for name in intfs_names:
-        intf = get_location_intf(config, name)
-        intfs_location.append(intf)
-    return intfs_location
-
-
-def get_config():
-    conf = Config()
-    options = get_options(conf)
-    interface_list = get_interface_list(conf)
-    location = get_location(conf)
-    lldp = {"options": options, "interface_list": interface_list, "location": location}
-    return lldp
-
-
-def verify(lldp):
-
-    # check location
-    for location in lldp["location"]:
-
-        # check civic-based
-        if len(location["civic_based"]) > 0:
-            if len(location["coordinate_based"]) > 0 or location["elin"]:
-                raise ConfigError("Can only configure 1 location type for interface {0}".format(location["name"]))
-
-            # check country-code
-            if not location["civic_based"]["country_code"]:
-                raise ConfigError("Invalid location for interface {0}: must configure the country code".format(location["name"]))
-
-            if not re.match(r"^[a-zA-Z]{2}$", location["civic_based"]["country_code"]):
-                raise ConfigError("Invalid location for interface {0}: country-code must be 2 characters".format(location["name"]))
-            # check ca-type
-            if len(location["civic_based"]["ca_type"]) < 0:
-                raise ConfigError("Invalid location for interface {0}: must define at least 1 ca-type".format(location["name"]))
-
-            for ca_type in location["civic_based"]["ca_type"]:
-                if not int(ca_type["name"]) in range(0, 129):
-                    raise ConfigError("Invalid location for interface {0}: ca-type must between 0-128".format(location["name"]))
-
-                if not ca_type["ca_val"]:
-                    raise ConfigError("Invalid location for interface {0}: must configure the ca-value for ca-type {1}".format(location["name"],ca_type["name"]))
-
-        # check coordinate-based
-        elif len(location["coordinate_based"]) > 0:
-            # check longitude and latitude
-            if not location["coordinate_based"]["longitude"]:
-                raise ConfigError("Must define longitude for interface {0}".format(location["name"]))
-
-            if not location["coordinate_based"]["latitude"]:
-                raise ConfigError("Must define latitude for interface {0}".format(location["name"]))
-
-            if not re.match(r"^(\d+)(\.\d+)?[nNsS]$", location["coordinate_based"]["latitude"]):
-                raise ConfigError("Invalid location for interface {0}: latitude should be a number followed by S or N".format(location["name"]))
-
-            if not re.match(r"^(\d+)(\.\d+)?[eEwW]$", location["coordinate_based"]["longitude"]):
-                raise ConfigError("Invalid location for interface {0}: longitude should be a number followed by E or W".format(location["name"]))
-
-            # check altitude and datum if exist
-            if location["coordinate_based"]["altitude"]:
-                if not re.match(r"^[-+0-9\.]+$", location["coordinate_based"]["altitude"]):
-                    raise ConfigError("Invalid location for interface {0}: altitude should be a positive or negative number".format(location["name"]))
-
-            if location["coordinate_based"]["datum"]:
-                if not re.match(r"^(WGS84|NAD83|MLLW)$", location["coordinate_based"]["datum"]):
-                    raise ConfigError("Invalid location for interface {0}: datum should be WGS84, NAD83, or MLLW".format(location["name"]))
-
-        # check elin
-        elif len(location["elin"]) > 0:
-            if not re.match(r"^[0-9]{10,25}$", location["elin"]):
-                raise ConfigError("Invalid location for interface {0}: ELIN number must be between 10-25 numbers".format(location["name"]))
-
-    # check options
-    if lldp["options"]["snmp"]:
-        if not lldp["options"]["sys_snmp"]:
-            raise ConfigError("SNMP must be configured to enable LLDP SNMP")
-
-
-def generate(config):
-    pass
-
-
-def apply(config):
-    pass
-
-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/vyos_config_mdns_repeater.py b/src/conf_mode/vyos_config_mdns_repeater.py
deleted file mode 100755
index e648fd64f..000000000
--- a/src/conf_mode/vyos_config_mdns_repeater.py
+++ /dev/null
@@ -1,93 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2017 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 netifaces
-import time
-
-from vyos.config import Config
-from vyos import ConfigError
-
-config_file = r'/etc/default/mdns-repeater'
-
-def get_config():
-    interface_list = []
-
-    conf = Config()
-    conf.set_level('service mdns repeater')
-    if not conf.exists(''):
-        return interface_list
-
-    if conf.exists('interface'):
-        intfs_names = []
-        intfs_names = conf.return_values('interface')
-
-        for name in intfs_names:
-            interface_list.append(name)
-
-    return interface_list
-
-def verify(mdns):
-    # '0' interfaces are possible, think of service deletion. Only '1' is not supported!
-    if len(mdns) == 1:
-        raise ConfigError('At least 2 interfaces must be specified but %d given!' % len(mdns))
-
-    # For mdns-repeater to work it is essential that the interfaces
-    # have an IP address assigned
-    for intf in mdns:
-        try:
-            netifaces.ifaddresses(intf)[netifaces.AF_INET]
-        except KeyError as e:
-            raise ConfigError('No IP address configured for interface "%s"!' % intf)
-
-    return None
-
-def generate(mdns):
-    config_header = '### Autogenerated by vyos-update-mdns-repeater.py on {tm} ###\n'.format(tm=time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()))
-    if len(mdns) > 0:
-        config_args = 'DAEMON_ARGS="' + ' '.join(str(e) for e in mdns) + '"\n'
-    else:
-        config_args = 'DAEMON_ARGS=""\n'
-
-    # write new configuration file
-    f = open(config_file, 'w')
-    f.write(config_header)
-    f.write(config_args)
-    f.close()
-
-    return None
-
-def apply(mdns):
-    if len(mdns) == 0:
-        cmd = "sudo systemctl stop mdns-repeater"
-    else:
-        cmd = "sudo systemctl restart mdns-repeater"
-
-    os.system(cmd)
-    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/vyos_config_ntp.py b/src/conf_mode/vyos_config_ntp.py
deleted file mode 100755
index 8be12e44e..000000000
--- a/src/conf_mode/vyos_config_ntp.py
+++ /dev/null
@@ -1,161 +0,0 @@
-#!/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 jinja2
-import ipaddress
-
-from vyos.config import Config
-from vyos import ConfigError
-
-config_file = r'/etc/ntp.conf'
-
-# Please be careful if you edit the template.
-config_tmpl = """
-### Autogenerated by vyos-config-ntp.py ###
-
-#
-# Non-configurable defaults
-#
-driftfile /var/lib/ntp/ntp.drift
-# By default, only allow ntpd to query time sources, ignore any incoming requests
-restrict default ignore
-# Local users have unrestricted access, allowing reconfiguration via ntpdc
-restrict 127.0.0.1
-restrict -6 ::1
-
-
-#
-# Configurable section
-#
-
-{% if servers -%}
-{% for s in servers -%}
-# Server configuration for: {{ s.name }}
-server {{ s.name }} iburst {{ s.options | join(" ") }}
-
-{% endfor -%}
-{% endif %}
-
-{% if allowed_networks -%}
-{% for n in allowed_networks -%}
-# Client configuration for network: {{ n.network }}
-restrict {{ n.address }} mask {{ n.netmask }} nomodify notrap nopeer
-
-{% endfor -%}
-{% endif %}
-
-"""
-
-default_config_data = {
-    'servers': [],
-    'allowed_networks': []
-}
-
-def get_config():
-    ntp = default_config_data
-    conf = Config()
-    if not conf.exists('system ntp'):
-        return None
-    else:
-        conf.set_level('system ntp')
-
-    if conf.exists('allow-clients address'):
-        networks = conf.return_values('allow-clients address')
-        for n in networks:
-            addr = ipaddress.ip_network(n)
-            net = {
-                "network" : n,
-                "address" : addr.network_address,
-                "netmask" : addr.netmask
-            }
-
-            ntp['allowed_networks'].append(net)
-
-    if conf.exists('server'):
-        for node in conf.list_nodes('server'):
-            options = []
-            server = {
-                "name": node,
-                "options": []
-            }
-            if conf.exists('server {0} dynamic'.format(node)):
-                options.append('dynamic')
-            if conf.exists('server {0} noselect'.format(node)):
-                options.append('noselect')
-            if conf.exists('server {0} preempt'.format(node)):
-                options.append('preempt')
-            if conf.exists('server {0} prefer'.format(node)):
-                options.append('prefer')
-
-            server['options'] = options
-            ntp['servers'].append(server)
-
-    return ntp
-
-def verify(ntp):
-    # bail out early - looks like removal from running config
-    if ntp is None:
-        return None
-
-    # Configuring allowed clients without a server makes no sense
-    if len(ntp['allowed_networks']) and not len(ntp['servers']):
-        raise ConfigError('NTP server not configured')
-
-    for n in ntp['allowed_networks']:
-        try:
-            addr = ipaddress.ip_network( n['network'] )
-            break
-        except ValueError:
-            raise ConfigError("{0} does not appear to be a valid IPv4 or IPv6 network, check host bits!".format(n['network']))
-
-    return None
-
-def generate(ntp):
-    # bail out early - looks like removal from running config
-    if ntp is None:
-        return None
-
-    tmpl = jinja2.Template(config_tmpl)
-    config_text = tmpl.render(ntp)
-    with open(config_file, 'w') as f:
-        f.write(config_text)
-
-    return None
-
-def apply(ntp):
-    if ntp is not None:
-        os.system('sudo /usr/sbin/invoke-rc.d ntp force-reload')
-    else:
-        # NTP suuport is removed in the commit
-        os.system('sudo /usr/sbin/invoke-rc.d ntp stop')
-        os.unlink(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/conf_mode/vyos_config_ssh.py b/src/conf_mode/vyos_config_ssh.py
deleted file mode 100755
index a4857bba9..000000000
--- a/src/conf_mode/vyos_config_ssh.py
+++ /dev/null
@@ -1,249 +0,0 @@
-#!/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 jinja2
-
-from vyos.config import Config
-from vyos import ConfigError
-
-config_file = r'/etc/ssh/sshd_config'
-
-# Please be careful if you edit the template.
-config_tmpl = """
-
-### Autogenerated by vyos-config-ssh.py ###
-
-# Non-configurable defaults
-Protocol 2
-HostKey /etc/ssh/ssh_host_rsa_key
-HostKey /etc/ssh/ssh_host_dsa_key
-HostKey /etc/ssh/ssh_host_ecdsa_key
-HostKey /etc/ssh/ssh_host_ed25519_key
-UsePrivilegeSeparation yes
-KeyRegenerationInterval 3600
-ServerKeyBits 1024
-SyslogFacility AUTH
-LoginGraceTime 120
-StrictModes yes
-RSAAuthentication yes
-PubkeyAuthentication yes
-IgnoreRhosts yes
-RhostsRSAAuthentication no
-HostbasedAuthentication no
-PermitEmptyPasswords no
-ChallengeResponseAuthentication no
-X11Forwarding yes
-X11DisplayOffset 10
-PrintMotd no
-PrintLastLog yes
-TCPKeepAlive yes
-Banner /etc/issue.net
-Subsystem sftp /usr/lib/openssh/sftp-server
-UsePAM yes
-HostKey /etc/ssh/ssh_host_key
-PermitRootLogin no
-
-# Specifies whether sshd should look up the remote host name,
-# and to check that the resolved host name for the remote IP
-# address maps back to the very same IP address.
-UseDNS {{ host_validation }}
-
-# Specifies the port number that sshd listens on.  The default is 22.
-# Multiple options of this type are permitted.
-Port {{ port }}
-
-# Gives the verbosity level that is used when logging messages from sshd
-LogLevel {{ log_level }}
-
-# Specifies whether password authentication is allowed
-PasswordAuthentication {{ password_authentication }}
-
-{% if listen_on -%}
-# Specifies the local addresses sshd should listen on
-{% for a in listen_on -%}
-ListenAddress {{ a }}
-{% endfor -%}
-{% endif %}
-
-{% if ciphers -%}
-# Specifies the ciphers allowed. Multiple ciphers must be comma-separated.
-#
-# NOTE: As of now, there is no 'multi' node for 'ciphers', thus we have only one :/
-Ciphers {{ ciphers | join(",") }}
-{% endif %}
-
-{% if mac -%}
-# Specifies the available MAC (message authentication code) algorithms. The MAC
-# algorithm is used for data integrity protection. Multiple algorithms must be
-# comma-separated.
-#
-# NOTE: As of now, there is no 'multi' node for 'mac', thus we have only one :/
-MACs {{ mac | join(",") }}
-{% endif %}
-
-{% if key_exchange -%}
-# Specifies the available KEX (Key Exchange) algorithms. Multiple algorithms must
-# be comma-separated.
-#
-# NOTE: As of now, there is no 'multi' node for 'key-exchange', thus we have only one :/
-KexAlgorithms {{ key_exchange | join(",") }}
-{% endif %}
-
-{% if allow_users -%}
-# This keyword can be followed by a list of user name patterns, separated by spaces.
-# If specified, login is allowed only for user names that match one of the patterns. 
-# Only user names are valid, a numerical user ID is not recognized.
-AllowUsers {{ allow_users | join(" ") }}
-{% endif %}
-
-{% if allow_groups -%}
-# This keyword can be followed by a list of group name patterns, separated by spaces.
-# If specified, login is allowed only for users whose primary group or supplementary
-# group list matches one of the patterns. Only group names are valid, a numerical group
-# ID is not recognized.
-AllowGroups {{ allow_groups | join(" ") }}
-{% endif %}
-
-{% if deny_users -%}
-# This keyword can be followed by a list of user name patterns, separated by spaces.
-# Login is disallowed for user names that match one of the patterns. Only user names
-# are valid, a numerical user ID is not	recognized.
-DenyUsers {{ deny_users | join(" ") }}
-{% endif %}
-
-{% if deny_groups -%}
-# This keyword can be followed by a list of group name patterns, separated by spaces.
-# Login is disallowed for users whose primary group or supplementary group list matches
-# one of the patterns. Only group names are valid, a numerical group ID is not recognized.
-DenyGroups {{ deny_groups | join(" ") }}
-{% endif %}
-"""
-
-default_config_data = {
-    'port' : '22',
-    'log_level': 'INFO',
-    'password_authentication': 'yes',
-    'host_validation': 'yes'
-}
-
-def get_config():
-    ssh = default_config_data
-    conf = Config()
-    if not conf.exists('service ssh'):
-        return None
-    else:
-        conf.set_level('service ssh')
-
-    if conf.exists('access-control allow user'):
-        allow_users = conf.return_values('access-control allow user')
-        ssh.setdefault('allow_users', allow_users)
-
-    if conf.exists('access-control allow group'):
-        allow_groups = conf.return_values('access-control allow group')
-        ssh.setdefault('allow_groups', allow_groups)
-
-    if conf.exists('access-control deny user'):
-        deny_users = conf.return_values('access-control deny user')
-        ssh.setdefault('deny_users', deny_users)
-
-    if conf.exists('access-control deny group'):
-        deny_groups = conf.return_values('access-control deny group')
-        ssh.setdefault('deny_groups', deny_groups)
-
-    if conf.exists('ciphers'):
-        ciphers = conf.return_values('ciphers')
-        ssh.setdefault('ciphers', ciphers)
-
-    if conf.exists('disable-host-validation'):
-        ssh['host_validation'] = 'no'
-
-    if conf.exists('disable-password-authentication'):
-        ssh['password_authentication'] = 'no'
-
-    if conf.exists('key-exchange'):
-        kex = conf.return_values('key-exchange')
-        ssh.setdefault('key_exchange', kex)
-
-    if conf.exists('listen-address'):
-        # We can listen on both IPv4 and IPv6 addresses
-        # Maybe there could be a check in the future if the configured IP address
-        # is configured on this system at all?
-        addresses = conf.return_values('listen-address')
-        listen = []
-
-        for addr in addresses:
-            listen.append(addr)
-
-        ssh.setdefault('listen_on', listen)
-
-    if conf.exists('loglevel'):
-        ssh['log_level'] = conf.return_value('loglevel')
-
-    if conf.exists('mac'):
-        mac = conf.return_values('mac')
-        ssh.setdefault('mac', mac)
-
-    if conf.exists('port'):
-        port = conf.return_value('port')
-        ssh.setdefault('port', port)
-
-    return ssh
-
-def verify(ssh):
-    if ssh is None:
-        return None
-
-    if 'loglevel' in ssh.keys():
-        allowed_loglevel = 'QUIET, FATAL, ERROR, INFO, VERBOSE'
-        if not ssh['loglevel'] in allowed_loglevel:
-            raise ConfigError('loglevel must be one of "{0}"\n'.format(allowed_loglevel))
-
-    return None
-
-def generate(ssh):
-    if ssh is None:
-        return None
-
-    tmpl = jinja2.Template(config_tmpl)
-    config_text = tmpl.render(ssh)
-    with open(config_file, 'w') as f:
-        f.write(config_text)
-    return None
-
-def apply(ssh):
-    if ssh is not None and 'port' in ssh.keys():
-        os.system("sudo systemctl restart ssh")
-    else:
-        # SSH access is removed in the commit
-        os.system("sudo systemctl stop ssh")
-        os.unlink(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/conf_mode/vyos_update_crontab.py b/src/conf_mode/vyos_update_crontab.py
deleted file mode 100755
index c19b88007..000000000
--- a/src/conf_mode/vyos_update_crontab.py
+++ /dev/null
@@ -1,148 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2017 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-#
-
-import os
-import re
-import sys
-
-from vyos.config import Config
-from vyos import ConfigError
-
-
-crontab_file = "/etc/cron.d/vyos-crontab"
-
-
-def format_task(minute="*", hour="*", day="*", dayofweek="*", month="*", user="root", rawspec=None, command=""):
-    fmt_full = "{minute} {hour} {day} {month} {dayofweek} {user} {command}\n"
-    fmt_raw = "{spec} {user} {command}\n"
-
-    if rawspec is None:
-        s = fmt_full.format(minute=minute, hour=hour, day=day,
-                            dayofweek=dayofweek, month=month, command=command, user=user)
-    else:
-        s = fmt_raw.format(spec=rawspec, user=user, command=command)
-
-    return s
-
-def split_interval(s):
-    result = re.search(r"(\d+)([mdh]?)", s)
-    value = int(result.group(1))
-    suffix = result.group(2)
-    return( (value, suffix) )
-
-def make_command(executable, arguments):
-    if arguments:
-        return("{0} {1}".format(executable, arguments))
-    else:
-        return(executable)
-
-def get_config():
-    conf = Config()
-    conf.set_level("system task-scheduler task")
-    task_names = conf.list_nodes("")
-    tasks = []
-
-    for name in task_names:
-        interval = conf.return_value("{0} interval".format(name))
-        spec = conf.return_value("{0} crontab-spec".format(name))
-        executable = conf.return_value("{0} executable path".format(name))
-        args = conf.return_value("{0} executable arguments".format(name))
-        task = {
-                "name": name,
-                "interval": interval,
-                "spec": spec,
-                "executable": executable,
-                "args": args
-              }
-        tasks.append(task)
-
-    return tasks
-
-def verify(tasks):
-    for task in tasks:
-        if not task["interval"] and not task["spec"]:
-            raise ConfigError("Invalid task {0}: must define either interval or crontab-spec".format(task["name"]))
-
-        if task["interval"]:
-            if task["spec"]:
-                raise ConfigError("Invalid task {0}: cannot use interval and crontab-spec at the same time".format(task["name"]))
- 
-            if not re.match(r"^\d+[mdh]?$", task["interval"]):
-                raise(ConfigError("Invalid interval {0} in task {1}: interval should be a number optionally followed by m, h, or d".format(task["name"], task["interval"])))
-            else:
-                # Check if values are within allowed range
-                value, suffix = split_interval(task["interval"])
-
-                if not suffix or suffix == "m":
-                    if value > 60:
-                        raise ConfigError("Invalid task {0}: interval in minutes must not exceed 60".format(task["name"]))
-                elif suffix == "h":
-                    if value > 24:
-                        raise ConfigError("Invalid task {0}: interval in hours must not exceed 24".format(task["name"]))
-                elif suffix == "d":
-                    if value > 31:
-                        raise ConfigError("Invalid task {0}: interval in days must not exceed 31".format(task["name"]))
-
-        if not task["executable"]:
-            raise ConfigError("Invalid task {0}: executable is not defined".format(task["name"]))
-        else:
-            # Check if executable exists and is executable
-            if not (os.path.isfile(task["executable"]) and os.access(task["executable"], os.X_OK)):
-                raise ConfigError("Invalid task {0}: file {1} does not exist or is not executable".format(task["name"], task["executable"]))
-
-def generate(tasks):
-    crontab_header = "### Generated by vyos-update-crontab.py ###\n"
-    if len(tasks) == 0:
-        if os.path.exists(crontab_file):
-            os.remove(crontab_file)
-        else:
-            pass
-    else:
-        crontab_lines = []
-        for task in tasks:
-            command = make_command(task["executable"], task["args"])
-            if task["spec"]:
-                line = format_task(command=command, rawspec=task["spec"])
-            else:
-                value, suffix = split_interval(task["interval"])
-                if not suffix or suffix == "m":
-                    line = format_task(command=command, minute="*/{0}".format(value))
-                elif suffix == "h":
-                    line = format_task(command=command, minute="0", hour="*/{0}".format(value))
-                elif suffix == "d":
-                    line = format_task(command=command, minute="0", hour="0", day="*/{0}".format(value))
-            crontab_lines.append(line)
-
-        with open(crontab_file, 'w') as f:
-            f.write(crontab_header)
-            f.writelines(crontab_lines)
-
-def apply(config):
-    # No daemon restarts etc. needed for cron
-    pass
-
-
-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/op_mode/dns_forwarding_restart.sh b/src/op_mode/dns_forwarding_restart.sh
new file mode 100755
index 000000000..12106fcc1
--- /dev/null
+++ b/src/op_mode/dns_forwarding_restart.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+if cli-shell-api exists service dns forwarding; then
+    echo "Restarting the DNS forwarding service"
+    systemctl restart pdns-recursor
+else
+    echo "DNS forwarding is not configured"
+fi
diff --git a/src/op_mode/dns_forwarding_statistics.py b/src/op_mode/dns_forwarding_statistics.py
new file mode 100755
index 000000000..3d1e30aee
--- /dev/null
+++ b/src/op_mode/dns_forwarding_statistics.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python3
+
+import subprocess
+import jinja2
+
+PDNS_CMD='/usr/bin/rec_control'
+
+OUT_TMPL_SRC = """
+DNS forwarding statistics:
+
+Cache entries: {{ cache_entries -}}
+Cache size: {{ cache_size }} kbytes
+
+"""
+
+
+if __name__ == '__main__':
+    data = {}
+
+    data['cache_entries'] = subprocess.check_output([PDNS_CMD, 'get cache-entries']).decode()
+    data['cache_size'] = "{0:.2f}".format( int(subprocess.check_output([PDNS_CMD, 'get cache-bytes']).decode()) / 1024 )
+
+    tmpl = jinja2.Template(OUT_TMPL_SRC)
+    print(tmpl.render(data))
diff --git a/src/op_mode/version.py b/src/op_mode/version.py
new file mode 100755
index 000000000..ce3b3b54f
--- /dev/null
+++ b/src/op_mode/version.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2016 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/>.
+#
+# File: vyos-show-version
+# Purpose:
+#    Displays image version and system information.
+#    Used by the "run show version" command.
+
+
+import os
+import sys
+import subprocess
+import argparse
+import json
+
+import pystache
+
+import vyos.version
+import vyos.limericks
+
+
+parser = argparse.ArgumentParser()
+parser.add_argument("-a", "--all", action="store_true", help="Include individual package versions")
+parser.add_argument("-f", "--funny", action="store_true", help="Add something funny to the output")
+parser.add_argument("-j", "--json", action="store_true", help="Produce JSON output")
+
+def read_file(name):
+    try:
+        with open (name, "r") as f:
+            data = f.read()
+        return data.strip()
+    except:
+        # This works since we only read /sys/class/* stuff
+        # with this function
+        return "Unknown"
+
+version_output_tmpl = """
+Version:          VyOS {{version}}
+Built by:         {{built_by}}
+Built on:         {{built_on}}
+Build ID:         {{build_id}}
+
+Architecture:     {{system_arch}}
+Boot via:         {{boot_via}}
+System type:      {{system_type}}
+
+Hardware vendor:  {{hardware_vendor}}
+Hardware model:   {{hardware_model}}
+Hardware S/N:     {{hardware_serial}}
+Hardware UUID:    {{hardware_uuid}}
+
+Copyright:        VyOS maintainers and contributors
+
+"""
+
+if __name__ == '__main__':
+    args = parser.parse_args()
+
+    version_data = vyos.version.get_version_data()
+
+    # Get system architecture (well, kernel architecture rather)
+    version_data['system_arch'] = subprocess.check_output('uname -m', shell=True).decode().strip()
+
+
+    # Get hypervisor name, if any
+    system_type = "bare metal"
+    try:
+        hypervisor = subprocess.check_output('hvinfo 2>/dev/null', shell=True).decode().strip()
+        system_type = "{0} guest".format(hypervisor)
+    except subprocess.CalledProcessError:
+        # hvinfo returns 1 if it cannot detect any hypervisor
+        pass
+    version_data['system_type'] = system_type
+
+
+    # Get boot type, it can be livecd, installed image, or, possible, a system installed
+    # via legacy "install system" mechanism
+    # In installed images, the squashfs image file is named after its image version,
+    # while on livecd it's just "filesystem.squashfs", that's how we tell a livecd boot
+    # from an installed image
+    boot_via = "installed image"
+    if subprocess.call(""" grep -e '^overlay.*/filesystem.squashfs' /proc/mounts >/dev/null""", shell=True) == 0:
+        boot_via = "livecd"
+    elif subprocess.call(""" grep '^overlay /' /proc/mounts >/dev/null """, shell=True) != 0:
+        boot_via = "legacy non-image installation"
+    version_data['boot_via'] = boot_via
+    
+
+    # Get hardware details from DMI
+    version_data['hardware_vendor'] = read_file('/sys/class/dmi/id/sys_vendor')
+    version_data['hardware_model']  = read_file('/sys/class/dmi/id/product_name')
+
+    # These two assume script is run as root, normal users can't access those files
+    version_data['hardware_serial'] = read_file('/sys/class/dmi/id/subsystem/id/product_serial')
+    version_data['hardware_uuid']   = read_file('/sys/class/dmi/id/subsystem/id/product_uuid')
+
+
+    if args.json:
+        print(json.dumps(version_data))
+        sys.exit(0)
+    else:
+        output = pystache.render(version_output_tmpl, version_data).strip()
+        print(output)
+
+        if args.all:
+           print("Package versions:")
+           os.system("dpkg -l")
+
+        if args.funny:
+            print(vyos.limericks.get_random())
diff --git a/src/op_mode/vyos_dns_forwarding_statistics.py b/src/op_mode/vyos_dns_forwarding_statistics.py
deleted file mode 100755
index 3d1e30aee..000000000
--- a/src/op_mode/vyos_dns_forwarding_statistics.py
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/usr/bin/env python3
-
-import subprocess
-import jinja2
-
-PDNS_CMD='/usr/bin/rec_control'
-
-OUT_TMPL_SRC = """
-DNS forwarding statistics:
-
-Cache entries: {{ cache_entries -}}
-Cache size: {{ cache_size }} kbytes
-
-"""
-
-
-if __name__ == '__main__':
-    data = {}
-
-    data['cache_entries'] = subprocess.check_output([PDNS_CMD, 'get cache-entries']).decode()
-    data['cache_size'] = "{0:.2f}".format( int(subprocess.check_output([PDNS_CMD, 'get cache-bytes']).decode()) / 1024 )
-
-    tmpl = jinja2.Template(OUT_TMPL_SRC)
-    print(tmpl.render(data))
diff --git a/src/op_mode/vyos_list_dumpable_interfaces.py b/src/op_mode/vyos_list_dumpable_interfaces.py
deleted file mode 100755
index 53ee89633..000000000
--- a/src/op_mode/vyos_list_dumpable_interfaces.py
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/usr/bin/env python3
-
-# Extract the list of interfaces available for traffic dumps from tcpdump -D
-
-import re
-import subprocess
-
-if __name__ == '__main__':
-    out = subprocess.check_output(['/usr/sbin/tcpdump', '-D']).decode().strip()
-    out = out.split("\n")
-
-    intfs = " ".join(map(lambda s: re.search(r'\d+\.(\S+)\s', s).group(1), out))
-
-    print(intfs)
diff --git a/src/op_mode/vyos_list_interfaces.py b/src/op_mode/vyos_list_interfaces.py
deleted file mode 100755
index 59c9dffad..000000000
--- a/src/op_mode/vyos_list_interfaces.py
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env python3
-
-import netifaces
-
-if __name__ == '__main__':
-    interfaces = netifaces.interfaces()
-
-    print(" ".join(interfaces))
diff --git a/src/op_mode/vyos_restart_dns_forwarding.sh b/src/op_mode/vyos_restart_dns_forwarding.sh
deleted file mode 100755
index 12106fcc1..000000000
--- a/src/op_mode/vyos_restart_dns_forwarding.sh
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/bin/sh
-
-if cli-shell-api exists service dns forwarding; then
-    echo "Restarting the DNS forwarding service"
-    systemctl restart pdns-recursor
-else
-    echo "DNS forwarding is not configured"
-fi
diff --git a/src/op_mode/vyos_show_version.py b/src/op_mode/vyos_show_version.py
deleted file mode 100755
index ce3b3b54f..000000000
--- a/src/op_mode/vyos_show_version.py
+++ /dev/null
@@ -1,123 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2016 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/>.
-#
-# File: vyos-show-version
-# Purpose:
-#    Displays image version and system information.
-#    Used by the "run show version" command.
-
-
-import os
-import sys
-import subprocess
-import argparse
-import json
-
-import pystache
-
-import vyos.version
-import vyos.limericks
-
-
-parser = argparse.ArgumentParser()
-parser.add_argument("-a", "--all", action="store_true", help="Include individual package versions")
-parser.add_argument("-f", "--funny", action="store_true", help="Add something funny to the output")
-parser.add_argument("-j", "--json", action="store_true", help="Produce JSON output")
-
-def read_file(name):
-    try:
-        with open (name, "r") as f:
-            data = f.read()
-        return data.strip()
-    except:
-        # This works since we only read /sys/class/* stuff
-        # with this function
-        return "Unknown"
-
-version_output_tmpl = """
-Version:          VyOS {{version}}
-Built by:         {{built_by}}
-Built on:         {{built_on}}
-Build ID:         {{build_id}}
-
-Architecture:     {{system_arch}}
-Boot via:         {{boot_via}}
-System type:      {{system_type}}
-
-Hardware vendor:  {{hardware_vendor}}
-Hardware model:   {{hardware_model}}
-Hardware S/N:     {{hardware_serial}}
-Hardware UUID:    {{hardware_uuid}}
-
-Copyright:        VyOS maintainers and contributors
-
-"""
-
-if __name__ == '__main__':
-    args = parser.parse_args()
-
-    version_data = vyos.version.get_version_data()
-
-    # Get system architecture (well, kernel architecture rather)
-    version_data['system_arch'] = subprocess.check_output('uname -m', shell=True).decode().strip()
-
-
-    # Get hypervisor name, if any
-    system_type = "bare metal"
-    try:
-        hypervisor = subprocess.check_output('hvinfo 2>/dev/null', shell=True).decode().strip()
-        system_type = "{0} guest".format(hypervisor)
-    except subprocess.CalledProcessError:
-        # hvinfo returns 1 if it cannot detect any hypervisor
-        pass
-    version_data['system_type'] = system_type
-
-
-    # Get boot type, it can be livecd, installed image, or, possible, a system installed
-    # via legacy "install system" mechanism
-    # In installed images, the squashfs image file is named after its image version,
-    # while on livecd it's just "filesystem.squashfs", that's how we tell a livecd boot
-    # from an installed image
-    boot_via = "installed image"
-    if subprocess.call(""" grep -e '^overlay.*/filesystem.squashfs' /proc/mounts >/dev/null""", shell=True) == 0:
-        boot_via = "livecd"
-    elif subprocess.call(""" grep '^overlay /' /proc/mounts >/dev/null """, shell=True) != 0:
-        boot_via = "legacy non-image installation"
-    version_data['boot_via'] = boot_via
-    
-
-    # Get hardware details from DMI
-    version_data['hardware_vendor'] = read_file('/sys/class/dmi/id/sys_vendor')
-    version_data['hardware_model']  = read_file('/sys/class/dmi/id/product_name')
-
-    # These two assume script is run as root, normal users can't access those files
-    version_data['hardware_serial'] = read_file('/sys/class/dmi/id/subsystem/id/product_serial')
-    version_data['hardware_uuid']   = read_file('/sys/class/dmi/id/subsystem/id/product_uuid')
-
-
-    if args.json:
-        print(json.dumps(version_data))
-        sys.exit(0)
-    else:
-        output = pystache.render(version_output_tmpl, version_data).strip()
-        print(output)
-
-        if args.all:
-           print("Package versions:")
-           os.system("dpkg -l")
-
-        if args.funny:
-            print(vyos.limericks.get_random())
diff --git a/src/tests/test_vyos_update_crontab.py b/src/tests/test_vyos_update_crontab.py
index 180871300..7acbbddc5 100644
--- a/src/tests/test_vyos_update_crontab.py
+++ b/src/tests/test_vyos_update_crontab.py
@@ -22,11 +22,11 @@ import unittest
 
 from vyos import ConfigError
 try:
-    from src.conf_mode import vyos_update_crontab
+    from src.conf_mode import task_scheduler
 except ModuleNotFoundError:  # for unittest.main()
     import sys
     sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
-    from src.conf_mode import vyos_update_crontab
+    from src.conf_mode import task_scheduler
 
 
 class TestUpdateCrontab(unittest.TestCase):
@@ -74,9 +74,9 @@ class TestUpdateCrontab(unittest.TestCase):
             with self.subTest(msg=t['name'], tasks=t['tasks'], expected=t['expected']):
                 if t['expected'] is not None:
                     with self.assertRaises(t['expected']):
-                        vyos_update_crontab.verify(t['tasks'])
+                        task_scheduler.verify(t['tasks'])
                 else:
-                    vyos_update_crontab.verify(t['tasks'])
+                    task_scheduler.verify(t['tasks'])
 
     def test_generate(self):
         tests = [
@@ -114,16 +114,16 @@ class TestUpdateCrontab(unittest.TestCase):
         ]
         for t in tests:
             with self.subTest(msg=t['name'], tasks=t['tasks'], expected=t['expected']):
-                vyos_update_crontab.crontab_file = tempfile.mkstemp()[1]
-                vyos_update_crontab.generate(t['tasks'])
+                task_scheduler.crontab_file = tempfile.mkstemp()[1]
+                task_scheduler.generate(t['tasks'])
                 if len(t['expected']) > 0:
-                    self.assertTrue(os.path.isfile(vyos_update_crontab.crontab_file))
-                    with open(vyos_update_crontab.crontab_file) as f:
+                    self.assertTrue(os.path.isfile(task_scheduler.crontab_file))
+                    with open(task_scheduler.crontab_file) as f:
                         actual = f.read()
                         self.assertEqual(t['expected'], actual.splitlines())
-                    os.remove(vyos_update_crontab.crontab_file)
+                    os.remove(task_scheduler.crontab_file)
                 else:
-                    self.assertFalse(os.path.isfile(vyos_update_crontab.crontab_file))
+                    self.assertFalse(os.path.isfile(task_scheduler.crontab_file))
 
 
 if __name__ == "__main__":
-- 
cgit v1.2.3