diff options
Diffstat (limited to 'src')
70 files changed, 1161 insertions, 1740 deletions
diff --git a/src/completion/list_interfaces.py b/src/completion/list_interfaces.py index 98b32797a..e27281433 100755 --- a/src/completion/list_interfaces.py +++ b/src/completion/list_interfaces.py @@ -2,7 +2,14 @@ import sys import argparse -from vyos.ifconfig import Interface +from vyos.ifconfig import Section + + +def matching(feature): + for section in Section.feature(feature): + for intf in Section.interfaces(section): + yield intf + parser = argparse.ArgumentParser() group = parser.add_mutually_exclusive_group() @@ -13,46 +20,23 @@ group.add_argument("-bo", "--bondable", action="store_true", help="List all bond args = parser.parse_args() -# XXX: Need to be rewritten using the data in the class definition -# XXX: It can be done once vti and input are moved into vyos -# XXX: We store for each class what type they are (broadcast, bridgeabe, ...) - if args.type: try: - interfaces = Interface.listing(args.type) - + interfaces = Section.interfaces(args.type) + print(" ".join(interfaces)) except ValueError as e: print(e, file=sys.stderr) print("") elif args.broadcast: - eth = Interface.listing("ethernet") - bridge = Interface.listing("bridge") - bond = Interface.listing("bonding") - interfaces = eth + bridge + bond + print(" ".join(matching("broadcast"))) elif args.bridgeable: - eth = Interface.listing("ethernet") - bond = Interface.listing("bonding") - l2tpv3 = Interface.listing("l2tpv3") - openvpn = Interface.listing("openvpn") - wireless = Interface.listing("wireless") - tunnel = Interface.listing("tunnel") - vxlan = Interface.listing("vxlan") - geneve = Interface.listing("geneve") - - interfaces = eth + bond + l2tpv3 + openvpn + vxlan + tunnel + wireless + geneve + print(" ".join(matching("bridgeable"))) elif args.bondable: - interfaces = [] - eth = Interface.listing("ethernet") - # we need to filter out VLAN interfaces identified by a dot (.) in their name - for intf in eth: - if not '.' in intf: - interfaces.append(intf) + print(" ".join([intf for intf in matching("bondable") if '.' not in intf])) else: - interfaces = Interface.listing() - -print(" ".join(interfaces)) + print(" ".join(Section.interfaces())) diff --git a/src/completion/list_openvpn_clients.py b/src/completion/list_openvpn_clients.py index 17b0c7008..177ac90c9 100755 --- a/src/completion/list_openvpn_clients.py +++ b/src/completion/list_openvpn_clients.py @@ -18,7 +18,7 @@ import os import sys import argparse -from vyos.ifconfig import Interface +from vyos.ifconfig import Section def get_client_from_interface(interface): clients = [] @@ -50,7 +50,7 @@ if __name__ == "__main__": if args.interface: clients = get_client_from_interface(args.interface) elif args.all: - for interface in Interface.listing("openvpn"): + for interface in Section.interfaces("openvpn"): clients += get_client_from_interface(interface) print(" ".join(clients)) diff --git a/src/conf_mode/accel_l2tp.py b/src/conf_mode/accel_l2tp.py deleted file mode 100755 index 4ca5a858a..000000000 --- a/src/conf_mode/accel_l2tp.py +++ /dev/null @@ -1,397 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2019-2020 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 re -import jinja2 -import socket -import time - -from jinja2 import FileSystemLoader, Environment - -from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir -from vyos import ConfigError -from vyos.util import run - - -pidfile = r'/var/run/accel_l2tp.pid' -l2tp_cnf_dir = r'/etc/accel-ppp/l2tp' -chap_secrets = l2tp_cnf_dir + '/chap-secrets' -l2tp_conf = l2tp_cnf_dir + '/l2tp.config' -# accel-pppd -d -c /etc/accel-ppp/l2tp/l2tp.config -p /var/run/accel_l2tp.pid - -# config path creation -if not os.path.exists(l2tp_cnf_dir): - os.makedirs(l2tp_cnf_dir) - -### -# inline helper functions -### -# depending on hw and threads, daemon needs a little to start -# if it takes longer than 100 * 0.5 secs, exception is being raised -# not sure if that's the best way to check it, but it worked so far quite well -### - - -def chk_con(): - cnt = 0 - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - while True: - try: - s.connect(("127.0.0.1", 2004)) - break - except ConnectionRefusedError: - time.sleep(0.5) - cnt += 1 - if cnt == 100: - raise("failed to start l2tp server") - break - - -def _accel_cmd(command): - return run(f'/usr/bin/accel-cmd -p 2004 {command}') - -### -# inline helper functions end -### - - -def get_config(): - c = Config() - if not c.exists('vpn l2tp remote-access '): - return None - - c.set_level('vpn l2tp remote-access') - config_data = { - 'authentication': { - 'mode': 'local', - 'local-users': { - }, - 'radiussrv': {}, - 'radiusopt': {}, - 'auth_proto': [], - 'mppe': 'prefer' - }, - 'outside_addr': '', - 'gateway_address': '10.255.255.0', - 'dns': [], - 'dnsv6': [], - 'wins': [], - 'client_ip_pool': None, - 'client_ip_subnets': [], - 'client_ipv6_pool': {}, - 'mtu': '1436', - 'ip6_column': '', - 'ip6_dp_column': '', - 'ppp_options': {}, - } - - ### general options ### - - if c.exists('dns-servers server-1'): - config_data['dns'].append(c.return_value('dns-servers server-1')) - if c.exists('dns-servers server-2'): - config_data['dns'].append(c.return_value('dns-servers server-2')) - if c.exists('dnsv6-servers'): - for dns6_server in c.return_values('dnsv6-servers'): - config_data['dnsv6'].append(dns6_server) - if c.exists('wins-servers server-1'): - config_data['wins'].append(c.return_value('wins-servers server-1')) - if c.exists('wins-servers server-2'): - config_data['wins'].append(c.return_value('wins-servers server-2')) - if c.exists('outside-address'): - config_data['outside_addr'] = c.return_value('outside-address') - - # auth local - if c.exists('authentication mode local'): - if c.exists('authentication local-users username'): - for usr in c.list_nodes('authentication local-users username'): - config_data['authentication']['local-users'].update( - { - usr: { - 'passwd': '', - 'state': 'enabled', - 'ip': '*', - 'upload': None, - 'download': None - } - } - ) - - if c.exists('authentication local-users username ' + usr + ' password'): - config_data['authentication']['local-users'][usr]['passwd'] = c.return_value( - 'authentication local-users username ' + usr + ' password') - if c.exists('authentication local-users username ' + usr + ' disable'): - config_data['authentication']['local-users'][usr]['state'] = 'disable' - if c.exists('authentication local-users username ' + usr + ' static-ip'): - config_data['authentication']['local-users'][usr]['ip'] = c.return_value( - 'authentication local-users username ' + usr + ' static-ip') - if c.exists('authentication local-users username ' + usr + ' rate-limit download'): - config_data['authentication']['local-users'][usr]['download'] = c.return_value( - 'authentication local-users username ' + usr + ' rate-limit download') - if c.exists('authentication local-users username ' + usr + ' rate-limit upload'): - config_data['authentication']['local-users'][usr]['upload'] = c.return_value( - 'authentication local-users username ' + usr + ' rate-limit upload') - - # authentication mode radius servers and settings - - if c.exists('authentication mode radius'): - config_data['authentication']['mode'] = 'radius' - rsrvs = c.list_nodes('authentication radius server') - for rsrv in rsrvs: - if c.return_value('authentication radius server ' + rsrv + ' fail-time') == None: - ftime = '0' - else: - ftime = str(c.return_value( - 'authentication radius server ' + rsrv + ' fail-time')) - if c.return_value('authentication radius-server ' + rsrv + ' req-limit') == None: - reql = '0' - else: - reql = str(c.return_value( - 'authentication radius server ' + rsrv + ' req-limit')) - - config_data['authentication']['radiussrv'].update( - { - rsrv: { - 'secret': c.return_value('authentication radius server ' + rsrv + ' key'), - 'fail-time': ftime, - 'req-limit': reql - } - } - ) - # Source ip address feature - if c.exists('authentication radius source-address'): - config_data['authentication']['radius_source_address'] = c.return_value( - 'authentication radius source-address') - - # advanced radius-setting - if c.exists('authentication radius acct-timeout'): - config_data['authentication']['radiusopt']['acct-timeout'] = c.return_value( - 'authentication radius acct-timeout') - if c.exists('authentication radius max-try'): - config_data['authentication']['radiusopt']['max-try'] = c.return_value( - 'authentication radius max-try') - if c.exists('authentication radius timeout'): - config_data['authentication']['radiusopt']['timeout'] = c.return_value( - 'authentication radius timeout') - if c.exists('authentication radius nas-identifier'): - config_data['authentication']['radiusopt']['nas-id'] = c.return_value( - 'authentication radius nas-identifier') - if c.exists('authentication radius dae-server'): - # Set default dae-server port if not defined - if c.exists('authentication radius dae-server port'): - dae_server_port = c.return_value( - 'authentication radius dae-server port') - else: - dae_server_port = "3799" - config_data['authentication']['radiusopt'].update( - { - 'dae-srv': { - 'ip-addr': c.return_value('authentication radius dae-server ip-address'), - 'port': dae_server_port, - 'secret': str(c.return_value('authentication radius dae-server secret')) - } - } - ) - # filter-id is the internal accel default if attribute is empty - # set here as default for visibility which may change in the future - if c.exists('authentication radius rate-limit enable'): - if not c.exists('authentication radius rate-limit attribute'): - config_data['authentication']['radiusopt']['shaper'] = { - 'attr': 'Filter-Id' - } - else: - config_data['authentication']['radiusopt']['shaper'] = { - 'attr': c.return_value('authentication radius rate-limit attribute') - } - if c.exists('authentication radius rate-limit vendor'): - config_data['authentication']['radiusopt']['shaper']['vendor'] = c.return_value( - 'authentication radius rate-limit vendor') - - if c.exists('client-ip-pool'): - if c.exists('client-ip-pool start') and c.exists('client-ip-pool stop'): - config_data['client_ip_pool'] = c.return_value( - 'client-ip-pool start') + '-' + re.search('[0-9]+$', c.return_value('client-ip-pool stop')).group(0) - - if c.exists('client-ip-pool subnet'): - config_data['client_ip_subnets'] = c.return_values( - 'client-ip-pool subnet') - - if c.exists('client-ipv6-pool prefix'): - config_data['client_ipv6_pool']['prefix'] = c.return_values( - 'client-ipv6-pool prefix') - config_data['ip6_column'] = 'ip6,' - if c.exists('client-ipv6-pool delegate-prefix'): - config_data['client_ipv6_pool']['delegate_prefix'] = c.return_values( - 'client-ipv6-pool delegate-prefix') - config_data['ip6_dp_column'] = 'ip6-dp,' - - if c.exists('mtu'): - config_data['mtu'] = c.return_value('mtu') - - # gateway address - if c.exists('gateway-address'): - config_data['gateway_address'] = c.return_value('gateway-address') - else: - # calculate gw-ip-address - if c.exists('client-ip-pool start'): - # use start ip as gw-ip-address - config_data['gateway_address'] = c.return_value( - 'client-ip-pool start') - elif c.exists('client-ip-pool subnet'): - # use first ip address from first defined pool - lst_ip = re.findall("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}", c.return_values( - 'client-ip-pool subnet')[0]) - config_data['gateway_address'] = lst_ip[0] - - if c.exists('authentication require'): - auth_mods = {'pap': 'pap', 'chap': 'auth_chap_md5', - 'mschap': 'auth_mschap_v1', 'mschap-v2': 'auth_mschap_v2'} - for proto in c.return_values('authentication require'): - config_data['authentication']['auth_proto'].append( - auth_mods[proto]) - else: - config_data['authentication']['auth_proto'] = ['auth_mschap_v2'] - - if c.exists('authentication mppe'): - config_data['authentication']['mppe'] = c.return_value( - 'authentication mppe') - - if c.exists('idle'): - config_data['idle_timeout'] = c.return_value('idle') - - # LNS secret - if c.exists('lns shared-secret'): - config_data['lns_shared_secret'] = c.return_value('lns shared-secret') - - if c.exists('ccp-disable'): - config_data['ccp_disable'] = True - - # ppp_options - ppp_options = {} - if c.exists('ppp-options'): - if c.exists('ppp-options lcp-echo-failure'): - ppp_options['lcp-echo-failure'] = c.return_value( - 'ppp-options lcp-echo-failure') - if c.exists('ppp-options lcp-echo-interval'): - ppp_options['lcp-echo-interval'] = c.return_value( - 'ppp-options lcp-echo-interval') - - if len(ppp_options) != 0: - config_data['ppp_options'] = ppp_options - - return config_data - - -def verify(c): - if c == None: - return None - - if c['authentication']['mode'] == 'local': - if not c['authentication']['local-users']: - raise ConfigError( - 'l2tp-server authentication local-users required') - for usr in c['authentication']['local-users']: - if not c['authentication']['local-users'][usr]['passwd']: - raise ConfigError('user ' + usr + ' requires a password') - - if c['authentication']['mode'] == 'radius': - if len(c['authentication']['radiussrv']) == 0: - raise ConfigError('radius server required') - for rsrv in c['authentication']['radiussrv']: - if c['authentication']['radiussrv'][rsrv]['secret'] == None: - raise ConfigError('radius server ' + rsrv + - ' needs a secret configured') - - # check for the existence of a client ip pool - if not c['client_ip_pool'] and not c['client_ip_subnets']: - raise ConfigError( - "set vpn l2tp remote-access client-ip-pool requires subnet or start/stop IP pool") - - # check ipv6 - if 'delegate_prefix' in c['client_ipv6_pool'] and not 'prefix' in c['client_ipv6_pool']: - raise ConfigError( - "\"set vpn l2tp remote-access client-ipv6-pool prefix\" required for delegate-prefix ") - - if len(c['dnsv6']) > 3: - raise ConfigError("Maximum allowed dnsv6-servers addresses is 3") - - -def generate(c): - if c == None: - return None - - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'l2tp') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader, trim_blocks=True) - - # accel-cmd reload doesn't work so any change results in a restart of the daemon - try: - if os.cpu_count() == 1: - c['thread_cnt'] = 1 - else: - c['thread_cnt'] = int(os.cpu_count()/2) - except KeyError: - if os.cpu_count() == 1: - c['thread_cnt'] = 1 - else: - c['thread_cnt'] = int(os.cpu_count()/2) - - tmpl = env.get_template('l2tp.config.tmpl') - config_text = tmpl.render(c) - open(l2tp_conf, 'w').write(config_text) - - if c['authentication']['local-users']: - tmpl = env.get_template('chap-secrets.tmpl') - chap_secrets_txt = tmpl.render(c) - old_umask = os.umask(0o077) - open(chap_secrets, 'w').write(chap_secrets_txt) - os.umask(old_umask) - - return c - - -def apply(c): - if c == None: - if os.path.exists(pidfile): - _accel_cmd('shutdown hard') - if os.path.exists(pidfile): - os.remove(pidfile) - return None - - if not os.path.exists(pidfile): - ret = run(f'/usr/sbin/accel-pppd -c {l2tp_conf} -p {pidfile} -d') - chk_con() - if ret != 0 and os.path.exists(pidfile): - os.remove(pidfile) - raise ConfigError('accel-pppd failed to start') - else: - # if gw ip changes, only restart doesn't work - _accel_cmd('restart') - - -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/bcast_relay.py b/src/conf_mode/bcast_relay.py index 8d4c4a89a..a3bc76ef8 100755 --- a/src/conf_mode/bcast_relay.py +++ b/src/conf_mode/bcast_relay.py @@ -19,12 +19,11 @@ import fnmatch from sys import exit from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError from vyos.util import call +from vyos.template import render config_file = r'/etc/default/udp-broadcast-relay' @@ -112,11 +111,6 @@ def generate(relay): if relay is None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'bcast-relay') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - config_dir = os.path.dirname(config_file) config_filename = os.path.basename(config_file) active_configs = [] @@ -146,16 +140,13 @@ def generate(relay): # configuration filename contains instance id file = config_file + str(r['id']) - tmpl = env.get_template('udp-broadcast-relay.tmpl') - config_text = tmpl.render(r) - with open(file, 'w') as f: - f.write(config_text) + render(file, 'bcast-relay/udp-broadcast-relay.tmpl', r) return None def apply(relay): # first stop all running services - call('sudo systemctl stop udp-broadcast-relay@{1..99}') + call('systemctl stop udp-broadcast-relay@{1..99}.service') if (relay is None) or relay['disabled']: return None @@ -165,7 +156,7 @@ def apply(relay): # Don't start individual instance when it's disabled if r['disabled']: continue - call('sudo systemctl start udp-broadcast-relay@{0}'.format(r['id'])) + call('systemctl start udp-broadcast-relay@{0}.service'.format(r['id'])) return None diff --git a/src/conf_mode/dhcp_relay.py b/src/conf_mode/dhcp_relay.py index c92d6a4e1..ce0e01308 100755 --- a/src/conf_mode/dhcp_relay.py +++ b/src/conf_mode/dhcp_relay.py @@ -16,15 +16,14 @@ import os -from jinja2 import FileSystemLoader, Environment from sys import exit from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir -from vyos import ConfigError +from vyos.template import render from vyos.util import call +from vyos import ConfigError -config_file = r'/etc/default/isc-dhcp-relay' +config_file = r'/run/dhcp-relay/dhcp.conf' default_config_data = { 'interface': [], @@ -96,28 +95,25 @@ def verify(relay): def generate(relay): # bail out early - looks like removal from running config - if relay is None: + if not relay: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'dhcp-relay') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - tmpl = env.get_template('config.tmpl') - config_text = tmpl.render(relay) - with open(config_file, 'w') as f: - f.write(config_text) + # Create configuration directory on demand + dirname = os.path.dirname(config_file) + if not os.path.isdir(dirname): + os.mkdir(dirname) + render(config_file, 'dhcp-relay/config.tmpl', relay) return None def apply(relay): - if relay is not None: - call('sudo systemctl restart isc-dhcp-relay.service') + if relay: + call('systemctl restart isc-dhcp-relay.service') else: # DHCP relay support is removed in the commit - call('sudo systemctl stop isc-dhcp-relay.service') - os.unlink(config_file) + call('systemctl stop isc-dhcp-relay.service') + if os.path.exists(config_file): + os.unlink(config_file) return None diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index 553247b88..da01f16eb 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -17,25 +17,19 @@ import os from ipaddress import ip_address, ip_network -from jinja2 import FileSystemLoader, Environment from socket import inet_ntoa from struct import pack from sys import exit from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos.validate import is_subnet_connected from vyos import ConfigError -from vyos.util import call +from vyos.template import render +from vyos.util import call, chown - -config_file = r'/etc/dhcp/dhcpd.conf' -lease_file = r'/config/dhcpd.leases' -pid_file = r'/var/run/dhcpd.pid' -daemon_config_file = r'/etc/default/isc-dhcpv4-server' +config_file = r'/run/dhcp-server/dhcpd.conf' default_config_data = { - 'lease_file': lease_file, 'disabled': False, 'ddns_enable': False, 'global_parameters': [], @@ -451,7 +445,7 @@ def get_config(): return dhcp def verify(dhcp): - if (dhcp is None) or (dhcp['disabled'] is True): + if not dhcp or dhcp['disabled']: return None # If DHCP is enabled we need one share-network @@ -597,49 +591,29 @@ def verify(dhcp): return None def generate(dhcp): - if dhcp is None: - return None - - if dhcp['disabled'] is True: - print('Warning: DHCP server will be deactivated because it is disabled') + if not dhcp or dhcp['disabled']: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'dhcp-server') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) + # Create configuration directory on demand + dirname = os.path.dirname(config_file) + if not os.path.isdir(dirname): + os.mkdir(dirname) - tmpl = env.get_template('dhcpd.conf.tmpl') - config_text = tmpl.render(dhcp) # Please see: https://phabricator.vyos.net/T1129 for quoting of the raw parameters # we can pass to ISC DHCPd - config_text = config_text.replace(""",'"') - - with open(config_file, 'w') as f: - f.write(config_text) - - tmpl = env.get_template('daemon.tmpl') - config_text = tmpl.render(dhcp) - with open(daemon_config_file, 'w') as f: - f.write(config_text) - + render(config_file, 'dhcp-server/dhcpd.conf.tmpl', dhcp, + formater=lambda _: _.replace(""", '"')) return None def apply(dhcp): - if (dhcp is None) or dhcp['disabled']: + if not dhcp or dhcp['disabled']: # DHCP server is removed in the commit - call('sudo systemctl stop isc-dhcpv4-server.service') + call('systemctl stop isc-dhcp-server.service') if os.path.exists(config_file): os.unlink(config_file) - if os.path.exists(daemon_config_file): - os.unlink(daemon_config_file) - else: - # If our file holding DHCP leases does yet not exist - create it - if not os.path.exists(lease_file): - os.mknod(lease_file) - - call('sudo systemctl restart isc-dhcpv4-server.service') + return None + call('systemctl restart isc-dhcp-server.service') return None if __name__ == '__main__': diff --git a/src/conf_mode/dhcpv6_relay.py b/src/conf_mode/dhcpv6_relay.py index 9355d9794..cb5a4bbfb 100755 --- a/src/conf_mode/dhcpv6_relay.py +++ b/src/conf_mode/dhcpv6_relay.py @@ -18,15 +18,13 @@ import os from sys import exit from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError from vyos.util import call +from vyos.template import render - -config_file = r'/etc/default/isc-dhcpv6-relay' +config_file = r'/run/dhcp-relay/dhcpv6.conf' default_config_data = { 'listen_addr': [], @@ -86,25 +84,22 @@ def generate(relay): if relay is None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'dhcpv6-relay') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - tmpl = env.get_template('config.tmpl') - config_text = tmpl.render(relay) - with open(config_file, 'w') as f: - f.write(config_text) + # Create configuration directory on demand + dirname = os.path.dirname(config_file) + if not os.path.isdir(dirname): + os.mkdir(dirname) + render(config_file, 'dhcpv6-relay/config.tmpl', relay) return None def apply(relay): if relay is not None: - call('sudo systemctl restart isc-dhcpv6-relay.service') + call('systemctl restart isc-dhcp-relay6.service') else: # DHCPv6 relay support is removed in the commit - call('sudo systemctl stop isc-dhcpv6-relay.service') - os.unlink(config_file) + call('systemctl stop isc-dhcp-relay6.service') + if os.path.exists(config_file): + os.unlink(config_file) return None diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py index 950ca1ce2..94a307826 100755 --- a/src/conf_mode/dhcpv6_server.py +++ b/src/conf_mode/dhcpv6_server.py @@ -19,22 +19,16 @@ import ipaddress from sys import exit from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir +from vyos.template import render +from vyos.util import call from vyos.validate import is_subnet_connected from vyos import ConfigError -from vyos.util import call - -config_file = r'/etc/dhcp/dhcpdv6.conf' -lease_file = r'/config/dhcpdv6.leases' -pid_file = r'/var/run/dhcpdv6.pid' -daemon_config_file = r'/etc/default/isc-dhcpv6-server' +config_file = r'/run/dhcp-server/dhcpdv6.conf' default_config_data = { - 'lease_file': lease_file, 'preference': '', 'disabled': False, 'shared_network': [] @@ -222,10 +216,7 @@ def get_config(): return dhcpv6 def verify(dhcpv6): - if dhcpv6 is None: - return None - - if dhcpv6['disabled']: + if not dhcpv6 or dhcpv6['disabled']: return None # If DHCP is enabled we need one share-network @@ -337,44 +328,25 @@ def verify(dhcpv6): return None def generate(dhcpv6): - if dhcpv6 is None: + if not dhcpv6 or dhcpv6['disabled']: return None - if dhcpv6['disabled']: - print('Warning: DHCPv6 server will be deactivated because it is disabled') - return None - - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'dhcpv6-server') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - tmpl = env.get_template('dhcpdv6.conf.tmpl') - config_text = tmpl.render(dhcpv6) - with open(config_file, 'w') as f: - f.write(config_text) - - tmpl = env.get_template('daemon.tmpl') - config_text = tmpl.render(dhcpv6) - with open(daemon_config_file, 'w') as f: - f.write(config_text) + # Create configuration directory on demand + dirname = os.path.dirname(config_file) + if not os.path.isdir(dirname): + os.mkdir(dirname) + render(config_file, 'dhcpv6-server/dhcpdv6.conf.tmpl', dhcpv6) return None def apply(dhcpv6): - if (dhcpv6 is None) or dhcpv6['disabled']: + if not dhcpv6 or dhcpv6['disabled']: # DHCP server is removed in the commit - call('sudo systemctl stop isc-dhcpv6-server.service') + call('systemctl stop isc-dhcp-server6.service') if os.path.exists(config_file): os.unlink(config_file) - if os.path.exists(daemon_config_file): - os.unlink(daemon_config_file) - else: - # If our file holding DHCPv6 leases does yet not exist - create it - if not os.path.exists(lease_file): - os.mknod(lease_file) - call('sudo systemctl restart isc-dhcpv6-server.service') + call('systemctl restart isc-dhcp-server6.service') return None diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index 4071c05c9..567dfa4b3 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -19,20 +19,19 @@ import argparse from sys import exit from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos.hostsd_client import Client as hostsd_client from vyos.util import wait_for_commit_lock from vyos import ConfigError from vyos.util import call +from vyos.template import render parser = argparse.ArgumentParser() parser.add_argument("--dhclient", action="store_true", help="Started from dhclient-script") -config_file = r'/etc/powerdns/recursor.conf' +config_file = r'/run/powerdns/recursor.conf' default_config_data = { 'allow_from': [], @@ -153,25 +152,21 @@ def generate(dns): if dns is None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'dns-forwarding') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader, trim_blocks=True) + dirname = os.path.dirname(config_file) + if not os.path.exists(dirname): + os.mkdir(dirname) - tmpl = env.get_template('recursor.conf.tmpl') - config_text = tmpl.render(dns) - with open(config_file, 'w') as f: - f.write(config_text) + render(config_file, 'dns-forwarding/recursor.conf.tmpl', dns, trim_blocks=True) return None def apply(dns): if dns is None: # DNS forwarding is removed in the commit - call("systemctl stop pdns-recursor") + call("systemctl stop pdns-recursor.service") if os.path.isfile(config_file): os.unlink(config_file) else: - call("systemctl restart pdns-recursor") + call("systemctl restart pdns-recursor.service") if __name__ == '__main__': args = parser.parse_args() diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py index b54d76b06..038f77cf9 100755 --- a/src/conf_mode/dynamic_dns.py +++ b/src/conf_mode/dynamic_dns.py @@ -18,18 +18,14 @@ import os from sys import exit from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from stat import S_IRUSR, S_IWUSR from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError from vyos.util import call +from vyos.template import render - -config_file = r'/etc/ddclient/ddclient.conf' -cache_file = r'/var/cache/ddclient/ddclient.cache' -pid_file = r'/var/run/ddclient/ddclient.pid' +config_file = r'/run/ddclient/ddclient.conf' # Mapping of service name to service protocol default_service_protocol = { @@ -48,9 +44,7 @@ default_service_protocol = { default_config_data = { 'interfaces': [], - 'cache_file': cache_file, - 'deleted': False, - 'pid_file': pid_file + 'deleted': False } def get_config(): @@ -221,28 +215,13 @@ def verify(dyndns): def generate(dyndns): # bail out early - looks like removal from running config if dyndns['deleted']: - if os.path.exists(config_file): - os.unlink(config_file) - return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'dynamic-dns') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - dirname = os.path.dirname(dyndns['pid_file']) - if not os.path.exists(dirname): - os.mkdir(dirname) - dirname = os.path.dirname(config_file) if not os.path.exists(dirname): os.mkdir(dirname) - tmpl = env.get_template('ddclient.conf.tmpl') - config_text = tmpl.render(dyndns) - with open(config_file, 'w') as f: - f.write(config_text) + render(config_file, 'dynamic-dns/ddclient.conf.tmpl', dyndns) # Config file must be accessible only by its owner os.chmod(config_file, S_IRUSR | S_IWUSR) @@ -250,18 +229,13 @@ def generate(dyndns): return None def apply(dyndns): - if os.path.exists(dyndns['cache_file']): - os.unlink(dyndns['cache_file']) - - if os.path.exists('/etc/ddclient.conf'): - os.unlink('/etc/ddclient.conf') - if dyndns['deleted']: - call('/etc/init.d/ddclient stop') - if os.path.exists(dyndns['pid_file']): - os.unlink(dyndns['pid_file']) + call('systemctl stop ddclient.service') + if os.path.exists(config_file): + os.unlink(config_file) + else: - call('/etc/init.d/ddclient restart') + call('systemctl restart ddclient.service') return None diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py index 1008f3fae..1354488ac 100755 --- a/src/conf_mode/flow_accounting_conf.py +++ b/src/conf_mode/flow_accounting_conf.py @@ -16,17 +16,18 @@ import os import re +from sys import exit import ipaddress from ipaddress import ip_address from jinja2 import FileSystemLoader, Environment -from sys import exit +from vyos.ifconfig import Section from vyos.ifconfig import Interface from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError from vyos.util import cmd +from vyos.render import render # default values @@ -60,7 +61,7 @@ def _sflow_default_agentip(config): return config.return_value('protocols ospfv3 parameters router-id') # if router-id was not found, use first available ip of any interface - for iface in Interface.listing(): + for iface in Section.interfaces(): for address in Interface(iface).get_addr(): # return an IP, if this is not loopback regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$') @@ -82,7 +83,7 @@ def _iptables_get_nflog(): for iptables_variant in ['iptables', 'ip6tables']: # run iptables, save output and split it by lines iptables_command = "sudo {0} -t {1} -S {2}".format(iptables_variant, iptables_nflog_table, iptables_nflog_chain) - cmd(iptables_command, universal_newlines=True, message='Failed to get flows list') + cmd(iptables_command, message='Failed to get flows list') iptables_out = stdout.splitlines() # parse each line and add information to list @@ -234,7 +235,7 @@ def verify(config): # check that all configured interfaces exists in the system for iface in config['interfaces']: - if not iface in Interface.listing(): + if not iface in Section.interfaces(): # chnged from error to warning to allow adding dynamic interfaces and interface templates # raise ConfigError("The {} interface is not presented in the system".format(iface)) print("Warning: the {} interface is not presented in the system".format(iface)) @@ -262,7 +263,7 @@ def verify(config): # check if configured sFlow agent-id exist in the system agent_id_presented = None - for iface in Interface.listing(): + for iface in Section.interfaces(): for address in Interface(iface).get_addr(): # check an IP, if this is not loopback regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$') @@ -334,16 +335,10 @@ def generate(config): timeout_string = "{}:{}={}".format(timeout_string, timeout_type, timeout_value) config['netflow']['timeout_string'] = timeout_string - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'netflow') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - # Generate daemon configs - tmpl = env.get_template('uacctd.conf.tmpl') - config_text = tmpl.render(templatecfg = config, snaplen = default_captured_packet_size) - with open(uacctd_conf_path, 'w') as file: - file.write(config_text) + render(uacctd_conf_path, 'netflow/uacctd.conf.tmpl', { + 'templatecfg': config, + 'snaplen': default_captured_packet_size, + }) def apply(config): @@ -351,9 +346,9 @@ def apply(config): command = None # Check if flow-accounting was removed and define command if not config['flow-accounting-configured']: - command = '/usr/bin/sudo /bin/systemctl stop uacctd' + command = 'systemctl stop uacctd.service' else: - command = '/usr/bin/sudo /bin/systemctl restart uacctd' + command = 'systemctl restart uacctd.service' # run command to start or stop flow-accounting cmd(command, raising=ConfigError, message='Failed to start/stop flow-accounting') diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index 7c2f79abc..dd5819f9f 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -173,7 +173,7 @@ def apply(config): # restart pdns if it is used ret = run('/usr/bin/rec_control ping') if ret == 0: - call('/etc/init.d/pdns-recursor restart >/dev/null') + call('systemctl restart pdns-recursor.service') return None diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index da7193c9b..7d3a1b9cb 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -18,15 +18,14 @@ import os from sys import exit from copy import deepcopy -from jinja2 import FileSystemLoader, Environment import vyos.defaults import vyos.certbot_util from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError from vyos.util import call +from vyos.template import render config_file = '/etc/nginx/sites-available/default' @@ -133,18 +132,10 @@ def generate(https): if https is None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'https') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader, trim_blocks=True) - if 'server_block_list' not in https or not https['server_block_list']: https['server_block_list'] = [default_server_block] - tmpl = env.get_template('nginx.default.tmpl') - config_text = tmpl.render(https) - with open(config_file, 'w') as f: - f.write(config_text) + render(config_file, 'https/nginx.default.tmpl', https, trim_blocks=True) return None diff --git a/src/conf_mode/igmp_proxy.py b/src/conf_mode/igmp_proxy.py index 77e2bb150..9fa591a2c 100755 --- a/src/conf_mode/igmp_proxy.py +++ b/src/conf_mode/igmp_proxy.py @@ -18,13 +18,12 @@ import os from sys import exit from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from netifaces import interfaces from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError from vyos.util import call +from vyos.template import render config_file = r'/etc/igmpproxy.conf' @@ -116,16 +115,7 @@ def generate(igmp_proxy): print('Warning: IGMP Proxy will be deactivated because it is disabled') return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'igmp-proxy') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - tmpl = env.get_template('igmpproxy.conf.tmpl') - config_text = tmpl.render(igmp_proxy) - with open(config_file, 'w') as f: - f.write(config_text) - + render(config_file, 'igmp-proxy/igmpproxy.conf.tmpl', igmp_proxy) return None def apply(igmp_proxy): diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 8a615ec62..b42765586 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -17,23 +17,20 @@ import os import re -from jinja2 import FileSystemLoader, Environment from copy import deepcopy -from sys import exit -from stat import S_IRUSR,S_IRWXU,S_IRGRP,S_IXGRP,S_IROTH,S_IXOTH -from grp import getgrnam -from ipaddress import ip_address,ip_network,IPv4Interface +from sys import exit,stderr +from ipaddress import IPv4Address,IPv4Network,summarize_address_range from netifaces import interfaces -from pwd import getpwnam from time import sleep from shutil import rmtree from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos.ifconfig import VTunIf -from vyos.util import process_running, cmd, is_bridge_member +from vyos.util import call, is_bridge_member, chown, chmod_600, chmod_755 from vyos.validate import is_addr_assigned from vyos import ConfigError +from vyos.template import render + user = 'openvpn' group = 'openvpn' @@ -75,10 +72,14 @@ default_config_data = { 'server_domain': '', 'server_max_conn': '', 'server_dns_nameserver': [], + 'server_pool': False, + 'server_pool_start': '', + 'server_pool_stop': '', + 'server_pool_netmask': '', 'server_push_route': [], 'server_reject_unconfigured': False, 'server_subnet': '', - 'server_topology': '', + 'server_topology': 'net30', 'shared_secret_file': '', 'tls': False, 'tls_auth': '', @@ -97,32 +98,9 @@ default_config_data = { def get_config_name(intf): - cfg_file = r'/opt/vyatta/etc/openvpn/openvpn-{}.conf'.format(intf) + cfg_file = f'/run/openvpn/{intf}.conf' return cfg_file -def openvpn_mkdir(directory): - # create directory on demand - if not os.path.exists(directory): - os.mkdir(directory) - - # fix permissions - corresponds to mode 755 - os.chmod(directory, S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH) - uid = getpwnam(user).pw_uid - gid = getgrnam(group).gr_gid - os.chown(directory, uid, gid) - -def fixup_permission(filename, permission=S_IRUSR): - """ - Check if the given file exists and change ownershit to root/vyattacfg - and appripriate file access permissions - default is user and group readable - """ - if os.path.isfile(filename): - os.chmod(filename, permission) - - # make file owned by root / vyattacfg - uid = getpwnam('root').pw_uid - gid = getgrnam('vyattacfg').gr_gid - os.chown(filename, uid, gid) def checkCertHeader(header, filename): """ @@ -139,6 +117,66 @@ def checkCertHeader(header, filename): return False +def getDefaultServer(network, topology, devtype): + """ + Gets the default server parameters for a "server" directive. + Currently only IPv4 routed but may be extended to support bridged and/or IPv6 in the future. + Logic from openvpn's src/openvpn/helper.c. + Returns a dict with addresses or False if the input parameters were incorrect. + """ + if not (topology and devtype): + return False + + if not (devtype == 'tun' or devtype == 'tap'): + return False + + if not network.prefixlen: + return False + elif (devtype == 'tun' and network.prefixlen > 29) or (devtype == 'tap' and network.prefixlen > 30): + return False + + server = { + 'local': '', + 'remote_netmask': '', + 'client_remote_netmask': '', + 'pool_start': '', + 'pool_stop': '', + 'pool_netmask': '' + } + + if devtype == 'tun': + if topology == 'net30' or topology == 'point-to-point': + server['local'] = network[1] + server['remote_netmask'] = network[2] + server['client_remote_netmask'] = server['local'] + + # pool start is 4th host IP in subnet (.4 in a /24) + server['pool_start'] = network[4] + + if network.prefixlen == 29: + server['pool_stop'] = network.broadcast_address + else: + # pool end is -4 from the broadcast address (.251 in a /24) + server['pool_stop'] = network[-5] + + elif topology == 'subnet': + server['local'] = network[1] + server['remote_netmask'] = str(network.netmask) + server['client_remote_netmask'] = server['remote_netmask'] + server['pool_start'] = network[2] + server['pool_stop'] = network[-3] + server['pool_netmask'] = server['remote_netmask'] + + elif devtype == 'tap': + server['local'] = network[1] + server['remote_netmask'] = str(network.netmask) + server['client_remote_netmask'] = server['remote_netmask'] + server['pool_start'] = network[2] + server['pool_stop'] = network[-2] + server['pool_netmask'] = server['remote_netmask'] + + return server + def get_config(): openvpn = deepcopy(default_config_data) conf = Config() @@ -308,10 +346,10 @@ def get_config(): # Server-mode subnet (from which client IPs are allocated) if conf.exists('server subnet'): - network = conf.return_value('server subnet') - tmp = IPv4Interface(network).with_netmask + # server_network is used later in this function + server_network = IPv4Network(conf.return_value('server subnet')) # convert the network in format: "192.0.2.0 255.255.255.0" for later use in template - openvpn['server_subnet'] = tmp.replace(r'/', ' ') + openvpn['server_subnet'] = server_network.with_netmask.replace(r'/', ' ') # Client-specific settings for client in conf.list_nodes('server client'): @@ -326,19 +364,6 @@ def get_config(): 'remote_netmask': '' } - # note: with "topology subnet", this is "<ip> <netmask>". - # with "topology p2p", this is "<ip> <our_ip>". - if openvpn['server_topology'] == 'subnet': - # we are only interested in the netmask portion of server_subnet - data['remote_netmask'] = openvpn['server_subnet'].split(' ')[1] - else: - # we need the server subnet in format 192.0.2.0/255.255.255.0 - subnet = openvpn['server_subnet'].replace(' ', r'/') - # get iterator over the usable hosts in the network - tmp = ip_network(subnet).hosts() - # OpenVPN always uses the subnets first available IP address - data['remote_netmask'] = list(tmp)[0] - # Option to disable client connection if conf.exists('disable'): data['disable'] = True @@ -349,13 +374,11 @@ def get_config(): # Route to be pushed to the client for network in conf.return_values('push-route'): - tmp = IPv4Interface(network).with_netmask - data['push_route'].append(tmp.replace(r'/', ' ')) + data['push_route'].append(IPv4Network(network).with_netmask.replace(r'/', ' ')) # Subnet belonging to the client for network in conf.return_values('subnet'): - tmp = IPv4Interface(network).with_netmask - data['subnet'].append(tmp.replace(r'/', ' ')) + data['subnet'].append(IPv4Network(network).with_netmask.replace(r'/', ' ')) # Append to global client list openvpn['client'].append(data) @@ -363,6 +386,19 @@ def get_config(): # re-set configuration level conf.set_level('interfaces openvpn ' + openvpn['intf']) + # Server client IP pool + if conf.exists('server client-ip-pool'): + openvpn['server_pool'] = True + + if conf.exists('server client-ip-pool start'): + openvpn['server_pool_start'] = conf.return_value('server client-ip-pool start') + + if conf.exists('server client-ip-pool stop'): + openvpn['server_pool_stop'] = conf.return_value('server client-ip-pool stop') + + if conf.exists('server client-ip-pool netmask'): + openvpn['server_pool_netmask'] = conf.return_value('server client-ip-pool netmask') + # DNS suffix to be pushed to all clients if conf.exists('server domain-name'): openvpn['server_domain'] = conf.return_value('server domain-name') @@ -378,8 +414,7 @@ def get_config(): # Route to be pushed to all clients if conf.exists('server push-route'): for network in conf.return_values('server push-route'): - tmp = IPv4Interface(network).with_netmask - openvpn['server_push_route'].append(tmp.replace(r'/', ' ')) + openvpn['server_push_route'].append(IPv4Network(network).with_netmask.replace(r'/', ' ')) # Reject connections from clients that are not explicitly configured if conf.exists('server reject-unconfigured-clients'): @@ -441,6 +476,26 @@ def get_config(): if not openvpn['tls_dh'] and openvpn['tls_key'] and checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls_key']): openvpn['tls_dh'] = 'none' + # Set defaults where necessary. + # If any of the input parameters are missing or wrong, + # this will return False and no defaults will be set. + default_server = getDefaultServer(server_network, openvpn['server_topology'], openvpn['type']) + if default_server: + # server-bridge doesn't require a pool so don't set defaults for it + if not openvpn['bridge_member']: + openvpn['server_pool'] = True + if not openvpn['server_pool_start']: + openvpn['server_pool_start'] = default_server['pool_start'] + + if not openvpn['server_pool_stop']: + openvpn['server_pool_stop'] = default_server['pool_stop'] + + if not openvpn['server_pool_netmask']: + openvpn['server_pool_netmask'] = default_server['pool_netmask'] + + for client in openvpn['client']: + client['remote_netmask'] = default_server['client_remote_netmask'] + return openvpn def verify(openvpn): @@ -535,10 +590,42 @@ def verify(openvpn): if not openvpn['tls_dh'] and not checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls_key']): raise ConfigError('Must specify "tls dh-file" when not using EC keys in server mode') - if not openvpn['server_subnet']: + if openvpn['server_subnet']: + subnet = IPv4Network(openvpn['server_subnet'].replace(' ', '/')) + + if openvpn['type'] == 'tun' and subnet.prefixlen > 29: + raise ConfigError('Server subnets smaller than /29 with device type "tun" are not supported') + elif openvpn['type'] == 'tap' and subnet.prefixlen > 30: + raise ConfigError('Server subnets smaller than /30 with device type "tap" are not supported') + + for client in openvpn['client']: + if client['ip'] and not IPv4Address(client['ip']) in subnet: + raise ConfigError(f'Client IP "{client["ip"]}" not in server subnet "{subnet}"') + + else: if not openvpn['bridge_member']: raise ConfigError('Must specify "server subnet" or "bridge member interface" in server mode') + + if openvpn['server_pool']: + if not (openvpn['server_pool_start'] and openvpn['server_pool_stop']): + raise ConfigError('Server client-ip-pool requires both start and stop addresses in bridged mode') + else: + v4PoolStart = IPv4Address(openvpn['server_pool_start']) + v4PoolStop = IPv4Address(openvpn['server_pool_stop']) + if v4PoolStart > v4PoolStop: + raise ConfigError(f'Server client-ip-pool start address {v4PoolStart} is larger than stop address {v4PoolStop}') + if (int(v4PoolStop) - int(v4PoolStart) >= 65536): + raise ConfigError(f'Server client-ip-pool is too large [{v4PoolStart} -> {v4PoolStop}], maximum is 65536 addresses.') + + v4PoolNets = list(summarize_address_range(v4PoolStart, v4PoolStop)) + for client in openvpn['client']: + if client['ip']: + for v4PoolNet in v4PoolNets: + if IPv4Address(client['ip']) in v4PoolNet: + print(f'Warning: Client "{client["name"]}" IP {client["ip"]} is in server IP pool, it is not reserved for this client.', + file=stderr) + else: # checks for both client and site-to-site go here if openvpn['server_reject_unconfigured']: @@ -665,143 +752,98 @@ def verify(openvpn): if not openvpn['auth_pass']: raise ConfigError('Password for authentication is missing') - # - # Client - # - subnet = openvpn['server_subnet'].replace(' ', '/') - for client in openvpn['client']: - if client['ip'] and not ip_address(client['ip']) in ip_network(subnet): - raise ConfigError('Client IP "{}" not in server subnet "{}'.format(client['ip'], subnet)) - return None def generate(openvpn): if openvpn['deleted'] or openvpn['disable']: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'openvpn') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - interface = openvpn['intf'] directory = os.path.dirname(get_config_name(interface)) - # we can't know which clients were deleted, remove all client configs - if os.path.isdir(os.path.join(directory, 'ccd', interface)): - rmtree(os.path.join(directory, 'ccd', interface), ignore_errors=True) + # we can't know in advance which clients have been, + # remove all client configs + ccd_dir = os.path.join(directory, 'ccd', interface) + if os.path.isdir(ccd_dir): + rmtree(ccd_dir, ignore_errors=True) # create config directory on demand - openvpn_mkdir(directory) - # create status directory on demand - openvpn_mkdir(directory + '/status') - # create client config dir on demand - openvpn_mkdir(directory + '/ccd') - # crete client config dir per interface on demand - openvpn_mkdir(directory + '/ccd/' + interface) + directories = [] + directories.append(f'{directory}/status') + directories.append(f'{directory}/ccd/{interface}') + for onedir in directories: + if not os.path.exists(onedir): + os.makedirs(onedir, 0o755) + chown(onedir, user, group) # Fix file permissons for keys - fixup_permission(openvpn['shared_secret_file']) - fixup_permission(openvpn['tls_key']) + fix_permissions = [] + fix_permissions.append(openvpn['shared_secret_file']) + fix_permissions.append(openvpn['tls_key']) # Generate User/Password authentication file + user_auth_file = f'/tmp/openvpn-{interface}-pw' if openvpn['auth']: - auth_file = '/tmp/openvpn-{}-pw'.format(interface) - with open(auth_file, 'w') as f: + with open(user_auth_file, 'w') as f: f.write('{}\n{}'.format(openvpn['auth_user'], openvpn['auth_pass'])) - - fixup_permission(auth_file) + # also change permission on auth file + fix_permissions.append(user_auth_file) else: # delete old auth file if present - if os.path.isfile('/tmp/openvpn-{}-pw'.format(interface)): - os.remove('/tmp/openvpn-{}-pw'.format(interface)) - - # get numeric uid/gid - uid = getpwnam(user).pw_uid - gid = getgrnam(group).gr_gid + if os.path.isfile(user_auth_file): + os.remove(user_auth_file) # Generate client specific configuration for client in openvpn['client']: - client_file = directory + '/ccd/' + interface + '/' + client['name'] - tmpl = env.get_template('client.conf.tmpl') - client_text = tmpl.render(client) - with open(client_file, 'w') as f: - f.write(client_text) - os.chown(client_file, uid, gid) - - tmpl = env.get_template('server.conf.tmpl') - config_text = tmpl.render(openvpn) + client_file = os.path.join(ccd_dir, client['name']) + render(client_file, 'openvpn/client.conf.tmpl', client) + chown(client_file, user, group) + # we need to support quoting of raw parameters from OpenVPN CLI # see https://phabricator.vyos.net/T1632 - config_text = config_text.replace(""",'"') - with open(get_config_name(interface), 'w') as f: - f.write(config_text) - os.chown(get_config_name(interface), uid, gid) + render(get_config_name(interface), 'openvpn/server.conf.tmpl', openvpn, + formater=lambda _: _.replace(""", '"')) + chown(get_config_name(interface), user, group) + + # Fixup file permissions + for file in fix_permissions: + chmod_600(file) return None def apply(openvpn): - pidfile = '/var/run/openvpn/{}.pid'.format(openvpn['intf']) - - # Always stop OpenVPN service. We can not send a SIGUSR1 for restart of the - # service as the configuration is not re-read. Stop daemon only if it's - # running - it could have died or killed by someone evil - if process_running(pidfile): - command = 'start-stop-daemon' - command += ' --stop ' - command += ' --quiet' - command += ' --oknodo' - command += ' --pidfile ' + pidfile - cmd(command) - - # cleanup old PID file - if os.path.isfile(pidfile): - os.remove(pidfile) + interface = openvpn['intf'] + call(f'systemctl stop openvpn@{interface}.service') # Do some cleanup when OpenVPN is disabled/deleted if openvpn['deleted'] or openvpn['disable']: # cleanup old configuration file - if os.path.isfile(get_config_name(openvpn['intf'])): - os.remove(get_config_name(openvpn['intf'])) + if os.path.isfile(get_config_name(interface)): + os.remove(get_config_name(interface)) # cleanup client config dir - directory = os.path.dirname(get_config_name(openvpn['intf'])) - if os.path.isdir(os.path.join(directory, 'ccd', openvpn['intf'])): - rmtree(os.path.join(directory, 'ccd', openvpn['intf']), ignore_errors=True) - - # cleanup auth file - if os.path.isfile('/tmp/openvpn-{}-pw'.format(openvpn['intf'])): - os.remove('/tmp/openvpn-{}-pw'.format(openvpn['intf'])) + directory = os.path.dirname(get_config_name(interface)) + ccd_dir = os.path.join(directory, 'ccd', interface) + if os.path.isdir(ccd_dir): + rmtree(ccd_dir, ignore_errors=True) return None # On configuration change we need to wait for the 'old' interface to # vanish from the Kernel, if it is not gone, OpenVPN will report: # ERROR: Cannot ioctl TUNSETIFF vtun10: Device or resource busy (errno=16) - while openvpn['intf'] in interfaces(): + while interface in interfaces(): sleep(0.250) # 250ms # No matching OpenVPN process running - maybe it got killed or none # existed - nevertheless, spawn new OpenVPN process - command = 'start-stop-daemon' - command += ' --start ' - command += ' --quiet' - command += ' --oknodo' - command += ' --pidfile ' + pidfile - command += ' --exec /usr/sbin/openvpn' - # now pass arguments to openvpn binary - command += ' --' - command += ' --daemon openvpn-' + openvpn['intf'] - command += ' --config ' + get_config_name(openvpn['intf']) - - # execute assembled command - cmd(command) + call(f'systemctl start openvpn@{interface}.service') # better late then sorry ... but we can only set interface alias after # OpenVPN has been launched and created the interface cnt = 0 - while openvpn['intf'] not in interfaces(): + while interface not in interfaces(): # If VPN tunnel can't be established because the peer/server isn't # (temporarily) available, the vtun interface never becomes registered # with the kernel, and the commit would hang if there is no bail out @@ -816,7 +858,7 @@ def apply(openvpn): try: # we need to catch the exception if the interface is not up due to # reason stated above - o = VTunIf(openvpn['intf']) + o = VTunIf(interface) # update interface description used e.g. within SNMP o.set_alias(openvpn['description']) # IPv6 address autoconfiguration @@ -834,7 +876,7 @@ def apply(openvpn): # TAP interface needs to be brought up explicitly if openvpn['type'] == 'tap': if not openvpn['disable']: - VTunIf(openvpn['intf']).set_admin_state('up') + VTunIf(interface).set_admin_state('up') return None diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py index 353a5a12c..f942b7d2f 100755 --- a/src/conf_mode/interfaces-pppoe.py +++ b/src/conf_mode/interfaces-pppoe.py @@ -18,14 +18,14 @@ import os from sys import exit from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from netifaces import interfaces from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos.ifconfig import Interface -from vyos.util import chown, chmod_x, cmd +from vyos.util import chown, chmod_755, cmd from vyos import ConfigError +from vyos.template import render + default_config_data = { 'access_concentrator': '', @@ -161,11 +161,6 @@ def verify(pppoe): return None def generate(pppoe): - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir["data"], "templates", "pppoe") - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader, trim_blocks=True) - # set up configuration file path variables where our templates will be # rendered into intf = pppoe['intf'] @@ -195,40 +190,26 @@ def generate(pppoe): else: # Create PPP configuration files - tmpl = env.get_template('peer.tmpl') - config_text = tmpl.render(pppoe) - with open(config_pppoe, 'w') as f: - f.write(config_text) - + render(config_pppoe, 'pppoe/peer.tmpl', + pppoe, trim_blocks=True) # Create script for ip-pre-up.d - tmpl = env.get_template('ip-pre-up.script.tmpl') - config_text = tmpl.render(pppoe) - with open(script_pppoe_pre_up, 'w') as f: - f.write(config_text) - + render(script_pppoe_pre_up, 'pppoe/ip-pre-up.script.tmpl', + pppoe, trim_blocks=True) # Create script for ip-up.d - tmpl = env.get_template('ip-up.script.tmpl') - config_text = tmpl.render(pppoe) - with open(script_pppoe_ip_up, 'w') as f: - f.write(config_text) - + render(script_pppoe_ip_up, 'pppoe/ip-up.script.tmpl', + pppoe, trim_blocks=True) # Create script for ip-down.d - tmpl = env.get_template('ip-down.script.tmpl') - config_text = tmpl.render(pppoe) - with open(script_pppoe_ip_down, 'w') as f: - f.write(config_text) - + render(script_pppoe_ip_down, 'pppoe/ip-down.script.tmpl', + pppoe, trim_blocks=True) # Create script for ipv6-up.d - tmpl = env.get_template('ipv6-up.script.tmpl') - config_text = tmpl.render(pppoe) - with open(script_pppoe_ipv6_up, 'w') as f: - f.write(config_text) + render(script_pppoe_ipv6_up, 'pppoe/ipv6-up.script.tmpl', + pppoe, trim_blocks=True) # make generated script file executable - chmod_x(script_pppoe_pre_up) - chmod_x(script_pppoe_ip_up) - chmod_x(script_pppoe_ip_down) - chmod_x(script_pppoe_ipv6_up) + chmod_755(script_pppoe_pre_up) + chmod_755(script_pppoe_ip_up) + chmod_755(script_pppoe_ip_down) + chmod_755(script_pppoe_ipv6_up) return None diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index 19538da72..c51048aeb 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -584,11 +584,17 @@ def apply(conf): if changes['section'] in 'create' and option in tunnel.options: # it was setup at creation continue + if not options[option]: + # remote can be set to '' and it would generate an invalide command + continue tunnel.set_interface(option, options[option]) # set other interface properties for option in ('alias', 'mtu', 'link_detect', 'multicast', 'allmulticast', 'vrf', 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits'): + if not options[option]: + # should never happen but better safe + continue tunnel.set_interface(option, options[option]) # Configure interface address(es) diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index 07c4537b4..498c24df0 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -19,21 +19,18 @@ from sys import exit from re import findall from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from netifaces import interfaces from netaddr import EUI, mac_unix_expanded from vyos.config import Config from vyos.configdict import list_diff, vlan_to_dict -from vyos.defaults import directories as vyos_data_dir from vyos.ifconfig import WiFiIf from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config -from vyos.util import process_running, chmod_x, chown, run, is_bridge_member +from vyos.util import chown, is_bridge_member, call from vyos import ConfigError +from vyos.template import render -user = 'root' -group = 'vyattacfg' default_config_data = { 'address': [], @@ -115,43 +112,16 @@ default_config_data = { } def get_conf_file(conf_type, intf): - cfg_dir = '/var/run/' + conf_type + cfg_dir = '/run/' + conf_type # create directory on demand if not os.path.exists(cfg_dir): - os.mkdir(cfg_dir) - chmod_x(cfg_dir) - chown(cfg_dir, user, group) + os.makedirs(cfg_dir, 0o755) + chown(cfg_dir, 'root', 'vyattacfg') - cfg_file = cfg_dir + r'/{}.cfg'.format(intf) + cfg_file = cfg_dir + r'/{}.conf'.format(intf) return cfg_file -def get_pid(conf_type, intf): - cfg_dir = '/var/run/' + conf_type - - # create directory on demand - if not os.path.exists(cfg_dir): - os.mkdir(cfg_dir) - chmod_x(cfg_dir) - chown(cfg_dir, user, group) - - cfg_file = cfg_dir + r'/{}.pid'.format(intf) - return cfg_file - - -def get_wpa_suppl_config_name(intf): - cfg_dir = '/var/run/wpa_supplicant' - - # create directory on demand - if not os.path.exists(cfg_dir): - os.mkdir(cfg_dir) - chmod_x(cfg_dir) - chown(cfg_dir, user, group) - - cfg_file = cfg_dir + r'/{}.cfg'.format(intf) - return cfg_file - - def get_config(): wifi = deepcopy(default_config_data) conf = Config() @@ -570,6 +540,9 @@ def verify(wifi): if not wifi['phy']: raise ConfigError('You must specify physical-device') + if not wifi['mode']: + raise ConfigError('You must specify a WiFi mode') + if wifi['op_mode'] == 'ap': c = Config() if not c.exists('system wifi-regulatory-domain'): @@ -627,38 +600,20 @@ def verify(wifi): return None def generate(wifi): - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir["data"], "templates", "wifi") - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) + interface = wifi['intf'] # always stop hostapd service first before reconfiguring it - pidfile = get_pid('hostapd', wifi['intf']) - if process_running(pidfile): - command = 'start-stop-daemon' - command += ' --stop ' - command += ' --quiet' - command += ' --oknodo' - command += ' --pidfile ' + pidfile - run(command) - + call(f'systemctl stop hostapd@{interface}.service') # always stop wpa_supplicant service first before reconfiguring it - pidfile = get_pid('wpa_supplicant', wifi['intf']) - if process_running(pidfile): - command = 'start-stop-daemon' - command += ' --stop ' - command += ' --quiet' - command += ' --oknodo' - command += ' --pidfile ' + pidfile - run(command) + call(f'systemctl stop wpa_supplicant@{interface}.service') # Delete config files if interface is removed if wifi['deleted']: - if os.path.isfile(get_conf_file('hostapd', wifi['intf'])): - os.unlink(get_conf_file('hostapd', wifi['intf'])) + if os.path.isfile(get_conf_file('hostapd', interface)): + os.unlink(get_conf_file('hostapd', interface)) - if os.path.isfile(get_conf_file('wpa_supplicant', wifi['intf'])): - os.unlink(get_conf_file('wpa_supplicant', wifi['intf'])) + if os.path.isfile(get_conf_file('wpa_supplicant', interface)): + os.unlink(get_conf_file('wpa_supplicant', interface)) return None @@ -676,7 +631,7 @@ def generate(wifi): tmp |= 0x020000000000 # we now need to add an offset to our MAC address indicating this # subinterfaces index - tmp += int(findall(r'\d+', wifi['intf'])[0]) + tmp += int(findall(r'\d+', interface)[0]) # convert integer to "real" MAC address representation mac = EUI(hex(tmp).split('x')[-1]) @@ -686,22 +641,19 @@ def generate(wifi): # render appropriate new config files depending on access-point or station mode if wifi['op_mode'] == 'ap': - tmpl = env.get_template('hostapd.conf.tmpl') - config_text = tmpl.render(wifi) - with open(get_conf_file('hostapd', wifi['intf']), 'w') as f: - f.write(config_text) + conf = get_conf_file('hostapd', interface) + render(conf, 'wifi/hostapd.conf.tmpl', wifi) elif wifi['op_mode'] == 'station': - tmpl = env.get_template('wpa_supplicant.conf.tmpl') - config_text = tmpl.render(wifi) - with open(get_conf_file('wpa_supplicant', wifi['intf']), 'w') as f: - f.write(config_text) + conf = get_conf_file('wpa_supplicant', interface) + render(conf, 'wifi/wpa_supplicant.conf.tmpl', wifi) return None def apply(wifi): + interface = wifi['intf'] if wifi['deleted']: - w = WiFiIf(wifi['intf']) + w = WiFiIf(interface) # delete interface w.remove() else: @@ -714,7 +666,7 @@ def apply(wifi): conf['phy'] = wifi['phy'] # Finally create the new interface - w = WiFiIf(wifi['intf'], **conf) + w = WiFiIf(interface, **conf) # assign/remove VRF w.set_vrf(wifi['vrf']) @@ -774,7 +726,7 @@ def apply(wifi): # remove no longer required VLAN interfaces (vif) for vif in wifi['vif_remove']: - e.del_vlan(vif) + w.del_vlan(vif) # create VLAN interfaces (vif) for vif in wifi['vif']: @@ -784,11 +736,11 @@ def apply(wifi): try: # on system bootup the above condition is true but the interface # does not exists, which throws an exception, but that's legal - e.del_vlan(vif['id']) + w.del_vlan(vif['id']) except: pass - vlan = e.add_vlan(vif['id']) + vlan = w.add_vlan(vif['id']) apply_vlan_config(vlan, vif) # Enable/Disable interface - interface is always placed in @@ -799,38 +751,10 @@ def apply(wifi): # Physical interface is now configured. Proceed by starting hostapd or # wpa_supplicant daemon. When type is monitor we can just skip this. if wifi['op_mode'] == 'ap': - command = 'start-stop-daemon' - command += ' --start ' - command += ' --quiet' - command += ' --oknodo' - command += ' --pidfile ' + get_pid('hostapd', wifi['intf']) - command += ' --exec /usr/sbin/hostapd' - # now pass arguments to hostapd binary - command += ' -- ' - command += ' -B' - command += ' -P ' + get_pid('hostapd', wifi['intf']) - command += ' ' + get_conf_file('hostapd', wifi['intf']) - - # execute assembled command - run(command) + call(f'systemctl start hostapd@{interface}.service') elif wifi['op_mode'] == 'station': - command = 'start-stop-daemon' - command += ' --start ' - command += ' --quiet' - command += ' --oknodo' - command += ' --pidfile ' + get_pid('hostapd', wifi['intf']) - command += ' --exec /sbin/wpa_supplicant' - # now pass arguments to hostapd binary - command += ' -- ' - command += ' -s -B -D nl80211' - command += ' -P ' + get_pid('wpa_supplicant', wifi['intf']) - command += ' -i ' + wifi['intf'] - command += ' -c ' + \ - get_conf_file('wpa_supplicant', wifi['intf']) - - # execute assembled command - run(command) + call(f'systemctl start wpa_supplicant@{interface}.service') return None diff --git a/src/conf_mode/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py index c44a993c4..da1855cd9 100755 --- a/src/conf_mode/interfaces-wirelessmodem.py +++ b/src/conf_mode/interfaces-wirelessmodem.py @@ -18,15 +18,17 @@ import os from sys import exit from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from netifaces import interfaces from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir -from vyos.util import chown, chmod_x, is_bridge_member +from vyos.util import chown +from vyos.util import chmod_755 +from vyos.util import is_bridge_member from vyos.util import cmd from vyos.util import call from vyos import ConfigError +from vyos.template import render + default_config_data = { 'address': [], @@ -141,11 +143,6 @@ def verify(wwan): return None def generate(wwan): - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'wwan') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - # set up configuration file path variables where our templates will be # rendered into intf = wwan['intf'] @@ -175,39 +172,20 @@ def generate(wwan): else: # Create PPP configuration files - tmpl = env.get_template('peer.tmpl') - config_text = tmpl.render(wwan) - with open(config_wwan, 'w') as f: - f.write(config_text) - + render(config_wwan, 'wwan/peer.tmpl', wwan) # Create PPP chat script - tmpl = env.get_template('chat.tmpl') - config_text = tmpl.render(wwan) - with open(config_wwan_chat, 'w') as f: - f.write(config_text) - + render(config_wwan_chat, 'wwan/chat.tmpl', wwan) # Create script for ip-pre-up.d - tmpl = env.get_template('ip-pre-up.script.tmpl') - config_text = tmpl.render(wwan) - with open(script_wwan_pre_up, 'w') as f: - f.write(config_text) - + render(script_wwan_pre_up, 'wwan/ip-pre-up.script.tmpl', wwan) # Create script for ip-up.d - tmpl = env.get_template('ip-up.script.tmpl') - config_text = tmpl.render(wwan) - with open(script_wwan_ip_up, 'w') as f: - f.write(config_text) - + render(script_wwan_ip_up, 'wwan/ip-up.script.tmpl', wwan) # Create script for ip-down.d - tmpl = env.get_template('ip-down.script.tmpl') - config_text = tmpl.render(wwan) - with open(script_wwan_ip_down, 'w') as f: - f.write(config_text) + render(script_wwan_ip_down, 'wwan/ip-down.script.tmpl', wwan) # make generated script file executable - chmod_x(script_wwan_pre_up) - chmod_x(script_wwan_ip_up) - chmod_x(script_wwan_ip_down) + chmod_755(script_wwan_pre_up) + chmod_755(script_wwan_ip_up) + chmod_755(script_wwan_ip_down) return None diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py index dc04e9131..4fffa11ee 100755 --- a/src/conf_mode/ipsec-settings.py +++ b/src/conf_mode/ipsec-settings.py @@ -18,13 +18,13 @@ import re import os from time import sleep -from jinja2 import FileSystemLoader, Environment from sys import exit from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError from vyos.util import call +from vyos.template import render + ra_conn_name = "remote-access" charon_conf_file = "/etc/strongswan.d/charon.conf" @@ -147,43 +147,26 @@ def verify(data): raise ConfigError("L2TP VPN configuration error: \"vpn ipsec ipsec-interfaces\" must be specified.") def generate(data): - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'ipsec') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader, trim_blocks=True) - - tmpl = env.get_template('charon.tmpl') - config_text = tmpl.render(data) - with open(charon_conf_file, 'w') as f: - f.write(config_text) + render(charon_conf_file, 'ipsec/charon.tmpl', data, trim_blocks=True) if data["ipsec_l2tp"]: remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_flie) - tmpl = env.get_template('ipsec.secrets.tmpl') - l2pt_ipsec_secrets_txt = tmpl.render(c) old_umask = os.umask(0o077) - with open(ipsec_secrets_flie,'w') as f: - f.write(l2pt_ipsec_secrets_txt) + render(ipsec_secrets_flie, 'ipsec/ipsec.secrets.tmpl', c, trim_blocks=True) os.umask(old_umask) - tmpl = env.get_template('remote-access.tmpl') - ipsec_ra_conn_txt = tmpl.render(c) old_umask = os.umask(0o077) # Create tunnels directory if does not exist if not os.path.exists(ipsec_ra_conn_dir): os.makedirs(ipsec_ra_conn_dir) - with open(ipsec_ra_conn_file,'w') as f: - f.write(ipsec_ra_conn_txt) + render(ipsec_ra_conn_file, 'ipsec/remote-access.tmpl', c, trim_blocks=True) os.umask(old_umask) - - tmpl = env.get_template('ipsec.conf.tmpl') - l2pt_ipsec_conf_txt = tmpl.render(c) old_umask = os.umask(0o077) - with open(ipsec_conf_flie,'a') as f: - f.write(l2pt_ipsec_conf_txt) + render(ipsec_conf_flie, 'ipsec/ipsec.conf.tmpl', c, trim_blocks=True) os.umask(old_umask) else: diff --git a/src/conf_mode/le_cert.py b/src/conf_mode/le_cert.py index 4b365a566..2db31d3fc 100755 --- a/src/conf_mode/le_cert.py +++ b/src/conf_mode/le_cert.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2019-2020 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 @@ -13,8 +13,6 @@ # # 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 @@ -25,7 +23,6 @@ from vyos import ConfigError from vyos.util import cmd from vyos.util import call - vyos_conf_scripts_dir = vyos.defaults.directories['conf_mode'] dependencies = [ @@ -86,17 +83,17 @@ def generate(cert): # certbot will attempt to reload nginx, even with 'certonly'; # start nginx if not active - ret = call('systemctl is-active --quiet nginx.ervice') + ret = call('systemctl is-active --quiet nginx.service') if ret: - call('sudo systemctl start nginx.service') + call('systemctl start nginx.service') request_certbot(cert) def apply(cert): if cert is not None: - call('sudo systemctl restart certbot.timer') + call('systemctl restart certbot.timer') else: - call('sudo systemctl stop certbot.timer') + call('systemctl stop certbot.timer') return None for dep in dependencies: diff --git a/src/conf_mode/lldp.py b/src/conf_mode/lldp.py index ec59c68d0..d128c1fe6 100755 --- a/src/conf_mode/lldp.py +++ b/src/conf_mode/lldp.py @@ -18,15 +18,14 @@ import os import re from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from sys import exit from vyos.config import Config from vyos.validate import is_addr_assigned,is_loopback_addr -from vyos.defaults import directories as vyos_data_dir from vyos.version import get_version_data from vyos import ConfigError from vyos.util import call +from vyos.template import render config_file = "/etc/default/lldpd" @@ -210,11 +209,6 @@ def generate(lldp): if lldp is None: return - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'lldp') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - # generate listen on interfaces for intf in lldp['interface_list']: tmp = '' @@ -226,16 +220,9 @@ def generate(lldp): lldp['options']['listen_on'].append(tmp) # generate /etc/default/lldpd - tmpl = env.get_template('lldpd.tmpl') - config_text = tmpl.render(lldp) - with open(config_file, 'w') as f: - f.write(config_text) - + render(config_file, 'lldp/lldpd.tmpl', lldp) # generate /etc/lldpd.d/01-vyos.conf - tmpl = env.get_template('vyos.conf.tmpl') - config_text = tmpl.render(lldp) - with open(vyos_config_file, 'w') as f: - f.write(config_text) + render(vyos_config_file, 'lldp/vyos.conf.tmpl', lldp) def apply(lldp): diff --git a/src/conf_mode/mdns_repeater.py b/src/conf_mode/mdns_repeater.py index 9230aaf61..a652553f7 100755 --- a/src/conf_mode/mdns_repeater.py +++ b/src/conf_mode/mdns_repeater.py @@ -18,14 +18,12 @@ import os from sys import exit from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from netifaces import ifaddresses, AF_INET from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError from vyos.util import call - +from vyos.template import render config_file = r'/etc/default/mdns-repeater' @@ -82,25 +80,16 @@ def generate(mdns): print('Warning: mDNS repeater will be deactivated because it is disabled') return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'mdns-repeater') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - tmpl = env.get_template('mdns-repeater.tmpl') - config_text = tmpl.render(mdns) - with open(config_file, 'w') as f: - f.write(config_text) - + render(config_file, 'mdns-repeater/mdns-repeater.tmpl', mdns) return None def apply(mdns): if (mdns is None) or mdns['disabled']: - call('sudo systemctl stop mdns-repeater') + call('systemctl stop mdns-repeater.service') if os.path.exists(config_file): os.unlink(config_file) else: - call('sudo systemctl restart mdns-repeater') + call('systemctl restart mdns-repeater.service') return None diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py index 75328dfd7..6d32f7fd6 100755 --- a/src/conf_mode/ntp.py +++ b/src/conf_mode/ntp.py @@ -18,14 +18,12 @@ import os from copy import deepcopy from ipaddress import ip_network -from jinja2 import FileSystemLoader, Environment from sys import exit from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir -from vyos import ConfigError from vyos.util import call - +from vyos.template import render +from vyos import ConfigError config_file = r'/etc/ntp.conf' @@ -100,16 +98,7 @@ def generate(ntp): if ntp is None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'ntp') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - tmpl = env.get_template('ntp.conf.tmpl') - config_text = tmpl.render(ntp) - with open(config_file, 'w') as f: - f.write(config_text) - + render(config_file, 'ntp/ntp.conf.tmpl', ntp) return None def apply(ntp): diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py index cf4db5f54..ed8c3637b 100755 --- a/src/conf_mode/protocols_bfd.py +++ b/src/conf_mode/protocols_bfd.py @@ -18,13 +18,12 @@ import os from sys import exit from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos.validate import is_ipv6_link_local, is_ipv6 from vyos import ConfigError from vyos.util import call +from vyos.template import render config_file = r'/tmp/bfd.frr' @@ -191,16 +190,7 @@ def generate(bfd): if bfd is None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'frr-bfd') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - tmpl = env.get_template('bfd.frr.tmpl') - config_text = tmpl.render(bfd) - with open(config_file, 'w') as f: - f.write(config_text) - + render(config_file, 'frr-bfd/bfd.frr.tmpl', bfd) return None def apply(bfd): diff --git a/src/conf_mode/protocols_igmp.py b/src/conf_mode/protocols_igmp.py index 141b1950d..9b338c5b9 100755 --- a/src/conf_mode/protocols_igmp.py +++ b/src/conf_mode/protocols_igmp.py @@ -17,13 +17,12 @@ import os from ipaddress import IPv4Address -from jinja2 import FileSystemLoader, Environment from sys import exit from vyos import ConfigError from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos.util import call +from vyos.template import render config_file = r'/tmp/igmp.frr' @@ -88,16 +87,7 @@ def generate(igmp): if igmp is None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'igmp') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - tmpl = env.get_template('igmp.frr.tmpl') - config_text = tmpl.render(igmp) - with open(config_file, 'w') as f: - f.write(config_text) - + render(config_file, 'igmp/igmp.frr.tmpl', igmp) return None def apply(igmp): diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py index b5753aea8..0a241277d 100755 --- a/src/conf_mode/protocols_mpls.py +++ b/src/conf_mode/protocols_mpls.py @@ -16,12 +16,10 @@ import os -from jinja2 import FileSystemLoader, Environment - from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError from vyos.util import call +from vyos.template import render config_file = r'/tmp/ldpd.frr' @@ -129,16 +127,7 @@ def generate(mpls): if mpls is None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'mpls') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - tmpl = env.get_template('ldpd.frr.tmpl') - config_text = tmpl.render(mpls) - with open(config_file, 'w') as f: - f.write(config_text) - + render(config_file, 'mpls/ldpd.frr.tmpl', mpls) return None def apply(mpls): diff --git a/src/conf_mode/protocols_pim.py b/src/conf_mode/protocols_pim.py index 44fc9293b..f12de4a72 100755 --- a/src/conf_mode/protocols_pim.py +++ b/src/conf_mode/protocols_pim.py @@ -17,13 +17,12 @@ import os from ipaddress import IPv4Address -from jinja2 import FileSystemLoader, Environment from sys import exit from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError from vyos.util import call +from vyos.template import render config_file = r'/tmp/pimd.frr' @@ -115,16 +114,7 @@ def generate(pim): if pim is None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'pim') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - tmpl = env.get_template('pimd.frr.tmpl') - config_text = tmpl.render(pim) - with open(config_file, 'w') as f: - f.write(config_text) - + render(config_file, 'pim/pimd.frr.tmpl', pim) return None def apply(pim): diff --git a/src/conf_mode/salt-minion.py b/src/conf_mode/salt-minion.py index bfc3a707e..236480854 100755 --- a/src/conf_mode/salt-minion.py +++ b/src/conf_mode/salt-minion.py @@ -17,16 +17,15 @@ import os from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from pwd import getpwnam from socket import gethostname from sys import exit from urllib3 import PoolManager from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError from vyos.util import call +from vyos.template import render config_file = r'/etc/salt/minion' @@ -88,18 +87,10 @@ def generate(salt): if salt is None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'salt-minion') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - if not os.path.exists(directory): os.makedirs(directory) - tmpl = env.get_template('minion.tmpl') - config_text = tmpl.render(salt) - with open(config_file, 'w') as f: - f.write(config_text) + render(config_file, 'salt-minion/minion.tmpl', salt) path = "/etc/salt/" for path in paths: diff --git a/src/conf_mode/service-ipoe.py b/src/conf_mode/service-ipoe.py index 5bd4aea2e..3a14d92ef 100755 --- a/src/conf_mode/service-ipoe.py +++ b/src/conf_mode/service-ipoe.py @@ -17,15 +17,15 @@ import os import re -from jinja2 import FileSystemLoader, Environment from socket import socket, AF_INET, SOCK_STREAM from sys import exit from time import sleep from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError from vyos.util import run +from vyos.template import render + ipoe_cnf_dir = r'/etc/accel-ppp/ipoe' ipoe_cnf = ipoe_cnf_dir + r'/ipoe.config' @@ -219,25 +219,15 @@ def generate(c): if c == None or not c: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'ipoe-server') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader, trim_blocks=True) - c['thread_cnt'] = _get_cpu() if c['auth']['mech'] == 'local': - tmpl = env.get_template('chap-secrets.tmpl') - chap_secrets_txt = tmpl.render(c) old_umask = os.umask(0o077) - with open(chap_secrets, 'w') as f: - f.write(chap_secrets_txt) + render(chap_secrets, 'ipoe-server/chap-secrets.tmpl', c, trim_blocks=True) os.umask(old_umask) - tmpl = env.get_template('ipoe.config.tmpl') - config_text = tmpl.render(c) - with open(ipoe_cnf, 'w') as f: - f.write(config_text) + render(ipoe_cnf, 'ipoe-server/ipoe.config.tmpl', c, trim_blocks=True) + # return c ?? return c diff --git a/src/conf_mode/service-pppoe.py b/src/conf_mode/service-pppoe.py index d3fc82406..a96249199 100755 --- a/src/conf_mode/service-pppoe.py +++ b/src/conf_mode/service-pppoe.py @@ -17,15 +17,15 @@ import os import re -from jinja2 import FileSystemLoader, Environment from socket import socket, AF_INET, SOCK_STREAM from sys import exit from time import sleep from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError from vyos.util import run +from vyos.template import render + pidfile = r'/var/run/accel_pppoe.pid' pppoe_cnf_dir = r'/etc/accel-ppp/pppoe' @@ -376,11 +376,6 @@ def generate(c): if c == None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'pppoe-server') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader, trim_blocks=True) - # accel-cmd reload doesn't work so any change results in a restart of the # daemon try: @@ -394,17 +389,11 @@ def generate(c): else: c['thread_cnt'] = int(os.cpu_count() / 2) - tmpl = env.get_template('pppoe.config.tmpl') - config_text = tmpl.render(c) - with open(pppoe_conf, 'w') as f: - f.write(config_text) + render(pppoe_conf, 'pppoe-server/pppoe.config.tmpl', c, trim_blocks=True) if c['authentication']['local-users']: - tmpl = env.get_template('chap-secrets.tmpl') - chap_secrets_txt = tmpl.render(c) old_umask = os.umask(0o077) - with open(chap_secrets, 'w') as f: - f.write(chap_secrets_txt) + render(chap_secrets, 'pppoe-server/chap-secrets.tmpl', c, trim_blocks=True) os.umask(old_umask) return c diff --git a/src/conf_mode/service-router-advert.py b/src/conf_mode/service-router-advert.py index 75a324260..620f3eacf 100755 --- a/src/conf_mode/service-router-advert.py +++ b/src/conf_mode/service-router-advert.py @@ -16,14 +16,13 @@ import os -from jinja2 import FileSystemLoader, Environment from stat import S_IRUSR, S_IWUSR, S_IRGRP from sys import exit from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError from vyos.util import call +from vyos.template import render config_file = r'/etc/radvd.conf' @@ -139,15 +138,7 @@ def generate(rtradv): if not rtradv['interfaces']: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'router-advert') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader, trim_blocks=True) - - tmpl = env.get_template('radvd.conf.tmpl') - config_text = tmpl.render(rtradv) - with open(config_file, 'w') as f: - f.write(config_text) + render(config_file, 'router-advert/radvd.conf.tmpl', rtradv, trim_blocks=True) # adjust file permissions of new configuration file if os.path.exists(config_file): diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index 4a69e8742..d654dcb84 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -20,14 +20,13 @@ from binascii import hexlify from time import sleep from stat import S_IRWXU, S_IXGRP, S_IXOTH, S_IROTH, S_IRGRP from sys import exit -from jinja2 import FileSystemLoader, Environment from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos.validate import is_ipv4, is_addr_assigned from vyos.version import get_version_data from vyos import ConfigError from vyos.util import call +from vyos.template import render config_file_client = r'/etc/snmp/snmp.conf' @@ -518,34 +517,14 @@ def generate(snmp): if snmp is None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'snmp') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - # Write client config file - tmpl = env.get_template('etc.snmp.conf.tmpl') - config_text = tmpl.render(snmp) - with open(config_file_client, 'w') as f: - f.write(config_text) - + render(config_file_client, 'snmp/etc.snmp.conf.tmpl', snmp) # Write server config file - tmpl = env.get_template('etc.snmpd.conf.tmpl') - config_text = tmpl.render(snmp) - with open(config_file_daemon, 'w') as f: - f.write(config_text) - + render(config_file_daemon, 'snmp/etc.snmpd.conf.tmpl', snmp) # Write access rights config file - tmpl = env.get_template('usr.snmpd.conf.tmpl') - config_text = tmpl.render(snmp) - with open(config_file_access, 'w') as f: - f.write(config_text) - + render(config_file_access, 'snmp/usr.snmpd.conf.tmpl', snmp) # Write access rights config file - tmpl = env.get_template('var.snmpd.conf.tmpl') - config_text = tmpl.render(snmp) - with open(config_file_user, 'w') as f: - f.write(config_text) + render(config_file_user, 'snmp/var.snmpd.conf.tmpl', snmp) return None diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py index a6cdb7ccc..ae79eac2d 100755 --- a/src/conf_mode/ssh.py +++ b/src/conf_mode/ssh.py @@ -15,13 +15,12 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -from jinja2 import FileSystemLoader, Environment from sys import exit from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError from vyos.util import call +from vyos.template import render config_file = r'/etc/ssh/sshd_config' @@ -120,15 +119,7 @@ def generate(ssh): if ssh is None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'ssh') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader, trim_blocks=True) - - tmpl = env.get_template('sshd_config.tmpl') - config_text = tmpl.render(ssh) - with open(config_file, 'w') as f: - f.write(config_text) + render(config_file, 'ssh/sshd_config.tmpl', ssh, trim_blocks=True) return None def apply(ssh): diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py index 43732cfae..6008ca0b3 100755 --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -16,7 +16,6 @@ import os -from jinja2 import FileSystemLoader, Environment from psutil import users from pwd import getpwall, getpwnam from stat import S_IRUSR, S_IWUSR, S_IRWXU, S_IRGRP, S_IXGRP @@ -24,10 +23,12 @@ from sys import exit from vyos.config import Config from vyos.configdict import list_diff -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError from vyos.util import cmd from vyos.util import call +from vyos.util import DEVNULL +from vyos.template import render + radius_config_file = "/etc/pam_radius_auth.conf" @@ -211,16 +212,16 @@ def generate(login): os.system("vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_set system login user '{}' authentication plaintext-password '' >/dev/null".format(user['name'])) os.system("vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_set system login user '{}' authentication encrypted-password '{}' >/dev/null".format(user['name'], user['password_encrypted'])) - if len(login['radius_server']) > 0: - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'system-login') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) + # env = os.environ.copy() + # env['vyos_libexec_dir'] = '/usr/libexec/vyos' - tmpl = env.get_template('pam_radius_auth.conf.tmpl') - config_text = tmpl.render(login) - with open(radius_config_file, 'w') as f: - f.write(config_text) + # call("/opt/vyatta/sbin/my_set system login user '{}' authentication plaintext-password ''".format(user['name']), + # env=env) + # call("/opt/vyatta/sbin/my_set system login user '{}' authentication encrypted-password '{}'".format(user['name'], user['password_encrypted']), + # env=env) + + if len(login['radius_server']) > 0: + render(radius_config_file, 'system-login/pam_radius_auth.conf.tmpl', login) uid = getpwnam('root').pw_uid gid = getpwnam('root').pw_gid @@ -256,7 +257,7 @@ def apply(login): command += " {}".format(user['name']) try: - call(command) + cmd(command) uid = getpwnam(user['name']).pw_uid gid = getpwnam(user['name']).pw_gid @@ -299,7 +300,7 @@ def apply(login): call('pkill -HUP -u {}'.format(user)) # Remove user account but leave home directory to be safe - call('userdel -r {} 2>/dev/null'.format(user)) + call(f'userdel -r {user}', stderr=DEVNULL) except Exception as e: raise ConfigError('Deleting user "{}" raised an exception: {}'.format(user, e)) @@ -309,8 +310,10 @@ def apply(login): # if len(login['radius_server']) > 0: try: + env = os.environ.copy() + env['DEBIAN_FRONTEND'] = 'noninteractive' # Enable RADIUS in PAM - os.system("DEBIAN_FRONTEND=noninteractive pam-auth-update --package --enable radius") + cmd("pam-auth-update --package --enable radius", env=env) # Make NSS system aware of RADIUS, too command = "sed -i -e \'/\smapname/b\' \ @@ -321,15 +324,18 @@ def apply(login): -e \'/^group:[^#]*$/s/: */&mapname /\' \ /etc/nsswitch.conf" - call(command) + cmd(command) except Exception as e: raise ConfigError('RADIUS configuration failed: {}'.format(e)) else: try: + env = os.environ.copy() + env['DEBIAN_FRONTEND'] = 'noninteractive' + # Disable RADIUS in PAM - os.system("DEBIAN_FRONTEND=noninteractive pam-auth-update --package --remove radius") + cmd("pam-auth-update --package --remove radius", env=env) command = "sed -i -e \'/^passwd:.*mapuid[ \t]/s/mapuid[ \t]//\' \ -e \'/^passwd:.*[ \t]mapname/s/[ \t]mapname//\' \ @@ -337,10 +343,10 @@ def apply(login): -e \'s/[ \t]*$//\' \ /etc/nsswitch.conf" - call(command) + cmd(command) except Exception as e: - raise ConfigError('Removing RADIUS configuration failed'.format(e)) + raise ConfigError('Removing RADIUS configuration failed.\n{}'.format(e)) return None diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py index 25b9b5bed..9da3d9157 100755 --- a/src/conf_mode/system-syslog.py +++ b/src/conf_mode/system-syslog.py @@ -17,13 +17,13 @@ import os import re -from jinja2 import FileSystemLoader, Environment from sys import exit from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError from vyos.util import run +from vyos.template import render + def get_config(): c = Config() @@ -192,22 +192,13 @@ def generate(c): if c == None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'syslog') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader, trim_blocks=True) - - tmpl = env.get_template('rsyslog.conf.tmpl') - config_text = tmpl.render(c) - with open('/etc/rsyslog.d/vyos-rsyslog.conf', 'w') as f: - f.write(config_text) + conf = '/etc/rsyslog.d/vyos-rsyslog.conf' + render(conf, 'syslog/rsyslog.conf.tmpl', c, trim_blocks=True) # eventually write for each file its own logrotate file, since size is # defined it shouldn't matter - tmpl = env.get_template('logrotate.tmpl') - config_text = tmpl.render(c) - with open('/etc/logrotate.d/vyos-rsyslog', 'w') as f: - f.write(config_text) + conf = '/etc/logrotate.d/vyos-rsyslog' + render(conf, 'syslog/logrotate.tmpl', c, trim_blocks=True) def verify(c): @@ -253,8 +244,8 @@ def verify(c): def apply(c): if not c: - return run('systemctl stop syslog') - return run('systemctl restart syslog') + return run('systemctl stop syslog.service') + return run('systemctl restart syslog.service') if __name__ == '__main__': try: diff --git a/src/conf_mode/system-wifi-regdom.py b/src/conf_mode/system-wifi-regdom.py index 943c42274..b222df0a9 100755 --- a/src/conf_mode/system-wifi-regdom.py +++ b/src/conf_mode/system-wifi-regdom.py @@ -18,11 +18,11 @@ import os from copy import deepcopy from sys import exit -from jinja2 import FileSystemLoader, Environment from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError +from vyos.template import render + config_80211_file='/etc/modprobe.d/cfg80211.conf' config_crda_file='/etc/default/crda' @@ -67,21 +67,8 @@ def generate(regdom): return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'wifi') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - tmpl = env.get_template('cfg80211.conf.tmpl') - config_text = tmpl.render(regdom) - with open(config_80211_file, 'w') as f: - f.write(config_text) - - tmpl = env.get_template('crda.tmpl') - config_text = tmpl.render(regdom) - with open(config_crda_file, 'w') as f: - f.write(config_text) - + render(config_80211_file, 'wifi/cfg80211.conf.tmpl', regdom) + render(config_crda_file, 'wifi/crda.tmpl', regdom) return None def apply(regdom): diff --git a/src/conf_mode/tftp_server.py b/src/conf_mode/tftp_server.py index 7a7246783..94c8bcf03 100755 --- a/src/conf_mode/tftp_server.py +++ b/src/conf_mode/tftp_server.py @@ -20,14 +20,13 @@ import pwd from copy import deepcopy from glob import glob -from jinja2 import FileSystemLoader, Environment from sys import exit from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos.validate import is_ipv4, is_addr_assigned from vyos import ConfigError from vyos.util import call +from vyos.template import render config_file = r'/etc/default/tftpd' @@ -90,11 +89,6 @@ def generate(tftpd): if tftpd is None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'tftp-server') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - idx = 0 for listen in tftpd['listen']: config = deepcopy(tftpd) @@ -103,11 +97,8 @@ def generate(tftpd): else: config['listen'] = ["[" + listen + "]" + tftpd['port'] + " -6"] - tmpl = env.get_template('default.tmpl') - config_text = tmpl.render(config) file = config_file + str(idx) - with open(file, 'w') as f: - f.write(config_text) + render(file, 'tftp-server/default.tmpl', config) idx = idx + 1 @@ -115,7 +106,7 @@ def generate(tftpd): def apply(tftpd): # stop all services first - then we will decide - call('systemctl stop tftpd@{0..20}') + call('systemctl stop tftpd@{0..20}.service') # bail out early - e.g. service deletion if tftpd is None: diff --git a/src/conf_mode/vpn-pptp.py b/src/conf_mode/vpn-pptp.py index 45b2c4b40..15b80f984 100755 --- a/src/conf_mode/vpn-pptp.py +++ b/src/conf_mode/vpn-pptp.py @@ -17,15 +17,15 @@ import os import re -from jinja2 import FileSystemLoader, Environment from socket import socket, AF_INET, SOCK_STREAM from sys import exit from time import sleep from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError from vyos.util import run +from vyos.template import render + pidfile = r'/var/run/accel_pptp.pid' pptp_cnf_dir = r'/etc/accel-ppp/pptp' @@ -206,11 +206,6 @@ def generate(c): if c == None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'pptp') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader, trim_blocks=True) - # accel-cmd reload doesn't work so any change results in a restart of the daemon try: if os.cpu_count() == 1: @@ -223,19 +218,13 @@ def generate(c): else: c['thread_cnt'] = int(os.cpu_count()/2) - tmpl = env.get_template('pptp.config.tmpl') - config_text = tmpl.render(c) - with open(pptp_conf, 'w') as f: - f.write(config_text) + render(pptp_conf, 'pptp/pptp.config.tmpl', c, trim_blocks=True) if c['authentication']['local-users']: - tmpl = env.get_template('chap-secrets.tmpl') - chap_secrets_txt = tmpl.render(c) old_umask = os.umask(0o077) - with open(chap_secrets, 'w') as f: - f.write(chap_secrets_txt) + render(chap_secrets, 'pptp/chap-secrets.tmpl', c, trim_blocks=True) os.umask(old_umask) - + # return c ?? return c diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py new file mode 100755 index 000000000..a8b183bef --- /dev/null +++ b/src/conf_mode/vpn_l2tp.py @@ -0,0 +1,386 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 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 + +from copy import deepcopy +from stat import S_IRUSR, S_IWUSR, S_IRGRP +from sys import exit +from time import sleep + +from ipaddress import ip_network + +from vyos.config import Config +from vyos.util import call +from vyos.validate import is_ipv4 +from vyos import ConfigError +from vyos.template import render + + +l2tp_conf = '/run/accel-pppd/l2tp.conf' +l2tp_chap_secrets = '/run/accel-pppd/l2tp.chap-secrets' + +default_config_data = { + 'auth_mode': 'local', + 'auth_ppp_mppe': 'prefer', + 'auth_proto': ['auth_mschap_v2'], + 'chap_secrets_file': l2tp_chap_secrets, # used in Jinja2 template + 'client_ip_pool': None, + 'client_ip_subnets': [], + 'client_ipv6_pool': [], + 'client_ipv6_delegate_prefix': [], + 'dnsv4': [], + 'dnsv6': [], + 'gateway_address': '10.255.255.0', + 'local_users' : [], + 'mtu': '1436', + 'outside_addr': '', + 'ppp_mppe': 'prefer', + 'ppp_echo_failure' : '3', + 'ppp_echo_interval' : '30', + 'ppp_echo_timeout': '0', + 'radius_server': [], + 'radius_acct_tmo': '3', + 'radius_max_try': '3', + 'radius_timeout': '3', + 'radius_nas_id': '', + 'radius_nas_ip': '', + 'radius_source_address': '', + 'radius_shaper_attr': '', + 'radius_shaper_vendor': '', + 'radius_dynamic_author': '', + 'wins': [], + 'ip6_column': [], + 'thread_cnt': 1 +} + +def get_config(): + conf = Config() + base_path = ['vpn', 'l2tp', 'remote-access'] + if not conf.exists(base_path): + return None + + conf.set_level(base_path) + l2tp = deepcopy(default_config_data) + + cpu = os.cpu_count() + if cpu > 1: + l2tp['thread_cnt'] = int(cpu/2) + + ### general options ### + if conf.exists(['name-server']): + for name_server in conf.return_values(['name-server']): + if is_ipv4(name_server): + l2tp['dnsv4'].append(name_server) + else: + l2tp['dnsv6'].append(name_server) + + if conf.exists(['wins-server']): + l2tp['wins'] = conf.return_values(['wins-server']) + + if conf.exists('outside-address'): + l2tp['outside_addr'] = conf.return_value('outside-address') + + if conf.exists(['authentication', 'mode']): + l2tp['auth_mode'] = conf.return_value(['authentication', 'mode']) + + if conf.exists(['authentication', 'protocols']): + auth_mods = { + 'pap': 'auth_pap', + 'chap': 'auth_chap_md5', + 'mschap': 'auth_mschap_v1', + 'mschap-v2': 'auth_mschap_v2' + } + + for proto in conf.return_values(['authentication', 'protocols']): + l2tp['auth_proto'].append(auth_mods[proto]) + + if conf.exists(['authentication', 'mppe']): + l2tp['auth_ppp_mppe'] = conf.return_value(['authentication', 'mppe']) + + # + # local auth + if conf.exists(['authentication', 'local-users']): + for username in conf.list_nodes(['authentication', 'local-users', 'username']): + user = { + 'name' : username, + 'password' : '', + 'state' : 'enabled', + 'ip' : '*', + 'upload' : None, + 'download' : None + } + + conf.set_level(base_path + ['authentication', 'local-users', 'username', username]) + + if conf.exists(['password']): + user['password'] = conf.return_value(['password']) + + if conf.exists(['disable']): + user['state'] = 'disable' + + if conf.exists(['static-ip']): + user['ip'] = conf.return_value(['static-ip']) + + if conf.exists(['rate-limit', 'download']): + user['download'] = conf.return_value(['rate-limit', 'download']) + + if conf.exists(['rate-limit', 'upload']): + user['upload'] = conf.return_value(['rate-limit', 'upload']) + + l2tp['local_users'].append(user) + + # + # RADIUS auth and settings + conf.set_level(base_path + ['authentication', 'radius']) + if conf.exists(['server']): + for server in conf.list_nodes(['server']): + radius = { + 'server' : server, + 'key' : '', + 'fail_time' : 0, + 'port' : '1812' + } + + conf.set_level(base_path + ['authentication', 'radius', 'server', server]) + + if conf.exists(['fail-time']): + radius['fail-time'] = conf.return_value(['fail-time']) + + if conf.exists(['port']): + radius['port'] = conf.return_value(['port']) + + if conf.exists(['key']): + radius['key'] = conf.return_value(['key']) + + if not conf.exists(['disable']): + l2tp['radius_server'].append(radius) + + # + # advanced radius-setting + conf.set_level(base_path + ['authentication', 'radius']) + + if conf.exists(['acct-timeout']): + l2tp['radius_acct_tmo'] = conf.return_value(['acct-timeout']) + + if conf.exists(['max-try']): + l2tp['radius_max_try'] = conf.return_value(['max-try']) + + if conf.exists(['timeout']): + l2tp['radius_timeout'] = conf.return_value(['timeout']) + + if conf.exists(['nas-identifier']): + l2tp['radius_nas_id'] = conf.return_value(['nas-identifier']) + + if conf.exists(['nas-ip-address']): + l2tp['radius_nas_ip'] = conf.return_value(['nas-ip-address']) + + if conf.exists(['source-address']): + l2tp['radius_source_address'] = conf.return_value(['source-address']) + + # Dynamic Authorization Extensions (DOA)/Change Of Authentication (COA) + if conf.exists(['dynamic-author']): + dae = { + 'port' : '', + 'server' : '', + 'key' : '' + } + + if conf.exists(['dynamic-author', 'server']): + dae['server'] = conf.return_value(['dynamic-author', 'server']) + + if conf.exists(['dynamic-author', 'port']): + dae['port'] = conf.return_value(['dynamic-author', 'port']) + + if conf.exists(['dynamic-author', 'key']): + dae['key'] = conf.return_value(['dynamic-author', 'key']) + + l2tp['radius_dynamic_author'] = dae + + if conf.exists(['rate-limit', 'enable']): + l2tp['radius_shaper_attr'] = 'Filter-Id' + c_attr = ['rate-limit', 'enable', 'attribute'] + if conf.exists(c_attr): + l2tp['radius_shaper_attr'] = conf.return_value(c_attr) + + c_vendor = ['rate-limit', 'enable', 'vendor'] + if conf.exists(c_vendor): + l2tp['radius_shaper_vendor'] = conf.return_value(c_vendor) + + conf.set_level(base_path) + if conf.exists(['client-ip-pool']): + if conf.exists(['client-ip-pool', 'start']) and conf.exists(['client-ip-pool', 'stop']): + start = conf.return_value(['client-ip-pool', 'start']) + stop = conf.return_value(['client-ip-pool', 'stop']) + l2tp['client_ip_pool'] = start + '-' + re.search('[0-9]+$', stop).group(0) + + if conf.exists(['client-ip-pool', 'subnet']): + l2tp['client_ip_subnets'] = conf.return_values(['client-ip-pool', 'subnet']) + + if conf.exists(['client-ipv6-pool', 'prefix']): + l2tp['ip6_column'].append('ip6') + for prefix in conf.list_nodes(['client-ipv6-pool', 'prefix']): + tmp = { + 'prefix': prefix, + 'mask': '64' + } + + if conf.exists(['client-ipv6-pool', 'prefix', prefix, 'mask']): + tmp['mask'] = conf.return_value(['client-ipv6-pool', 'prefix', prefix, 'mask']) + + l2tp['client_ipv6_pool'].append(tmp) + + if conf.exists(['client-ipv6-pool', 'delegate']): + l2tp['ip6_column'].append('ip6-db') + for prefix in conf.list_nodes(['client-ipv6-pool', 'delegate']): + tmp = { + 'prefix': prefix, + 'mask': '' + } + + if conf.exists(['client-ipv6-pool', 'delegate', prefix, 'mask']): + tmp['mask'] = conf.return_value(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']) + + l2tp['client_ipv6_delegate_prefix'].append(tmp) + + if conf.exists(['mtu']): + l2tp['mtu'] = conf.return_value(['mtu']) + + # gateway address + if conf.exists(['gateway-address']): + l2tp['gateway_address'] = conf.return_value(['gateway-address']) + else: + # calculate gw-ip-address + if conf.exists(['client-ip-pool', 'start']): + # use start ip as gw-ip-address + l2tp['gateway_address'] = conf.return_value(['client-ip-pool', 'start']) + + elif conf.exists(['client-ip-pool', 'subnet']): + # use first ip address from first defined pool + subnet = conf.return_values(['client-ip-pool', 'subnet'])[0] + subnet = ip_network(subnet) + l2tp['gateway_address'] = str(list(subnet.hosts())[0]) + + # LNS secret + if conf.exists(['lns', 'shared-secret']): + l2tp['lns_shared_secret'] = conf.return_value(['lns', 'shared-secret']) + + if conf.exists(['ccp-disable']): + l2tp[['ccp_disable']] = True + + # PPP options + if conf.exists(['idle']): + l2tp['ppp_echo_timeout'] = conf.return_value(['idle']) + + if conf.exists(['ppp-options', 'lcp-echo-failure']): + l2tp['ppp_echo_failure'] = conf.return_value(['ppp-options', 'lcp-echo-failure']) + + if conf.exists(['ppp-options', 'lcp-echo-interval']): + l2tp['ppp_echo_interval'] = conf.return_value(['ppp-options', 'lcp-echo-interval']) + + return l2tp + + +def verify(l2tp): + if not l2tp: + return None + + if l2tp['auth_mode'] == 'local': + if not l2tp['local_users']: + raise ConfigError('L2TP local auth mode requires local users to be configured!') + + for user in l2tp['local_users']: + if not user['password']: + raise ConfigError(f"Password required for user {user['name']}") + + elif l2tp['auth_mode'] == 'radius': + if len(l2tp['radius_server']) == 0: + raise ConfigError("RADIUS authentication requires at least one server") + + for radius in l2tp['radius_server']: + if not radius['key']: + raise ConfigError(f"Missing RADIUS secret for server {{ radius['key'] }}") + + # check for the existence of a client ip pool + if not (l2tp['client_ip_pool'] or l2tp['client_ip_subnets']): + raise ConfigError( + "set vpn l2tp remote-access client-ip-pool requires subnet or start/stop IP pool") + + # check ipv6 + if l2tp['client_ipv6_delegate_prefix'] and not l2tp['client_ipv6_pool']: + raise ConfigError('IPv6 prefix delegation requires client-ipv6-pool prefix') + + for prefix in l2tp['client_ipv6_delegate_prefix']: + if not prefix['mask']: + raise ConfigError('Delegation-prefix required for individual delegated networks') + + if len(l2tp['wins']) > 2: + raise ConfigError('Not more then two IPv4 WINS name-servers can be configured') + + if len(l2tp['dnsv4']) > 2: + raise ConfigError('Not more then two IPv4 DNS name-servers can be configured') + + if len(l2tp['dnsv6']) > 3: + raise ConfigError('Not more then three IPv6 DNS name-servers can be configured') + + return None + + +def generate(l2tp): + if not l2tp: + return None + + dirname = os.path.dirname(l2tp_conf) + if not os.path.exists(dirname): + os.mkdir(dirname) + + render(l2tp_conf, 'l2tp/l2tp.config.tmpl', c, trim_blocks=True) + + if l2tp['auth_mode'] == 'local': + render(l2tp_chap_secrets, 'l2tp/chap-secrets.tmpl', l2tp) + os.chmod(l2tp_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP) + + else: + if os.path.exists(l2tp_chap_secrets): + os.unlink(l2tp_chap_secrets) + + return None + + +def apply(l2tp): + if not l2tp: + call('systemctl stop accel-ppp@l2tp.service') + + if os.path.exists(l2tp_conf): + os.unlink(l2tp_conf) + + if os.path.exists(l2tp_chap_secrets): + os.unlink(l2tp_chap_secrets) + + return None + + call('systemctl restart accel-ppp@l2tp.service') + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index ca0844c50..438731972 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -18,49 +18,24 @@ import os from time import sleep from sys import exit -from socket import socket, AF_INET, SOCK_STREAM from copy import deepcopy from stat import S_IRUSR, S_IWUSR, S_IRGRP -from jinja2 import FileSystemLoader, Environment from vyos.config import Config from vyos import ConfigError -from vyos.defaults import directories as vyos_data_dir -from vyos.util import process_running -from vyos.util import process_running, cmd, run - -pidfile = r'/var/run/accel_sstp.pid' -sstp_cnf_dir = r'/etc/accel-ppp/sstp' -chap_secrets = sstp_cnf_dir + '/chap-secrets' -sstp_conf = sstp_cnf_dir + '/sstp.config' - -# config path creation -if not os.path.exists(sstp_cnf_dir): - os.makedirs(sstp_cnf_dir) - -def chk_con(): - cnt = 0 - s = socket(AF_INET, SOCK_STREAM) - while True: - try: - s.connect(("127.0.0.1", 2005)) - s.close() - break - except ConnectionRefusedError: - sleep(0.5) - cnt += 1 - if cnt == 100: - raise("failed to start sstp server") - break - - -def _accel_cmd(command): - return run(f'/usr/bin/accel-cmd -p 2005 {command}') +from vyos.util import call, run +from vyos.template import render + + +sstp_conf = '/run/accel-pppd/sstp.conf' +sstp_chap_secrets = '/run/accel-pppd/sstp.chap-secrets' default_config_data = { 'local_users' : [], 'auth_mode' : 'local', - 'auth_proto' : [], + 'auth_proto' : ['auth_mschap_v2'], + 'chap_secrets_file': sstp_chap_secrets, # used in Jinja2 template + 'client_gateway': '', 'radius_server' : [], 'radius_acct_tmo' : '3', 'radius_max_try' : '3', @@ -77,11 +52,11 @@ default_config_data = { 'client_ip_pool' : [], 'dnsv4' : [], 'mtu' : '', - 'ppp_mppe' : '', + 'ppp_mppe' : 'prefer', 'ppp_echo_failure' : '', 'ppp_echo_interval' : '', 'ppp_echo_timeout' : '', - 'thread_cnt' : '' + 'thread_cnt' : 1 } def get_config(): @@ -93,10 +68,9 @@ def get_config(): conf.set_level(base_path) - cpu = int(os.cpu_count()/2) - if cpu < 1: - cpu = 1 - sstp['thread_cnt'] = cpu + cpu = os.cpu_count() + if cpu > 1: + sstp['thread_cnt'] = int(cpu/2) if conf.exists(['authentication', 'mode']): sstp['auth_mode'] = conf.return_value(['authentication', 'mode']) @@ -214,6 +188,8 @@ def get_config(): # authentication protocols conf.set_level(base_path + ['authentication']) if conf.exists(['protocols']): + # clear default list content, now populate with actual CLI values + sstp['auth_proto'] = [] auth_mods = { 'pap': 'auth_pap', 'chap': 'auth_chap_md5', @@ -224,9 +200,6 @@ def get_config(): for proto in conf.return_values(['protocols']): sstp['auth_proto'].append(auth_mods[proto]) - else: - sstp['auth_proto'] = ['auth_mschap_v2'] - # # read in SSL certs conf.set_level(base_path + ['ssl']) @@ -262,7 +235,7 @@ def get_config(): # read in PPP stuff conf.set_level(base_path + ['ppp-settings']) if conf.exists('mppe'): - sstp['ppp_mppe'] = conf.return_value('ppp-settings mppe') + sstp['ppp_mppe'] = conf.return_value(['ppp-settings', 'mppe']) if conf.exists(['lcp-echo-failure']): sstp['ppp_echo_failure'] = conf.return_value(['lcp-echo-failure']) @@ -283,7 +256,7 @@ def verify(sstp): # vertify auth settings if sstp['auth_mode'] == 'local': if not sstp['local_users']: - raise ConfigError('sstp-server authentication local-users required') + raise ConfigError('SSTP local auth mode requires local users to be configured!') for user in sstp['local_users']: if not user['password']: @@ -303,7 +276,7 @@ def verify(sstp): raise ConfigError("Client gateway IP address required") if len(sstp['dnsv4']) > 2: - raise ConfigError("Only 2 DNS name-servers can be configured") + raise ConfigError('Not more then two IPv4 DNS name-servers can be configured') if not sstp['ssl_ca'] or not sstp['ssl_cert'] or not sstp['ssl_key']: raise ConfigError('One or more SSL certificates missing') @@ -326,69 +299,38 @@ def verify(sstp): raise ConfigError(f"Missing RADIUS secret for server {{ radius['key'] }}") def generate(sstp): - if sstp is None: + if not sstp: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'sstp') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader, trim_blocks=True) + dirname = os.path.dirname(sstp_conf) + if not os.path.exists(dirname): + os.mkdir(dirname) # accel-cmd reload doesn't work so any change results in a restart of the daemon - tmpl = env.get_template('sstp.config.tmpl') - config_text = tmpl.render(sstp) - with open(sstp_conf, 'w') as f: - f.write(config_text) + render(sstp_conf, 'sstp/sstp.config.tmpl', sstp, trim_blocks=True) if sstp['local_users']: - tmpl = env.get_template('chap-secrets.tmpl') - config_text = tmpl.render(sstp) - with open(chap_secrets, 'w') as f: - f.write(config_text) - - os.chmod(chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP) + render(sstp_chap_secrets, 'sstp/chap-secrets.tmpl', sstp, trim_blocks=True) + os.chmod(sstp_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP) else: - if os.path.exists(chap_secrets): - os.unlink(chap_secrets) + if os.path.exists(sstp_chap_secrets): + os.unlink(sstp_chap_secrets) return sstp def apply(sstp): - if sstp is None: - if process_running(pidfile): - command = 'start-stop-daemon' - command += ' --stop ' - command += ' --quiet' - command += ' --oknodo' - command += ' --pidfile ' + pidfile - cmd(command) + if not sstp: + call('systemctl stop accel-ppp@sstp.service') - if os.path.exists(pidfile): - os.remove(pidfile) + if os.path.exists(sstp_conf): + os.unlink(sstp_conf) - return None + if os.path.exists(sstp_chap_secrets): + os.unlink(sstp_chap_secrets) - if not process_running(pidfile): - if os.path.exists(pidfile): - os.remove(pidfile) - - command = 'start-stop-daemon' - command += ' --start ' - command += ' --quiet' - command += ' --oknodo' - command += ' --pidfile ' + pidfile - command += ' --exec /usr/sbin/accel-pppd' - # now pass arguments to accel-pppd binary - command += ' --' - command += ' -c ' + sstp_conf - command += ' -p ' + pidfile - command += ' -d' - cmd(command) - - chk_con() + return None - else: - _accel_cmd('restart') + call('systemctl restart accel-ppp@sstp.service') if __name__ == '__main__': diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 586424c09..eb73293a9 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -18,15 +18,15 @@ import os from sys import exit from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from json import loads from vyos.config import Config from vyos.configdict import list_diff -from vyos.defaults import directories as vyos_data_dir from vyos.ifconfig import Interface from vyos.util import read_file, cmd from vyos import ConfigError +from vyos.template import render + config_file = r'/etc/iproute2/rt_tables.d/vyos-vrf.conf' @@ -178,16 +178,7 @@ def verify(vrf_config): return None def generate(vrf_config): - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'vrf') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - tmpl = env.get_template('vrf.conf.tmpl') - config_text = tmpl.render(vrf_config) - with open(config_file, 'w') as f: - f.write(config_text) - + render(config_file, 'vrf/vrf.conf.tmpl', vrf_config) return None def apply(vrf_config): diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py index 3f1b73385..b9b0405e2 100755 --- a/src/conf_mode/vrrp.py +++ b/src/conf_mode/vrrp.py @@ -18,16 +18,16 @@ import os from sys import exit from ipaddress import ip_address, ip_interface, IPv4Interface, IPv6Interface, IPv4Address, IPv6Address -from jinja2 import FileSystemLoader, Environment from json import dumps from pathlib import Path import vyos.config import vyos.keepalived -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError from vyos.util import call +from vyos.template import render + daemon_file = "/etc/default/keepalived" config_file = "/etc/keepalived/keepalived.conf" @@ -201,11 +201,6 @@ def verify(data): def generate(data): - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'vrrp') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - vrrp_groups, sync_groups = data # Remove disabled groups from the sync group member lists @@ -217,16 +212,9 @@ def generate(data): # Filter out disabled groups vrrp_groups = list(filter(lambda x: x["disable"] is not True, vrrp_groups)) - tmpl = env.get_template('keepalived.conf.tmpl') - config_text = tmpl.render({"groups": vrrp_groups, "sync_groups": sync_groups}) - with open(config_file, 'w') as f: - f.write(config_text) - - tmpl = env.get_template('daemon.tmpl') - config_text = tmpl.render() - with open(daemon_file, 'w') as f: - f.write(config_text) - + render(config_file, 'vrrp/keepalived.conf.tmpl', + {"groups": vrrp_groups, "sync_groups": sync_groups}) + render(daemon_file, 'vrrp/daemon.tmpl', {}) return None diff --git a/src/etc/init.d/isc-dhcpv4-server b/src/etc/init.d/isc-dhcpv4-server deleted file mode 100755 index 94a1020ac..000000000 --- a/src/etc/init.d/isc-dhcpv4-server +++ /dev/null @@ -1,113 +0,0 @@ -#!/bin/sh -# -# - -### BEGIN INIT INFO -# Provides: isc-dhcpv4-server -# Required-Start: $remote_fs $network $syslog -# Required-Stop: $remote_fs $network $syslog -# Should-Start: $local_fs slapd $named -# Should-Stop: $local_fs slapd -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: IPv4 DHCP server -# Description: Dynamic Host Configuration Protocol Server for IPv4 -### END INIT INFO - -PATH=/sbin:/bin:/usr/sbin:/usr/bin - -test -f /usr/sbin/dhcpd || exit 0 - -DHCPD_DEFAULT="${DHCPD_DEFAULT:-/etc/default/isc-dhcpv4-server}" - -# It is not safe to start if we don't have a default configuration... -if [ ! -f "$DHCPD_DEFAULT" ]; then - echo "$DHCPD_DEFAULT does not exist! - Aborting..." - exit 0 -fi - -. /lib/lsb/init-functions - -# Read init script configuration -[ -f "$DHCPD_DEFAULT" ] && . "$DHCPD_DEFAULT" - -NAME=dhcpd -DESC="ISC DHCP server" -# fallback to default config file -DHCPD_CONF=${DHCPD_CONF:-/etc/dhcp/dhcpd.conf} -# try to read pid file name from config file, with fallback to /var/run/dhcpd.pid -if [ -z "$DHCPD_PID" ]; then - DHCPD_PID=$(sed -n -e 's/^[ \t]*pid-file-name[ \t]*"(.*)"[ \t]*;.*$/\1/p' < "$DHCPD_CONF" 2>/dev/null | head -n 1) -fi -DHCPD_PID="${DHCPD_PID:-/var/run/dhcpd.pid}" - -test_config() -{ - if ! /usr/sbin/dhcpd -t $OPTIONS -q -cf "$DHCPD_CONF" > /dev/null 2>&1; then - echo "dhcpd self-test failed. Please fix $DHCPD_CONF." - echo "The error was: " - /usr/sbin/dhcpd -t $OPTIONS -cf "$DHCPD_CONF" - exit 1 - fi - touch /var/lib/dhcp/dhcpd.leases -} - -# single arg is -v for messages, -q for none -check_status() -{ - if [ ! -r "$DHCPD_PID" ]; then - test "$1" != -v || echo "$NAME is not running." - return 3 - fi - if read pid < "$DHCPD_PID" && ps -p "$pid" > /dev/null 2>&1; then - test "$1" != -v || echo "$NAME is running." - return 0 - else - test "$1" != -v || echo "$NAME is not running but $DHCPD_PID exists." - return 1 - fi -} - -case "$1" in - start) - test_config - log_daemon_msg "Starting $DESC" "$NAME" - start-stop-daemon --start --oknodo --quiet --pidfile "$DHCPD_PID" \ - --exec /usr/sbin/dhcpd -- \ - -q $OPTIONS -cf "$DHCPD_CONF" -pf "$DHCPD_PID" $INTERFACES - sleep 2 - - if check_status -q; then - log_end_msg 0 - else - log_failure_msg "check syslog for diagnostics." - log_end_msg 1 - exit 1 - fi - ;; - stop) - log_daemon_msg "Stopping $DESC" "$NAME" - start-stop-daemon --stop --oknodo --quiet --pidfile "$DHCPD_PID" - log_end_msg $? - rm -f "$DHCPD_PID" - ;; - restart | force-reload) - test_config - $0 stop - sleep 2 - $0 start - if [ "$?" != "0" ]; then - exit 1 - fi - ;; - status) - echo -n "Status of $DESC: " - check_status -v - exit "$?" - ;; - *) - echo "Usage: $0 {start|stop|restart|force-reload|status}" - exit 1 -esac - -exit 0 diff --git a/src/etc/init.d/isc-dhcpv6-relay b/src/etc/init.d/isc-dhcpv6-relay deleted file mode 100755 index e553eafd1..000000000 --- a/src/etc/init.d/isc-dhcpv6-relay +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/sh -# -# - -### BEGIN INIT INFO -# Provides: isc-dhcpv6-relay -# Required-Start: $remote_fs $network -# Required-Stop: $remote_fs $network -# Should-Start: $local_fs -# Should-Stop: $local_fs -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: IPv6 DHCP relay -# Description: Dynamic Host Configuration Protocol Relay for IPv6 -### END INIT INFO - -# It is not safe to start if we don't have a default configuration... -if [ ! -f /etc/default/isc-dhcpv6-relay ]; then - echo "/etc/default/isc-dhcpv6-relay does not exist! - Aborting..." - exit 1 -fi - -# Source init functions -. /lib/lsb/init-functions - -# Read init script configuration (interfaces the daemon should listen on -# and the DHCP server we should forward requests to.) -[ -f /etc/default/isc-dhcpv6-relay ] && . /etc/default/isc-dhcpv6-relay - -DHCRELAYPID=/var/run/dhcv6relay.pid - -case "$1" in - start) - start-stop-daemon --start --oknodo --quiet --pidfile $DHCRELAYPID \ - --exec /usr/sbin/dhcrelay -- -q $OPTIONS -pf $DHCRELAYPID - ;; - stop) - start-stop-daemon --stop --oknodo --quiet --pidfile $DHCRELAYPID - ;; - restart | force-reload) - $0 stop - sleep 2 - $0 start - ;; - *) - echo "Usage: /etc/init.d/isc-dhcpv6-relay {start|stop|restart|force-reload}" - exit 1 -esac - -exit 0 diff --git a/src/etc/init.d/isc-dhcpv6-server b/src/etc/init.d/isc-dhcpv6-server deleted file mode 100755 index f6b27cb4a..000000000 --- a/src/etc/init.d/isc-dhcpv6-server +++ /dev/null @@ -1,113 +0,0 @@ -#!/bin/sh -# -# - -### BEGIN INIT INFO -# Provides: isc-dhcpv6-server -# Required-Start: $remote_fs $network $syslog -# Required-Stop: $remote_fs $network $syslog -# Should-Start: $local_fs slapd $named -# Should-Stop: $local_fs slapd -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: IPv6 DHCP server -# Description: Dynamic Host Configuration Protocol Server for IPv6 -### END INIT INFO - -PATH=/sbin:/bin:/usr/sbin:/usr/bin - -test -f /usr/sbin/dhcpd || exit 0 - -DHCPD_DEFAULT="${DHCPD_DEFAULT:-/etc/default/isc-dhcpv6-server}" - -# It is not safe to start if we don't have a default configuration... -if [ ! -f "$DHCPD_DEFAULT" ]; then - echo "$DHCPD_DEFAULT does not exist! - Aborting..." - exit 0 -fi - -. /lib/lsb/init-functions - -# Read init script configuration -[ -f "$DHCPD_DEFAULT" ] && . "$DHCPD_DEFAULT" - -NAME=dhcpdv6 -DESC="ISC DHCP server IPv6" -# fallback to default config file -DHCPD_CONF=${DHCPD_CONF:-/etc/dhcp/dhcpdv6.conf} -# try to read pid file name from config file, with fallback to /var/run/dhcpdv6.pid -if [ -z "$DHCPD_PID" ]; then - DHCPD_PID=$(sed -n -e 's/^[ \t]*pid-file-name[ \t]*"(.*)"[ \t]*;.*$/\1/p' < "$DHCPD_CONF" 2>/dev/null | head -n 1) -fi -DHCPD_PID="${DHCPD_PID:-/var/run/dhcpdv6.pid}" - -test_config() -{ - if ! /usr/sbin/dhcpd -t $OPTIONS -q -cf "$DHCPD_CONF" > /dev/null 2>&1; then - echo "dhcpd self-test failed. Please fix $DHCPD_CONF." - echo "The error was: " - /usr/sbin/dhcpd -t $OPTIONS -cf "$DHCPD_CONF" - exit 1 - fi - touch /var/lib/dhcp/dhcpdv6.leases -} - -# single arg is -v for messages, -q for none -check_status() -{ - if [ ! -r "$DHCPD_PID" ]; then - test "$1" != -v || echo "$NAME is not running." - return 3 - fi - if read pid < "$DHCPD_PID" && ps -p "$pid" > /dev/null 2>&1; then - test "$1" != -v || echo "$NAME is running." - return 0 - else - test "$1" != -v || echo "$NAME is not running but $DHCPD_PID exists." - return 1 - fi -} - -case "$1" in - start) - test_config - log_daemon_msg "Starting $DESC" "$NAME" - start-stop-daemon --start --oknodo --quiet --pidfile "$DHCPD_PID" \ - --exec /usr/sbin/dhcpd -- \ - -q $OPTIONS -cf "$DHCPD_CONF" -pf "$DHCPD_PID" $INTERFACES - sleep 2 - - if check_status -q; then - log_end_msg 0 - else - log_failure_msg "check syslog for diagnostics." - log_end_msg 1 - exit 1 - fi - ;; - stop) - log_daemon_msg "Stopping $DESC" "$NAME" - start-stop-daemon --stop --oknodo --quiet --pidfile "$DHCPD_PID" - log_end_msg $? - rm -f "$DHCPD_PID" - ;; - restart | force-reload) - test_config - $0 stop - sleep 2 - $0 start - if [ "$?" != "0" ]; then - exit 1 - fi - ;; - status) - echo -n "Status of $DESC: " - check_status -v - exit "$?" - ;; - *) - echo "Usage: $0 {start|stop|restart|force-reload|status}" - exit 1 -esac - -exit 0 diff --git a/src/etc/systemd/system/hostapd@.service.d/override.conf b/src/etc/systemd/system/hostapd@.service.d/override.conf new file mode 100644 index 000000000..bb8e81d7a --- /dev/null +++ b/src/etc/systemd/system/hostapd@.service.d/override.conf @@ -0,0 +1,10 @@ +[Unit] +After= +After=vyos-router.service + +[Service] +WorkingDirectory=/run/hostapd +EnvironmentFile= +ExecStart= +ExecStart=/usr/sbin/hostapd -B -P /run/hostapd/%i.pid /run/hostapd/%i.conf +PIDFile=/run/hostapd/%i.pid diff --git a/src/etc/systemd/system/openvpn@.service.d/override.conf b/src/etc/systemd/system/openvpn@.service.d/override.conf new file mode 100644 index 000000000..7946484a3 --- /dev/null +++ b/src/etc/systemd/system/openvpn@.service.d/override.conf @@ -0,0 +1,9 @@ +[Unit] +After= +After=vyos-router.service + +[Service] +WorkingDirectory= +WorkingDirectory=/run/openvpn +ExecStart= +ExecStart=/usr/sbin/openvpn --daemon openvpn-%i --config %i.conf --status %i.status 30 --writepid %i.pid diff --git a/src/etc/systemd/system/pdns-recursor.service.d/override.conf b/src/etc/systemd/system/pdns-recursor.service.d/override.conf new file mode 100644 index 000000000..602d7b774 --- /dev/null +++ b/src/etc/systemd/system/pdns-recursor.service.d/override.conf @@ -0,0 +1,5 @@ +[Service] +WorkingDirectory= +WorkingDirectory=/run/powerdns +ExecStart= +ExecStart=/usr/sbin/pdns_recursor --daemon=no --write-pid=no --disable-syslog --log-timestamp=no --config-dir=/run/powerdns diff --git a/src/etc/systemd/system/wpa_supplicant@.service.d/override.conf b/src/etc/systemd/system/wpa_supplicant@.service.d/override.conf new file mode 100644 index 000000000..20b25b726 --- /dev/null +++ b/src/etc/systemd/system/wpa_supplicant@.service.d/override.conf @@ -0,0 +1,10 @@ +[Unit] +After= +After=vyos-router.service + +[Service] +WorkingDirectory= +WorkingDirectory=/run/wpa_supplicant +EnvironmentFile= +ExecStart= +ExecStart=/sbin/wpa_supplicant -c%I.conf -Dnl80211,wext -i%I diff --git a/src/migration-scripts/l2tp/2-to-3 b/src/migration-scripts/l2tp/2-to-3 new file mode 100755 index 000000000..bd0839e03 --- /dev/null +++ b/src/migration-scripts/l2tp/2-to-3 @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 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/>. + +# - remove primary/secondary identifier from nameserver +# - TODO: remove radius server req-limit + +import os +import sys + +from sys import argv, exit +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['vpn', 'l2tp', 'remote-access'] +if not config.exists(base): + # Nothing to do + exit(0) +else: + + # Migrate IPv4 DNS servers + dns_base = base + ['dns-servers'] + if config.exists(dns_base): + for server in ['server-1', 'server-2']: + if config.exists(dns_base + [server]): + dns = config.return_value(dns_base + [server]) + config.set(base + ['name-server'], value=dns, replace=False) + + config.delete(dns_base) + + # Migrate IPv6 DNS servers + dns_base = base + ['dnsv6-servers'] + if config.exists(dns_base): + for server in config.return_values(dns_base): + config.set(base + ['name-server'], value=server, replace=False) + + config.delete(dns_base) + + # Migrate IPv4 WINS servers + wins_base = base + ['wins-servers'] + if config.exists(wins_base): + for server in ['server-1', 'server-2']: + if config.exists(wins_base + [server]): + wins = config.return_value(wins_base + [server]) + config.set(base + ['wins-server'], value=wins, replace=False) + + config.delete(wins_base) + + + # Remove RADIUS server req-limit node + radius_base = base + ['authentication', 'radius'] + if config.exists(radius_base): + for server in config.list_nodes(radius_base + ['server']): + if config.exists(radius_base + ['server', server, 'req-limit']): + config.delete(radius_base + ['server', server, 'req-limit']) + + # Migrate IPv6 prefixes + ipv6_base = base + ['client-ipv6-pool'] + if config.exists(ipv6_base + ['prefix']): + prefix_old = config.return_values(ipv6_base + ['prefix']) + # delete old prefix CLI nodes + config.delete(ipv6_base + ['prefix']) + # create ned prefix tag node + config.set(ipv6_base + ['prefix']) + config.set_tag(ipv6_base + ['prefix']) + + for p in prefix_old: + prefix = p.split(',')[0] + mask = p.split(',')[1] + config.set(ipv6_base + ['prefix', prefix, 'mask'], value=mask) + + if config.exists(ipv6_base + ['delegate-prefix']): + prefix_old = config.return_values(ipv6_base + ['delegate-prefix']) + # delete old delegate prefix CLI nodes + config.delete(ipv6_base + ['delegate-prefix']) + # create ned delegation tag node + config.set(ipv6_base + ['delegate ']) + config.set_tag(ipv6_base + ['delegate ']) + + for p in prefix_old: + prefix = p.split(',')[0] + mask = p.split(',')[1] + config.set(ipv6_base + ['delegate', prefix, 'mask'], value=mask) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/op_mode/dns_forwarding_restart.sh b/src/op_mode/dns_forwarding_restart.sh index 8e556f2f0..64cc92115 100755 --- a/src/op_mode/dns_forwarding_restart.sh +++ b/src/op_mode/dns_forwarding_restart.sh @@ -2,7 +2,7 @@ if cli-shell-api existsEffective service dns forwarding; then echo "Restarting the DNS forwarding service" - systemctl restart pdns-recursor + systemctl restart pdns-recursor.service else echo "DNS forwarding is not configured" fi diff --git a/src/op_mode/dynamic_dns.py b/src/op_mode/dynamic_dns.py index 405dd9f04..e4e5043d5 100755 --- a/src/op_mode/dynamic_dns.py +++ b/src/op_mode/dynamic_dns.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 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 @@ -23,8 +23,7 @@ import time from vyos.config import Config from vyos.util import call - -cache_file = r'/var/cache/ddclient/ddclient.cache' +cache_file = r'/run/ddclient/ddclient.cache' OUT_TMPL_SRC = """ {%- for entry in hosts -%} @@ -86,9 +85,9 @@ def show_status(): def update_ddns(): - call('systemctl stop ddclient') + call('systemctl stop ddclient.service') os.remove(cache_file) - call('systemctl start ddclient') + call('systemctl start ddclient.service') def main(): diff --git a/src/op_mode/flow_accounting_op.py b/src/op_mode/flow_accounting_op.py index 7f3ad7476..bf8c39fd6 100755 --- a/src/op_mode/flow_accounting_op.py +++ b/src/op_mode/flow_accounting_op.py @@ -70,13 +70,13 @@ def _is_host(host): # check if flow-accounting running def _uacctd_running(): - command = '/usr/bin/sudo /bin/systemctl status uacctd > /dev/null' + command = 'systemctl status uacctd.service > /dev/null' return run(command) == 0 # get list of interfaces def _get_ifaces_dict(): # run command to get ifaces list - out = cmd('/bin/ip link show', universal_newlines=True) + out = cmd('/bin/ip link show') # read output ifaces_out = out.splitlines() @@ -95,7 +95,6 @@ def _get_ifaces_dict(): def _get_flows_list(): # run command to get flows list out = cmd(f'/usr/bin/pmacct -s -O json -T flows -p {uacctd_pipefile}', - universal_newlines=True, message='Failed to get flows list') # read output @@ -196,7 +195,7 @@ if not _uacctd_running(): # restart pmacct daemon if cmd_args.action == 'restart': # run command to restart flow-accounting - cmd('/usr/bin/sudo /bin/systemctl restart uacctd', + cmd('systemctl restart uacctd.service', message='Failed to restart flow-accounting') # clear in-memory collected flows diff --git a/src/op_mode/generate_ssh_server_key.py b/src/op_mode/generate_ssh_server_key.py index f65d383c0..cbc9ef973 100755 --- a/src/op_mode/generate_ssh_server_key.py +++ b/src/op_mode/generate_ssh_server_key.py @@ -14,14 +14,13 @@ # 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 - +from sys import exit from vyos.util import ask_yes_no from vyos.util import cmd if not ask_yes_no('Do you really want to remove the existing SSH host keys?'): - sys.exit(0) + exit(0) -cmd('sudo rm -v /etc/ssh/ssh_host_*') -cmd('sudo dpkg-reconfigure openssh-server') -cmd('sudo systemctl restart ssh') +cmd('rm -v /etc/ssh/ssh_host_*') +cmd('dpkg-reconfigure openssh-server') +cmd('systemctl restart ssh.service') diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py index 0f3619411..4ab91384b 100755 --- a/src/op_mode/powerctrl.py +++ b/src/op_mode/powerctrl.py @@ -24,6 +24,7 @@ from vyos.util import ask_yes_no from vyos.util import cmd from vyos.util import call from vyos.util import run +from vyos.util import STDOUT systemd_sched_file = "/run/systemd/shutdown/scheduled" @@ -97,14 +98,14 @@ def execute_shutdown(time, reboot = True, ask=True): chk_vyatta_based_reboots() ### - out = cmd(f'/sbin/shutdown {action} now') + out = cmd(f'/sbin/shutdown {action} now', stderr=STDOUT) print(out.split(",",1)[0]) return elif len(time) == 1: # Assume the argument is just time ts = parse_time(time[0]) if ts: - cmd(f'/sbin/shutdown {action} {time[0]}') + cmd(f'/sbin/shutdown {action} {time[0]}', stderr=STDOUT) else: sys.exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0])) elif len(time) == 2: @@ -115,7 +116,7 @@ def execute_shutdown(time, reboot = True, ask=True): t = datetime.combine(ds, ts) td = t - datetime.now() t2 = 1 + int(td.total_seconds())//60 # Get total minutes - cmd('/sbin/shutdown {action} {t2}') + cmd('/sbin/shutdown {action} {t2}', stderr=STDOUT) else: if not ts: sys.exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0])) diff --git a/src/op_mode/reset_openvpn.py b/src/op_mode/reset_openvpn.py index 618cad5ea..dbd3eb4d1 100755 --- a/src/op_mode/reset_openvpn.py +++ b/src/op_mode/reset_openvpn.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 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 @@ -14,57 +14,18 @@ # 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 - -from time import sleep -from netifaces import interfaces -from vyos.util import process_running, cmd - -def get_config_name(intf): - cfg_file = r'/opt/vyatta/etc/openvpn/openvpn-{}.conf'.format(intf) - return cfg_file - -def get_pid_file(intf): - pid_file = r'/var/run/openvpn/{}.pid'.format(intf) - return pid_file - +from sys import argv, exit +from vyos.util import call if __name__ == '__main__': - if (len(sys.argv) < 1): - print("Must specify OpenVPN interface name!") - sys.exit(1) - - interface = sys.argv[1] - if os.path.isfile(get_config_name(interface)): - pidfile = '/var/run/openvpn/{}.pid'.format(interface) - if process_running(pidfile): - command = 'start-stop-daemon' - command += ' --stop' - command += ' --oknodo' - command += ' --quiet' - command += ' --pidfile ' + pidfile - cmd(command) - - # When stopping OpenVPN we need to wait for the 'old' interface to - # vanish from the Kernel, if it is not gone, OpenVPN will report: - # ERROR: Cannot ioctl TUNSETIFF vtun10: Device or resource busy (errno=16) - while interface in interfaces(): - sleep(0.250) # 250ms - - # re-start OpenVPN process - command = 'start-stop-daemon' - command += ' --start' - command += ' --oknodo' - command += ' --quiet' - command += ' --pidfile ' + get_pid_file(interface) - command += ' --exec /usr/sbin/openvpn' - # now pass arguments to openvpn binary - command += ' --' - command += ' --daemon openvpn-' + interface - command += ' --config ' + get_config_name(interface) + if (len(argv) < 1): + print('Must specify OpenVPN interface name!') + exit(1) - cmd(command) + interface = argv[1] + if os.path.isfile(f'/run/openvpn/{interface}.conf'): + call(f'systemctl restart openvpn@{interface}.service') else: - print("OpenVPN interface {} does not exist!".format(interface)) - sys.exit(1) + print(f'OpenVPN interface "{interface}" does not exist!') + exit(1) diff --git a/src/op_mode/restart_dhcp_relay.py b/src/op_mode/restart_dhcp_relay.py index 66dc435b3..af4fb2d15 100755 --- a/src/op_mode/restart_dhcp_relay.py +++ b/src/op_mode/restart_dhcp_relay.py @@ -39,7 +39,7 @@ if __name__ == '__main__': if not c.exists_effective('service dhcp-relay'): print("DHCP relay service not configured") else: - call('sudo systemctl restart isc-dhcp-relay.service') + call('systemctl restart isc-dhcp-server.service') sys.exit(0) elif args.ipv6: @@ -47,7 +47,7 @@ if __name__ == '__main__': if not c.exists_effective('service dhcpv6-relay'): print("DHCPv6 relay service not configured") else: - call('sudo systemctl restart isc-dhcpv6-relay.service') + call('systemctl restart isc-dhcp-server6.service') sys.exit(0) else: diff --git a/src/op_mode/show_dhcp.py b/src/op_mode/show_dhcp.py index a79033f69..c49e604b7 100755 --- a/src/op_mode/show_dhcp.py +++ b/src/op_mode/show_dhcp.py @@ -193,7 +193,7 @@ if __name__ == '__main__': sys.exit(0) # if dhcp server is down, inactive leases may still be shown as active, so warn the user. - if call('systemctl -q is-active isc-dhcpv4-server.service') != 0: + if call('systemctl -q is-active isc-dhcp-server.service') != 0: print("WARNING: DHCP server is configured but not started. Data may be stale.") if args.leases: diff --git a/src/op_mode/show_dhcpv6.py b/src/op_mode/show_dhcpv6.py index 18baa5517..d686defc0 100755 --- a/src/op_mode/show_dhcpv6.py +++ b/src/op_mode/show_dhcpv6.py @@ -179,7 +179,7 @@ if __name__ == '__main__': sys.exit(0) # if dhcp server is down, inactive leases may still be shown as active, so warn the user. - if call('systemctl -q is-active isc-dhcpv6-server.service') != 0: + if call('systemctl -q is-active isc-dhcp-server6.service') != 0: print("WARNING: DHCPv6 server is configured but not started. Data may be stale.") if args.leases: diff --git a/src/op_mode/version.py b/src/op_mode/version.py index fe6ecbae5..8599c958f 100755 --- a/src/op_mode/version.py +++ b/src/op_mode/version.py @@ -33,6 +33,7 @@ import vyos.limericks from vyos.util import cmd from vyos.util import call from vyos.util import run +from vyos.util import DEVNULL parser = argparse.ArgumentParser() @@ -82,7 +83,7 @@ if __name__ == '__main__': # Get hypervisor name, if any system_type = "bare metal" try: - hypervisor = cmd('hvinfo 2>/dev/null') + hypervisor = cmd('hvinfo',stderr=DEVNULL) system_type = "{0} guest".format(hypervisor) except OSError: # hvinfo returns 1 if it cannot detect any hypervisor diff --git a/src/system/keepalived-fifo.py b/src/system/keepalived-fifo.py index 2778deaab..7e2076820 100755 --- a/src/system/keepalived-fifo.py +++ b/src/system/keepalived-fifo.py @@ -87,7 +87,7 @@ class KeepalivedFifo: def _run_command(self, command): logger.debug("Running the command: {}".format(command)) try: - cmd(command, universal_newlines=True) + cmd(command) except OSError as err: logger.error(f'Unable to execute command "{command}": {err}') diff --git a/src/systemd/accel-ppp@.service b/src/systemd/accel-ppp@.service new file mode 100644 index 000000000..256112769 --- /dev/null +++ b/src/systemd/accel-ppp@.service @@ -0,0 +1,16 @@ +[Unit] +Description=Accel-PPP - High performance VPN server application for Linux +RequiresMountsFor=/run +ConditionPathExists=/run/accel-pppd/%i.conf +After=vyos-router.service + +[Service] +WorkingDirectory=/run/accel-pppd +ExecStart=/usr/sbin/accel-pppd -d -p /run/accel-pppd/%i.pid -c /run/accel-pppd/%i.conf +ExecReload=/bin/kill -SIGUSR1 $MAINPID +PIDFile=/run/accel-pppd/%i.pid +Type=forking +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/ddclient.service b/src/systemd/ddclient.service new file mode 100644 index 000000000..a4d55827a --- /dev/null +++ b/src/systemd/ddclient.service @@ -0,0 +1,14 @@ +[Unit] +Description=Dynamic DNS Update Client +RequiresMountsFor=/run +ConditionPathExists=/run/ddclient/ddclient.conf +After=vyos-router.service + +[Service] +WorkingDirectory=/run/ddclient +Type=forking +PIDFile=/run/ddclient/ddclient.pid +ExecStart=/usr/sbin/ddclient -cache /run/ddclient/ddclient.cache -pid /run/ddclient/ddclient.pid -file /run/ddclient/ddclient.conf + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/isc-dhcp-relay.service b/src/systemd/isc-dhcp-relay.service new file mode 100644 index 000000000..ebf4d234e --- /dev/null +++ b/src/systemd/isc-dhcp-relay.service @@ -0,0 +1,14 @@ +[Unit] +Description=ISC DHCP IPv4 relay +Documentation=man:dhcrelay(8) +Wants=network-online.target +ConditionPathExists=/run/dhcp-relay/dhcp.conf +After=vyos-router.service + +[Service] +WorkingDirectory=/run/dhcp-relay +EnvironmentFile=/run/dhcp-relay/dhcp.conf +ExecStart=/usr/sbin/dhcrelay -d -4 $OPTIONS + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/isc-dhcp-relay6.service b/src/systemd/isc-dhcp-relay6.service new file mode 100644 index 000000000..a477618b1 --- /dev/null +++ b/src/systemd/isc-dhcp-relay6.service @@ -0,0 +1,14 @@ +[Unit] +Description=ISC DHCP IPv6 relay +Documentation=man:dhcrelay(8) +Wants=network-online.target +ConditionPathExists=/run/dhcp-relay/dhcpv6.conf +After=vyos-router.service + +[Service] +WorkingDirectory=/run/dhcp-relay +EnvironmentFile=/run/dhcp-relay/dhcpv6.conf +ExecStart=/usr/sbin/dhcrelay -d -6 $OPTIONS + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/isc-dhcp-server.service b/src/systemd/isc-dhcp-server.service new file mode 100644 index 000000000..d848e3df1 --- /dev/null +++ b/src/systemd/isc-dhcp-server.service @@ -0,0 +1,19 @@ +[Unit] +Description=ISC DHCP IPv4 server +Documentation=man:dhcpd(8) +RequiresMountsFor=/run +ConditionPathExists=/run/dhcp-server/dhcpd.conf +After=vyos-router.service + +[Service] +WorkingDirectory=/run/dhcp-server +# The leases files need to be root:vyattacfg even when dropping privileges +ExecStart=/bin/sh -ec '\ + CONFIG_FILE=/run/dhcp-server/dhcpd.conf; \ + [ -e /config/dhcpd.leases ] || touch /config/dhcpd.leases; \ + chown root:vyattacfg /config/dhcpd.leases; \ + chmod 664 /config/dhcpd.leases; \ + exec /usr/sbin/dhcpd -user nobody -group nogroup -f -4 -pf /run/dhcp-server/dhcpd.pid -cf $CONFIG_FILE -lf /config/dhcpd.leases' + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/isc-dhcp-server6.service b/src/systemd/isc-dhcp-server6.service new file mode 100644 index 000000000..743f16840 --- /dev/null +++ b/src/systemd/isc-dhcp-server6.service @@ -0,0 +1,18 @@ +[Unit] +Description=ISC DHCP IPv6 server +Documentation=man:dhcpd(8) +RequiresMountsFor=/run +ConditionPathExists=/run/dhcp-server/dhcpd.conf +After=vyos-router.service + +[Service] +WorkingDirectory=/run/dhcp-server +# The leases files need to be root:vyattacfg even when dropping privileges +ExecStart=/bin/sh -ec '\ + [ -e /config/dhcpdv6.leases ] || touch /config/dhcpdv6.leases; \ + chown root:vyattacfg /config/dhcpdv6.leases; \ + chmod 664 /config/dhcpdv6.leases; \ + exec /usr/sbin/dhcpd -user nobody -group nogroup -f -6 -pf /run/dhcp-server/dhcpdv6.pid -cf /run/dhcp-server/dhcpdv6.conf -lf /config/dhcpdv6.leases' + +[Install] +WantedBy=multi-user.target diff --git a/src/etc/systemd/system/ppp@.service b/src/systemd/ppp@.service index d271efb41..bb4622034 100644 --- a/src/etc/systemd/system/ppp@.service +++ b/src/systemd/ppp@.service @@ -1,6 +1,6 @@ [Unit] Description=Dialing PPP connection %I -After=network.target +After=vyos-router.service [Service] ExecStart=/usr/sbin/pppd call %I nodetach nolog diff --git a/src/systemd/tftpd@.service b/src/systemd/tftpd@.service index e5c289466..266bc0962 100644 --- a/src/systemd/tftpd@.service +++ b/src/systemd/tftpd@.service @@ -1,6 +1,6 @@ [Unit] Description=TFTP server -After=network.target +After=vyos-router.service RequiresMountsFor=/run [Service] |