summaryrefslogtreecommitdiff
path: root/src/conf_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-xsrc/conf_mode/vyos_config_bcast_relay.py124
-rwxr-xr-xsrc/conf_mode/vyos_config_dns_forwarding.py213
-rwxr-xr-xsrc/conf_mode/vyos_config_host_name.py96
-rw-r--r--src/conf_mode/vyos_config_lldp.py218
-rwxr-xr-xsrc/conf_mode/vyos_config_mdns_repeater.py93
-rwxr-xr-xsrc/conf_mode/vyos_config_ntp.py161
-rwxr-xr-xsrc/conf_mode/vyos_config_ssh.py249
-rwxr-xr-xsrc/conf_mode/vyos_update_crontab.py148
8 files changed, 1302 insertions, 0 deletions
diff --git a/src/conf_mode/vyos_config_bcast_relay.py b/src/conf_mode/vyos_config_bcast_relay.py
new file mode 100755
index 000000000..785690d9c
--- /dev/null
+++ b/src/conf_mode/vyos_config_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/vyos_config_dns_forwarding.py b/src/conf_mode/vyos_config_dns_forwarding.py
new file mode 100755
index 000000000..be48cde60
--- /dev/null
+++ b/src/conf_mode/vyos_config_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/vyos_config_host_name.py b/src/conf_mode/vyos_config_host_name.py
new file mode 100755
index 000000000..2a245b211
--- /dev/null
+++ b/src/conf_mode/vyos_config_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/vyos_config_lldp.py b/src/conf_mode/vyos_config_lldp.py
new file mode 100644
index 000000000..ba7e9cb13
--- /dev/null
+++ b/src/conf_mode/vyos_config_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/vyos_config_mdns_repeater.py b/src/conf_mode/vyos_config_mdns_repeater.py
new file mode 100755
index 000000000..e648fd64f
--- /dev/null
+++ b/src/conf_mode/vyos_config_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/vyos_config_ntp.py b/src/conf_mode/vyos_config_ntp.py
new file mode 100755
index 000000000..8be12e44e
--- /dev/null
+++ b/src/conf_mode/vyos_config_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/vyos_config_ssh.py b/src/conf_mode/vyos_config_ssh.py
new file mode 100755
index 000000000..a4857bba9
--- /dev/null
+++ b/src/conf_mode/vyos_config_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/vyos_update_crontab.py b/src/conf_mode/vyos_update_crontab.py
new file mode 100755
index 000000000..c19b88007
--- /dev/null
+++ b/src/conf_mode/vyos_update_crontab.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)