From d4c10902026b76a6372cf738fc98860b29b39759 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 16 May 2018 16:01:38 +0200 Subject: T644: remove prefixing from all scripts and update environment variables with VyOS paths. --- 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 ----------------- 16 files changed, 1302 insertions(+), 1302 deletions(-) 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 (limited to 'src/conf_mode') 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 . +# +# + +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 . +# +# + +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 . +# +# + +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 . +# +# +# + +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 . +# +# + +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 . +# +# + +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 . +# +# + +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 . +# +# + +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 . -# -# - -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 . -# -# - -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 . -# -# - -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 . -# -# -# - -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 . -# -# - -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 . -# -# - -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 . -# -# - -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 . -# -# - -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) -- cgit v1.2.3