diff options
Diffstat (limited to 'src/conf_mode')
55 files changed, 1381 insertions, 1782 deletions
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/arp.py b/src/conf_mode/arp.py index 3daa892d7..fde7dc521 100755 --- a/src/conf_mode/arp.py +++ b/src/conf_mode/arp.py @@ -22,7 +22,7 @@ import re import syslog as sl from vyos.config import Config -from vyos.util import run +from vyos.util import call from vyos import ConfigError arp_cmd = '/usr/sbin/arp' @@ -82,12 +82,12 @@ def generate(c): def apply(c): for ip_addr in c['remove']: sl.syslog(sl.LOG_NOTICE, "arp -d " + ip_addr) - run(f'{arp_cmd} -d {ip_addr} >/dev/null 2>&1') + call(f'{arp_cmd} -d {ip_addr} >/dev/null 2>&1') for ip_addr in c['update']: sl.syslog(sl.LOG_NOTICE, "arp -s " + ip_addr + " " + c['update'][ip_addr]) updated = c['update'][ip_addr] - run(f'{arp_cmd} -s {ip_addr} {updated}') + call(f'{arp_cmd} -s {ip_addr} {updated}') if __name__ == '__main__': diff --git a/src/conf_mode/bcast_relay.py b/src/conf_mode/bcast_relay.py index f6d90776c..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 run +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 - run('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 - run('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 1d6d4c6e3..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.template import render +from vyos.util import call from vyos import ConfigError -from vyos.util import run -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: - run('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 - run('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 69aebe2f4..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 run +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 - run('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) - - run('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 a67deb6c7..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 run +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: - run('sudo systemctl restart isc-dhcpv6-relay.service') + call('systemctl restart isc-dhcp-relay6.service') else: # DHCPv6 relay support is removed in the commit - run('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 003e80915..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 run - -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: - return None - - if dhcpv6['disabled']: - print('Warning: DHCPv6 server will be deactivated because it is disabled') + if not dhcpv6 or dhcpv6['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 - run('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) - run('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 5dc599425..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 run +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 - run("systemctl stop pdns-recursor") + call("systemctl stop pdns-recursor.service") if os.path.isfile(config_file): os.unlink(config_file) else: - run("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 b9163f7b3..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 run +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']: - run('/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: - run('/etc/init.d/ddclient restart') + call('systemctl restart ddclient.service') return None diff --git a/src/conf_mode/firewall_options.py b/src/conf_mode/firewall_options.py index 90f004bc4..0b800f48f 100755 --- a/src/conf_mode/firewall_options.py +++ b/src/conf_mode/firewall_options.py @@ -21,7 +21,7 @@ import copy from vyos.config import Config from vyos import ConfigError -from vyos.util import run +from vyos.util import call default_config_data = { @@ -87,19 +87,19 @@ def apply(tcp): target = 'VYOS_FW_OPTIONS' # always cleanup iptables - run('iptables --table mangle --delete FORWARD --jump {} >&/dev/null'.format(target)) - run('iptables --table mangle --flush {} >&/dev/null'.format(target)) - run('iptables --table mangle --delete-chain {} >&/dev/null'.format(target)) + call('iptables --table mangle --delete FORWARD --jump {} >&/dev/null'.format(target)) + call('iptables --table mangle --flush {} >&/dev/null'.format(target)) + call('iptables --table mangle --delete-chain {} >&/dev/null'.format(target)) # always cleanup ip6tables - run('ip6tables --table mangle --delete FORWARD --jump {} >&/dev/null'.format(target)) - run('ip6tables --table mangle --flush {} >&/dev/null'.format(target)) - run('ip6tables --table mangle --delete-chain {} >&/dev/null'.format(target)) + call('ip6tables --table mangle --delete FORWARD --jump {} >&/dev/null'.format(target)) + call('ip6tables --table mangle --flush {} >&/dev/null'.format(target)) + call('ip6tables --table mangle --delete-chain {} >&/dev/null'.format(target)) # Setup new iptables rules if tcp['new_chain4']: - run('iptables --table mangle --new-chain {} >&/dev/null'.format(target)) - run('iptables --table mangle --append FORWARD --jump {} >&/dev/null'.format(target)) + call('iptables --table mangle --new-chain {} >&/dev/null'.format(target)) + call('iptables --table mangle --append FORWARD --jump {} >&/dev/null'.format(target)) for opts in tcp['intf_opts']: intf = opts['intf'] @@ -111,13 +111,13 @@ def apply(tcp): # adjust TCP MSS per interface if mss: - run('iptables --table mangle --append {} --out-interface {} --protocol tcp ' \ + call('iptables --table mangle --append {} --out-interface {} --protocol tcp ' '--tcp-flags SYN,RST SYN --jump TCPMSS --set-mss {} >&/dev/null'.format(target, intf, mss)) # Setup new ip6tables rules if tcp['new_chain6']: - run('ip6tables --table mangle --new-chain {} >&/dev/null'.format(target)) - run('ip6tables --table mangle --append FORWARD --jump {} >&/dev/null'.format(target)) + call('ip6tables --table mangle --new-chain {} >&/dev/null'.format(target)) + call('ip6tables --table mangle --append FORWARD --jump {} >&/dev/null'.format(target)) for opts in tcp['intf_opts']: intf = opts['intf'] @@ -129,7 +129,7 @@ def apply(tcp): # adjust TCP MSS per interface if mss: - run('ip6tables --table mangle --append {} --out-interface {} --protocol tcp ' + call('ip6tables --table mangle --append {} --out-interface {} --protocol tcp ' '--tcp-flags SYN,RST SYN --jump TCPMSS --set-mss {} >&/dev/null'.format(target, intf, mss)) 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 690d1e030..dd5819f9f 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -33,7 +33,9 @@ import vyos.hostsd_client from vyos.config import Config from vyos import ConfigError -from vyos.util import cmd, run +from vyos.util import cmd +from vyos.util import call +from vyos.util import run default_config_data = { @@ -157,21 +159,21 @@ def apply(config): # rsyslog runs into a race condition at boot time with systemd # restart rsyslog only if the hostname changed. hostname_old = cmd('hostnamectl --static') - cmd(f'hostnamectl set-hostname --static {hostname_new}') + call(f'hostnamectl set-hostname --static {hostname_new}') # Restart services that use the hostname if hostname_new != hostname_old: - run("systemctl restart rsyslog.service") + call("systemctl restart rsyslog.service") # If SNMP is running, restart it too - ret = run("pgrep snmpd > /dev/null") + ret = run("pgrep snmpd") if ret == 0: - run("systemctl restart snmpd.service") + call("systemctl restart snmpd.service") # restart pdns if it is used - ret = run('/usr/bin/rec_control ping >/dev/null 2>&1') + ret = run('/usr/bin/rec_control ping') if ret == 0: - run('/etc/init.d/pdns-recursor restart >/dev/null') + call('systemctl restart pdns-recursor.service') return None diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py index 91b8aa34b..26f4aea7f 100755 --- a/src/conf_mode/http-api.py +++ b/src/conf_mode/http-api.py @@ -24,7 +24,8 @@ from copy import deepcopy import vyos.defaults from vyos.config import Config from vyos import ConfigError -from vyos.util import cmd, run +from vyos.util import cmd +from vyos.util import call config_file = '/etc/vyos/http-api.conf' @@ -91,9 +92,9 @@ def generate(http_api): def apply(http_api): if http_api is not None: - run('sudo systemctl restart vyos-http-api.service') + call('sudo systemctl restart vyos-http-api.service') else: - run('sudo systemctl stop vyos-http-api.service') + call('sudo systemctl stop vyos-http-api.service') for dep in dependencies: cmd(f'{vyos_conf_scripts_dir}/{dep}', raising=ConfigError) diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index 777792229..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 run +from vyos.util import call +from vyos.template import render config_file = '/etc/nginx/sites-available/default' @@ -133,26 +132,18 @@ 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 def apply(https): if https is not None: - run('sudo systemctl restart nginx.service') + call('sudo systemctl restart nginx.service') else: - run('sudo systemctl stop nginx.service') + call('sudo systemctl stop nginx.service') if __name__ == '__main__': try: diff --git a/src/conf_mode/igmp_proxy.py b/src/conf_mode/igmp_proxy.py index abe473530..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 run +from vyos.util import call +from vyos.template import render config_file = r'/etc/igmpproxy.conf' @@ -116,26 +115,17 @@ 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): if igmp_proxy is None or igmp_proxy['disable']: # IGMP Proxy support is removed in the commit - run('sudo systemctl stop igmpproxy.service') + call('sudo systemctl stop igmpproxy.service') if os.path.exists(config_file): os.unlink(config_file) else: - run('systemctl restart igmpproxy.service') + call('systemctl restart igmpproxy.service') return None diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py index 6a002bc06..fd1f218d1 100755 --- a/src/conf_mode/interfaces-bonding.py +++ b/src/conf_mode/interfaces-bonding.py @@ -24,7 +24,8 @@ from vyos.ifconfig import BondIf from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config from vyos.configdict import list_diff, vlan_to_dict from vyos.config import Config -from vyos.util import run, is_bridge_member +from vyos.util import is_bridge_member +from vyos.util import call from vyos import ConfigError default_config_data = { @@ -91,7 +92,7 @@ def get_config(): if not os.path.isfile('/sys/class/net/bonding_masters'): import syslog syslog.syslog(syslog.LOG_NOTICE, "loading bonding kernel module") - if run('modprobe bonding max_bonds=0 miimon=250') != 0: + if call('modprobe bonding max_bonds=0 miimon=250') != 0: syslog.syslog(syslog.LOG_NOTICE, "failed loading bonding kernel module") raise ConfigError("failed loading bonding kernel module") @@ -398,32 +399,20 @@ def apply(bond): # update interface description used e.g. within SNMP b.set_alias(bond['description']) - # get DHCP config dictionary and update values - opt = b.get_dhcp_options() - if bond['dhcp_client_id']: - opt['client_id'] = bond['dhcp_client_id'] + b.dhcp.v4.options['client_id'] = bond['dhcp_client_id'] if bond['dhcp_hostname']: - opt['hostname'] = bond['dhcp_hostname'] + b.dhcp.v4.options['hostname'] = bond['dhcp_hostname'] if bond['dhcp_vendor_class_id']: - opt['vendor_class_id'] = bond['dhcp_vendor_class_id'] - - # store DHCP config dictionary - used later on when addresses are aquired - b.set_dhcp_options(opt) - - # get DHCPv6 config dictionary and update values - opt = b.get_dhcpv6_options() + b.dhcp.v4.options['vendor_class_id'] = bond['dhcp_vendor_class_id'] if bond['dhcpv6_prm_only']: - opt['dhcpv6_prm_only'] = True + b.dhcp.v6.options['dhcpv6_prm_only'] = True if bond['dhcpv6_temporary']: - opt['dhcpv6_temporary'] = True - - # store DHCPv6 config dictionary - used later on when addresses are required - b.set_dhcpv6_options(opt) + b.dhcp.v6.options['dhcpv6_temporary'] = True # ignore link state changes b.set_link_detect(bond['disable_link_detect']) diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py index 79247ee51..93c6db97e 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -300,32 +300,20 @@ def apply(bridge): # update interface description used e.g. within SNMP br.set_alias(bridge['description']) - # get DHCP config dictionary and update values - opt = br.get_dhcp_options() - if bridge['dhcp_client_id']: - opt['client_id'] = bridge['dhcp_client_id'] + br.dhcp.v4.options['client_id'] = bridge['dhcp_client_id'] if bridge['dhcp_hostname']: - opt['hostname'] = bridge['dhcp_hostname'] + br.dhcp.v4.options['hostname'] = bridge['dhcp_hostname'] if bridge['dhcp_vendor_class_id']: - opt['vendor_class_id'] = bridge['dhcp_vendor_class_id'] - - # store DHCPv6 config dictionary - used later on when addresses are aquired - br.set_dhcp_options(opt) - - # get DHCPv6 config dictionary and update values - opt = br.get_dhcpv6_options() + br.dhcp.v4.options['vendor_class_id'] = bridge['dhcp_vendor_class_id'] if bridge['dhcpv6_prm_only']: - opt['dhcpv6_prm_only'] = True + br.dhcp.v6.options['dhcpv6_prm_only'] = True if bridge['dhcpv6_temporary']: - opt['dhcpv6_temporary'] = True - - # store DHCPv6 config dictionary - used later on when addresses are aquired - br.set_dhcpv6_options(opt) + br.dhcp.v6.options['dhcpv6_temporary'] = True # assign/remove VRF br.set_vrf(bridge['vrf']) diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index 15e9b4185..5a977d797 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -301,32 +301,20 @@ def apply(eth): # update interface description used e.g. within SNMP e.set_alias(eth['description']) - # get DHCP config dictionary and update values - opt = e.get_dhcp_options() - if eth['dhcp_client_id']: - opt['client_id'] = eth['dhcp_client_id'] + e.dhcp.v4.options['client_id'] = eth['dhcp_client_id'] if eth['dhcp_hostname']: - opt['hostname'] = eth['dhcp_hostname'] + e.dhcp.v4.options['hostname'] = eth['dhcp_hostname'] if eth['dhcp_vendor_class_id']: - opt['vendor_class_id'] = eth['dhcp_vendor_class_id'] - - # store DHCP config dictionary - used later on when addresses are aquired - e.set_dhcp_options(opt) - - # get DHCPv6 config dictionary and update values - opt = e.get_dhcpv6_options() + e.dhcp.v4.options['vendor_class_id'] = eth['dhcp_vendor_class_id'] if eth['dhcpv6_prm_only']: - opt['dhcpv6_prm_only'] = True + e.dhcp.v6.options['dhcpv6_prm_only'] = True if eth['dhcpv6_temporary']: - opt['dhcpv6_temporary'] = True - - # store DHCPv6 config dictionary - used later on when addresses are aquired - e.set_dhcpv6_options(opt) + e.dhcp.v6.options['dhcpv6_temporary'] = True # ignore link state changes e.set_link_detect(eth['disable_link_detect']) diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py index 0400cb849..11ba9acdd 100755 --- a/src/conf_mode/interfaces-l2tpv3.py +++ b/src/conf_mode/interfaces-l2tpv3.py @@ -22,7 +22,8 @@ from copy import deepcopy from vyos.config import Config from vyos.ifconfig import L2TPv3If, Interface from vyos import ConfigError -from vyos.util import run, is_bridge_member +from vyos.util import call +from vyos.util import is_bridge_member from netifaces import interfaces default_config_data = { @@ -51,7 +52,7 @@ def check_kmod(): modules = ['l2tp_eth', 'l2tp_netlink', 'l2tp_ip', 'l2tp_ip6'] for module in modules: if not os.path.exists(f'/sys/module/{module}'): - if run(f'modprobe {module}') != 0: + if call(f'modprobe {module}') != 0: raise ConfigError(f'Loading Kernel module {module} failed') def get_config(): diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index e9b40bb38..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'): @@ -428,6 +463,7 @@ def get_config(): # Minimum required TLS version if conf.exists('tls tls-version-min'): openvpn['tls_version_min'] = conf.return_value('tls tls-version-min') + openvpn['tls'] = True if conf.exists('shared-secret-key-file'): openvpn['shared_secret_file'] = conf.return_value('shared-secret-key-file') @@ -440,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): @@ -489,7 +545,11 @@ def verify(openvpn): # OpenVPN site-to-site - VERIFY # if openvpn['mode'] == 'site-to-site': - if not (openvpn['local_address'] or openvpn['bridge_member']): + if openvpn['ncp_ciphers']: + raise ConfigError('encryption ncp-ciphers cannot be specified in site-to-site mode, only server or client') + + if openvpn['mode'] == 'site-to-site' and not openvpn['bridge_member']: + if not openvpn['local_address']: raise ConfigError('Must specify "local-address" or "bridge member interface"') for host in openvpn['remote_host']: @@ -506,15 +566,10 @@ def verify(openvpn): if openvpn['local_address'] == openvpn['local_host']: raise ConfigError('"local-address" cannot be the same as "local-host"') - if openvpn['ncp_ciphers']: - raise ConfigError('encryption ncp-ciphers cannot be specified in site-to-site mode, only server or client') - else: + # checks for client-server or site-to-site bridged if openvpn['local_address'] or openvpn['remote_address']: - raise ConfigError('Cannot specify "local-address" or "remote-address" in client-server mode') - - elif openvpn['bridge_member']: - raise ConfigError('Cannot specify "local-address" or "remote-address" in bridge mode') + raise ConfigError('Cannot specify "local-address" or "remote-address" in client-server or bridge mode') # # OpenVPN server mode - VERIFY @@ -535,9 +590,41 @@ 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" option in server mode') + 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 @@ -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 407547175..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_file, 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': '', @@ -155,14 +155,12 @@ def verify(pppoe): if vrf_name and vrf_name not in interfaces(): raise ConfigError(f'VRF {vrf_name} does not exist') + if pppoe['on_demand'] and pppoe['vrf']: + raise ConfigError('On-demand dialing and VRF can not be used at the same time') + 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) - # set up configuration file path variables where our templates will be # rendered into intf = pppoe['intf'] @@ -192,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 @@ -240,7 +224,7 @@ def apply(pppoe): cmd(f'systemctl start ppp@{intf}.service') # make logfile owned by root / vyattacfg - chown_file(pppoe['logfile'], 'root', 'vyattacfg') + chown(pppoe['logfile'], 'root', 'vyattacfg') return None diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py index 50b5a12a0..655006146 100755 --- a/src/conf_mode/interfaces-pseudo-ethernet.py +++ b/src/conf_mode/interfaces-pseudo-ethernet.py @@ -51,8 +51,8 @@ default_config_data = { 'ipv6_forwarding': 1, 'ipv6_dup_addr_detect': 1, 'intf': '', - 'link': '', - 'link_changed': False, + 'source_interface': '', + 'source_interface_changed': False, 'mac': '', 'mode': 'private', 'vif_s': [], @@ -166,12 +166,12 @@ def get_config(): if conf.exists('ipv6 dup-addr-detect-transmits'): peth['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits')) - # Lower link device - if conf.exists(['link']): - peth['link'] = conf.return_value(['link']) - tmp = conf.return_effective_value(['link']) - if tmp != peth['link']: - peth['link_changed'] = True + # Physical interface + if conf.exists(['source-interface']): + peth['source_interface'] = conf.return_value(['source-interface']) + tmp = conf.return_effective_value(['source-interface']) + if tmp != peth['source_interface']: + peth['source_interface_changed'] = True # Media Access Control (MAC) address if conf.exists(['mac']): @@ -227,10 +227,10 @@ def verify(peth): 'is a member of bridge "{1}"!'.format(interface, bridge)) return None - if not peth['link']: + if not peth['source_interface']: raise ConfigError('Link device must be set for virtual ethernet {}'.format(peth['intf'])) - if not peth['link'] in interfaces(): + if not peth['source_interface'] in interfaces(): raise ConfigError('Pseudo-ethernet source interface does not exist') vrf_name = peth['vrf'] @@ -253,12 +253,12 @@ def apply(peth): p.remove() return None - elif peth['link_changed']: + elif peth['source_interface_changed']: # Check if MACVLAN interface already exists. Parameters like the - # underlaying link device can not be changed on the fly and the - # interface needs to be recreated from the bottom. + # underlaying source-interface device can not be changed on the fly + # and the interface needs to be recreated from the bottom. # - # link_changed also means - the interface was not present in the + # source_interface_changed also means - the interface was not present in the # beginning and is newly created if peth['intf'] in interfaces(): p = MACVLANIf(peth['intf']) @@ -269,7 +269,7 @@ def apply(peth): conf = deepcopy(MACVLANIf.get_config()) # Assign MACVLAN instance configuration parameters to config dict - conf['link'] = peth['link'] + conf['source_interface'] = peth['source_interface'] conf['mode'] = peth['mode'] # It is safe to "re-create" the interface always, there is a sanity check @@ -281,32 +281,20 @@ def apply(peth): # update interface description used e.g. within SNMP p.set_alias(peth['description']) - # get DHCP config dictionary and update values - opt = p.get_dhcp_options() - if peth['dhcp_client_id']: - opt['client_id'] = peth['dhcp_client_id'] + p.dhcp.v4.options['client_id'] = peth['dhcp_client_id'] if peth['dhcp_hostname']: - opt['hostname'] = peth['dhcp_hostname'] + p.dhcp.v4.options['hostname'] = peth['dhcp_hostname'] if peth['dhcp_vendor_class_id']: - opt['vendor_class_id'] = peth['dhcp_vendor_class_id'] - - # store DHCP config dictionary - used later on when addresses are aquired - p.set_dhcp_options(opt) - - # get DHCPv6 config dictionary and update values - opt = p.get_dhcpv6_options() + p.dhcp.v4.options['vendor_class_id'] = peth['dhcp_vendor_class_id'] if peth['dhcpv6_prm_only']: - opt['dhcpv6_prm_only'] = True + p.dhcp.v6.options['dhcpv6_prm_only'] = True if peth['dhcpv6_temporary']: - opt['dhcpv6_temporary'] = True - - # store DHCPv6 config dictionary - used later on when addresses are aquired - p.set_dhcpv6_options(opt) + p.dhcp.v6.options['dhcpv6_temporary'] = True # ignore link state changes p.set_link_detect(peth['disable_link_detect']) diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index 646e61c53..c51048aeb 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -26,21 +26,68 @@ from vyos.ifconfig.afi import IP4, IP6 from vyos.configdict import list_diff from vyos.validate import is_ipv4, is_ipv6 from vyos import ConfigError +from vyos.dicts import FixedDict +class ConfigurationState(Config): + """ + The current API require a dict to be generated by get_config() + which is then consumed by verify(), generate() and apply() -class FixedDict(dict): - def __init__ (self, **options): - self._allowed = options.keys() - super().__init__(**options) + ConfiguartionState is an helper class wrapping Config and providing + an common API to this dictionary structure - def __setitem__ (self, k, v): - if k not in self._allowed: - raise ConfigError(f'Option "{k}" has no defined default') - super().__setitem__(k, v) + Its to_dict() function return a dictionary containing three fields, + each a dict, called options, changes, actions. + options: + + contains the configuration options for the dict and its value + {'options': {'commment': 'test'}} will be set if + 'set interface dummy dum1 description test' was used and + the key 'commment' is used to index the description info. + + changes: + + per key, let us know how the data was modified using one of the action + a special key called 'section' is used to indicate what happened to the + section. for example: + + 'set interface dummy dum1 description test' when no interface was setup + will result in the following changes + {'changes': {'section': 'create', 'comment': 'create'}} + + on an existing interface, depending if there was a description + 'set interface dummy dum1 description test' will result in one of + {'changes': {'comment': 'create'}} (not present before) + {'changes': {'comment': 'static'}} (unchanged) + {'changes': {'comment': 'modify'}} (changed from half) + + and 'delete interface dummy dummy1 description' will result in: + {'changes': {'comment': 'delete'}} + + actions: + + for each action list the configuration key which were changes + in our example if we added the 'description' and added an IP we would have + {'actions': { 'create': ['comment'], 'modify': ['addresses-add']}} + + the actions are: + 'create': it did not exist previously and was created + 'modify': it did exist previously but its content changed + 'static': it did exist and did not change + 'delete': it was present but was removed from the configuration + 'absent': it was not and is not present + which for each field represent how it was modified since the last commit + """ -class ConfigurationState (Config): def __init__ (self, section, default): + """ + initialise the class for a given configuration path: + + >>> conf = ConfigurationState('interfaces ethernet eth1') + all further references to get_value(s) and get_effective(s) + will be for this part of the configuration (eth1) + """ super().__init__() self.section = section self.default = deepcopy(default) @@ -61,6 +108,15 @@ class ConfigurationState (Config): self.changes['section'] = 'create' def _act(self, section): + """ + Returns for a given configuration field determine what happened to it + + 'create': it did not exist previously and was created + 'modify': it did exist previously but its content changed + 'static': it did exist and did not change + 'delete': it was present but was removed from the configuration + 'absent': it was not and is not present + """ if self.exists(section): if self.exists_effective(section): if self.return_value(section) != self.return_effective_value(section): @@ -89,24 +145,71 @@ class ConfigurationState (Config): self.options[name] = value def get_value(self, name, key, default=None): + """ + >>> conf.get_value('comment', 'description') + will place the string of 'interface dummy description test' + into the dictionnary entry 'comment' using Config.return_value + (the data in the configuration to apply) + """ if self._action(name, key) in ('delete', 'absent'): return return self._get(name, key, default, self.return_value) def get_values(self, name, key, default=None): + """ + >>> conf.get_values('addresses-add', 'address') + will place a list made of the IP present in 'interface dummy dum1 address' + into the dictionnary entry 'addr' using Config.return_values + (the data in the configuration to apply) + """ if self._action(name, key) in ('delete', 'absent'): return return self._get(name, key, default, self.return_values) def get_effective(self, name, key, default=None): + """ + >>> conf.get_value('comment', 'description') + will place the string of 'interface dummy description test' + into the dictionnary entry 'comment' using Config.return_effective_value + (the data in the configuration to apply) + """ self._action(name, key) return self._get(name, key, default, self.return_effective_value) def get_effectives(self, name, key, default=None): + """ + >>> conf.get_effectives('addresses-add', 'address') + will place a list made of the IP present in 'interface ethernet eth1 address' + into the dictionnary entry 'addresses-add' using Config.return_effectives_value + (the data in the un-modified configuration) + """ self._action(name, key) return self._get(name, key, default, self.return_effectives_value) def load(self, mapping): + """ + load will take a dictionary defining how we wish the configuration + to be parsed and apply this definition to set the data. + + >>> mapping = { + 'addresses-add' : ('address', True, None), + 'comment' : ('description', False, 'auto'), + } + >>> conf.load(mapping) + + mapping is a dictionary where each key represents the name we wish + to have (such as 'addresses-add'), with a list a content representing + how the data should be parsed: + - the configuration section name + such as 'address' under 'interface ethernet eth1' + - boolean indicating if this data can have multiple values + for 'address', True, as multiple IPs can be set + for 'description', False, as it is a single string + - default represent the default value if absent from the configuration + 'None' indicate that no default should be set if the configuration + does not have the configuration section + + """ for local_name, (config_name, multiple, default) in mapping.items(): if multiple: self.get_values(local_name, config_name, default) @@ -114,12 +217,21 @@ class ConfigurationState (Config): self.get_value(local_name, config_name, default) def remove_default (self,*options): + """ + remove all the values which were not changed from the default + """ for option in options: if self.exists(option) and self_return_value(option) != self.default[option]: continue del self.options[option] def to_dict (self): + """ + provide a dictionary with the generated data for the configuration + options: the configuration value for the key + changes: per key how they changed from the previous configuration + actions: per changes all the options which were changed + """ # as we have to use a dict() for the API for verify and apply the options return { 'options': self.options, @@ -203,7 +315,8 @@ def get_class (options): } kls = dispatch[options['type']] - if options['type'] == 'gre' and not options['remote']: + if options['type'] == 'gre' and not options['remote'] \ + and not options['key'] and not options['multicast']: # will use GreTapIf on GreIf deletion but it does not matter return GRETapIf elif options['type'] == 'sit' and options['6rd-prefix']: @@ -471,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-vxlan.py b/src/conf_mode/interfaces-vxlan.py index b9bfb242a..6639a9b0d 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -42,7 +42,8 @@ default_config_data = { 'ipv6_eui64_prefix': '', 'ipv6_forwarding': 1, 'ipv6_dup_addr_detect': 1, - 'link': '', + 'source_address': '', + 'source_interface': '', 'mtu': 1450, 'remote': '', 'remote_port': 8472, # The Linux implementation of VXLAN pre-dates @@ -124,9 +125,13 @@ def get_config(): if conf.exists('ipv6 dup-addr-detect-transmits'): vxlan['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits')) + # VXLAN source address + if conf.exists('source-address'): + vxlan['source_address'] = conf.return_value('source-address') + # VXLAN underlay interface - if conf.exists('link'): - vxlan['link'] = conf.return_value('link') + if conf.exists('source-interface'): + vxlan['source_interface'] = conf.return_value('source-interface') # Maximum Transmission Unit (MTU) if conf.exists('mtu'): @@ -162,18 +167,22 @@ def verify(vxlan): print('WARNING: RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU') if vxlan['group']: - if not vxlan['link']: + if not vxlan['source_interface']: raise ConfigError('Multicast VXLAN requires an underlaying interface ') - if not vxlan['link'] in interfaces(): + + if not vxlan['source_interface'] in interfaces(): raise ConfigError('VXLAN source interface does not exist') + if not (vxlan['group'] or vxlan['remote'] or vxlan['source_address']): + raise ConfigError('Group, remote or source-address must be configured') + if not vxlan['vni']: raise ConfigError('Must configure VNI for VXLAN') - if vxlan['link']: + if vxlan['source_interface']: # VXLAN adds a 50 byte overhead - we need to check the underlaying MTU # if our configured MTU is at least 50 bytes less - underlay_mtu = int(Interface(vxlan['link']).get_mtu()) + underlay_mtu = int(Interface(vxlan['source_interface']).get_mtu()) if underlay_mtu < (vxlan['mtu'] + 50): raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \ 'MTU is to small ({})'.format(underlay_mtu)) @@ -202,7 +211,8 @@ def apply(vxlan): # Assign VXLAN instance configuration parameters to config dict conf['vni'] = vxlan['vni'] conf['group'] = vxlan['group'] - conf['dev'] = vxlan['link'] + conf['src_address'] = vxlan['source_address'] + conf['src_interface'] = vxlan['source_interface'] conf['remote'] = vxlan['remote'] conf['port'] = vxlan['remote_port'] diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index 54121a6c1..8bf81c747 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -14,173 +14,181 @@ # 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 +from sys import exit from copy import deepcopy from netifaces import interfaces -from vyos import ConfigError from vyos.config import Config from vyos.configdict import list_diff -from vyos.util import run, is_bridge_member from vyos.ifconfig import WireGuardIf +from vyos.util import chown, is_bridge_member, chmod_750 +from vyos.util import call +from vyos import ConfigError kdir = r'/config/auth/wireguard' +default_config_data = { + 'intfc': '', + 'address': [], + 'address_remove': [], + 'description': '', + 'lport': None, + 'deleted': False, + 'disable': False, + 'fwmark': 0x00, + 'mtu': 1420, + 'peer': [], + 'peer_remove': [], # stores public keys of peers to remove + 'pk': f'{kdir}/default/private.key', + 'vrf': '' +} + def _check_kmod(): - if not os.path.exists('/sys/module/wireguard'): - if run('modprobe wireguard') != 0: - raise ConfigError("modprobe wireguard failed") + modules = ['wireguard'] + for module in modules: + if not os.path.exists(f'/sys/module/{module}'): + if call(f'modprobe {module}') != 0: + raise ConfigError(f'Loading Kernel module {module} failed') def _migrate_default_keys(): if os.path.exists(f'{kdir}/private.key') and not os.path.exists(f'{kdir}/default/private.key'): - old_umask = os.umask(0o027) location = f'{kdir}/default' - run(f'sudo mkdir -p {location}') - run(f'sudo chgrp vyattacfg {location}') - run(f'sudo chmod 750 {location}') + if not os.path.exists(location): + os.makedirs(location) + + chown(location, 'root', 'vyattacfg') + chmod_750(location) os.rename(f'{kdir}/private.key', f'{location}/private.key') os.rename(f'{kdir}/public.key', f'{location}/public.key') - os.umask(old_umask) def get_config(): - c = Config() - if not c.exists(['interfaces', 'wireguard']): - return None + conf = Config() + base = ['interfaces', 'wireguard'] # determine tagNode instance if 'VYOS_TAGNODE_VALUE' not in os.environ: raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - dflt_cnf = { - 'intfc': '', - 'addr': [], - 'addr_remove': [], - 'descr': '', - 'lport': None, - 'delete': False, - 'state': 'up', - 'fwmark': 0x00, - 'mtu': 1420, - 'peer': {}, - 'peer_remove': [], - 'pk': '{}/default/private.key'.format(kdir) - } - - ifname = str(os.environ['VYOS_TAGNODE_VALUE']) - wg = deepcopy(dflt_cnf) - wg['intfc'] = ifname - wg['descr'] = ifname - - c.set_level(['interfaces', 'wireguard']) - - # interface removal state - if not c.exists(ifname) and c.exists_effective(ifname): - wg['delete'] = True - - if not wg['delete']: - c.set_level(['interfaces', 'wireguard', ifname]) - if c.exists(['address']): - wg['addr'] = c.return_values(['address']) - - # determine addresses which need to be removed - eff_addr = c.return_effective_values(['address']) - wg['addr_remove'] = list_diff(eff_addr, wg['addr']) - - # ifalias description - if c.exists(['description']): - wg['descr'] = c.return_value(['description']) - - # link state - if c.exists(['disable']): - wg['state'] = 'down' - - # local port to listen on - if c.exists(['port']): - wg['lport'] = c.return_value(['port']) - - # fwmark value - if c.exists(['fwmark']): - wg['fwmark'] = c.return_value(['fwmark']) - - # mtu - if c.exists('mtu'): - wg['mtu'] = c.return_value('mtu') - - # private key - if c.exists(['private-key']): - wg['pk'] = "{0}/{1}/private.key".format( - kdir, c.return_value(['private-key'])) - - # peer removal, wg identifies peers by its pubkey - peer_eff = c.list_effective_nodes(['peer']) - peer_rem = list_diff(peer_eff, c.list_nodes(['peer'])) - for p in peer_rem: - wg['peer_remove'].append( - c.return_effective_value(['peer', p, 'pubkey'])) - - # peer settings - if c.exists(['peer']): - for p in c.list_nodes(['peer']): - if not c.exists(['peer', p, 'disable']): - wg['peer'].update( - { - p: { - 'allowed-ips': [], - 'address': '', - 'port': '', - 'pubkey': '' - } - } - ) - # peer allowed-ips - if c.exists(['peer', p, 'allowed-ips']): - wg['peer'][p]['allowed-ips'] = c.return_values( - ['peer', p, 'allowed-ips']) - # peer address - if c.exists(['peer', p, 'address']): - wg['peer'][p]['address'] = c.return_value( - ['peer', p, 'address']) - # peer port - if c.exists(['peer', p, 'port']): - wg['peer'][p]['port'] = c.return_value( - ['peer', p, 'port']) - # persistent-keepalive - if c.exists(['peer', p, 'persistent-keepalive']): - wg['peer'][p]['persistent-keepalive'] = c.return_value( - ['peer', p, 'persistent-keepalive']) - # preshared-key - if c.exists(['peer', p, 'preshared-key']): - wg['peer'][p]['psk'] = c.return_value( - ['peer', p, 'preshared-key']) - # peer pubkeys - key_eff = c.return_effective_value(['peer', p, 'pubkey']) - key_cfg = c.return_value(['peer', p, 'pubkey']) - wg['peer'][p]['pubkey'] = key_cfg - - # on a pubkey change we need to remove the pubkey first - # peers are identified by pubkey, so key update means - # peer removal and re-add - if key_eff != key_cfg and key_eff != None: - wg['peer_remove'].append(key_cfg) - - # if a peer is disabled, we have to exec a remove for it's pubkey - else: - peer_key = c.return_value(['peer', p, 'pubkey']) - wg['peer_remove'].append(peer_key) + wg = deepcopy(default_config_data) + wg['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + + # Check if interface has been removed + if not conf.exists(base + [wg['intf']]): + wg['deleted'] = True + return wg + + conf.set_level(base + [wg['intf']]) + + # retrieve configured interface addresses + if conf.exists(['address']): + wg['address'] = conf.return_values(['address']) + + # get interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed + eff_addr = conf.return_effective_values(['address']) + wg['address_remove'] = list_diff(eff_addr, wg['address']) + + # retrieve interface description + if conf.exists(['description']): + wg['description'] = conf.return_value(['description']) + + # disable interface + if conf.exists(['disable']): + wg['disable'] = True + + # local port to listen on + if conf.exists(['port']): + wg['lport'] = conf.return_value(['port']) + + # fwmark value + if conf.exists(['fwmark']): + wg['fwmark'] = int(conf.return_value(['fwmark'])) + + # Maximum Transmission Unit (MTU) + if conf.exists('mtu'): + wg['mtu'] = int(conf.return_value(['mtu'])) + + # retrieve VRF instance + if conf.exists('vrf'): + wg['vrf'] = conf.return_value('vrf') + + # private key + if conf.exists(['private-key']): + wg['pk'] = "{0}/{1}/private.key".format( + kdir, conf.return_value(['private-key'])) + + # peer removal, wg identifies peers by its pubkey + peer_eff = conf.list_effective_nodes(['peer']) + peer_rem = list_diff(peer_eff, conf.list_nodes(['peer'])) + for peer in peer_rem: + wg['peer_remove'].append( + conf.return_effective_value(['peer', peer, 'pubkey'])) + + # peer settings + if conf.exists(['peer']): + for p in conf.list_nodes(['peer']): + # set new config level for this peer + conf.set_level(base + [wg['intf'], 'peer', p]) + peer = { + 'allowed-ips': [], + 'address': '', + 'name': p, + 'persistent_keepalive': '', + 'port': '', + 'psk': '', + 'pubkey': '' + } + + # peer allowed-ips + if conf.exists(['allowed-ips']): + peer['allowed-ips'] = conf.return_values(['allowed-ips']) + + # peer address + if conf.exists(['address']): + peer['address'] = conf.return_value(['address']) + + # peer port + if conf.exists(['port']): + peer['port'] = conf.return_value(['port']) + + # persistent-keepalive + if conf.exists(['persistent-keepalive']): + peer['persistent_keepalive'] = conf.return_value(['persistent-keepalive']) + + # preshared-key + if conf.exists(['preshared-key']): + peer['psk'] = conf.return_value(['preshared-key']) + + # peer pubkeys + if conf.exists(['pubkey']): + key_eff = conf.return_effective_value(['pubkey']) + key_cfg = conf.return_value(['pubkey']) + peer['pubkey'] = key_cfg + + # on a pubkey change we need to remove the pubkey first + # peers are identified by pubkey, so key update means + # peer removal and re-add + if key_eff != key_cfg and key_eff != None: + wg['peer_remove'].append(key_cfg) + + # if a peer is disabled, we have to exec a remove for it's pubkey + if conf.exists(['disable']): + wg['peer_remove'].append(peer['pubkey']) + else: + wg['peer'].append(peer) + return wg -def verify(c): - if not c: - return None +def verify(wg): + interface = wg['intf'] - if c['delete']: - interface = c['intfc'] + if wg['deleted']: is_member, bridge = is_bridge_member(interface) if is_member: # can not use a f'' formatted-string here as bridge would not get @@ -189,98 +197,100 @@ def verify(c): 'is a member of bridge "{1}"!'.format(interface, bridge)) return None - if not os.path.exists(c['pk']): - raise ConfigError( - "No keys found, generate them by executing: \'run generate wireguard [keypair|named-keypairs]\'") - - if not c['delete']: - if not c['addr']: - raise ConfigError("ERROR: IP address required") - if not c['peer']: - raise ConfigError("ERROR: peer required") - for p in c['peer']: - if not c['peer'][p]['allowed-ips']: - raise ConfigError("ERROR: allowed-ips required for peer " + p) - if not c['peer'][p]['pubkey']: - raise ConfigError("peer pubkey required for peer " + p) - - -def apply(c): - # no wg configs left, remove all interface from system - # maybe move it into ifconfig.py - if not c: - net_devs = os.listdir('/sys/class/net/') - for dev in net_devs: - if os.path.isdir('/sys/class/net/' + dev): - buf = open('/sys/class/net/' + dev + '/uevent', 'r').read() - if re.search("DEVTYPE=wireguard", buf, re.I | re.M): - wg_intf = re.sub("INTERFACE=", "", re.search( - "INTERFACE=.*", buf, re.I | re.M).group(0)) - # XXX: we are ignoring any errors here - run(f'ip l d dev {wg_intf} >/dev/null') - return None + vrf_name = wg['vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF "{vrf_name}" does not exist') + + if not os.path.exists(wg['pk']): + raise ConfigError('No keys found, generate them by executing:\n' \ + '"run generate wireguard [keypair|named-keypairs]"') + if not wg['address']: + raise ConfigError(f'IP address required for interface "{interface}"!') + + if not wg['peer']: + raise ConfigError(f'Peer required for interface "{interface}"!') + + # run checks on individual configured WireGuard peer + for peer in wg['peer']: + peer_name = peer['name'] + if not peer['allowed-ips']: + raise ConfigError(f'Peer allowed-ips required for peer "{peer_name}"!') + + if not peer['pubkey']: + raise ConfigError(f'Peer public-key required for peer "{peer_name}"!') + + +def apply(wg): # init wg class - intfc = WireGuardIf(c['intfc']) + w = WireGuardIf(wg['intf']) # single interface removal - if c['delete']: - intfc.remove() + if wg['deleted']: + w.remove() return None - # remove IP addresses - for ip in c['addr_remove']: - intfc.del_addr(ip) + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second + for addr in wg['address_remove']: + w.del_addr(addr) + for addr in wg['address']: + w.add_addr(addr) - # add IP addresses - for ip in c['addr']: - intfc.add_addr(ip) + # Maximum Transmission Unit (MTU) + w.set_mtu(wg['mtu']) - # interface mtu - intfc.set_mtu(int(c['mtu'])) + # update interface description used e.g. within SNMP + w.set_alias(wg['description']) - # ifalias for snmp from description - intfc.set_alias(str(c['descr'])) + # assign/remove VRF + w.set_vrf(wg['vrf']) # remove peers - if c['peer_remove']: - for pkey in c['peer_remove']: - intfc.remove_peer(pkey) + for pub_key in wg['peer_remove']: + w.remove_peer(pub_key) # peer pubkey # setting up the wg interface - intfc.config['private-key'] = c['pk'] - for p in c['peer']: + w.config['private-key'] = c['pk'] + + for peer in wg['peer']: # peer pubkey - intfc.config['pubkey'] = str(c['peer'][p]['pubkey']) + w.config['pubkey'] = peer['pubkey'] # peer allowed-ips - intfc.config['allowed-ips'] = c['peer'][p]['allowed-ips'] + w.config['allowed-ips'] = peer['allowed-ips'] # local listen port - if c['lport']: - intfc.config['port'] = c['lport'] + if wg['lport']: + w.config['port'] = wg['lport'] # fwmark if c['fwmark']: - intfc.config['fwmark'] = c['fwmark'] + w.config['fwmark'] = wg['fwmark'] + # endpoint - if c['peer'][p]['address'] and c['peer'][p]['port']: - intfc.config['endpoint'] = "{}:{}".format(c['peer'][p]['address'], c['peer'][p]['port']) + if peer['address'] and peer['port']: + w.config['endpoint'] = '{}:{}'.format( + peer['address'], peer['port']) # persistent-keepalive - if 'persistent-keepalive' in c['peer'][p]: - intfc.config['keepalive'] = c['peer'][p]['persistent-keepalive'] + if peer['persistent_keepalive']: + w.config['keepalive'] = peer['persistent_keepalive'] # maybe move it into ifconfig.py # preshared-key - needs to be read from a file - if 'psk' in c['peer'][p]: + if peer['psk']: psk_file = '/config/auth/wireguard/psk' - old_umask = os.umask(0o077) - open(psk_file, 'w').write(str(c['peer'][p]['psk'])) - os.umask(old_umask) - intfc.config['psk'] = psk_file - intfc.update() + with open(psk_file, 'w') as f: + f.write(peer['psk']) + w.config['psk'] = psk_file + + w.update() - # interface state - intfc.set_admin_state(c['state']) + # Enable/Disable interface + if wg['disable']: + w.set_admin_state('down') + else: + w.set_admin_state('up') return None @@ -293,4 +303,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index 709085b0f..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_file, 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_file(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_file(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_file(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']) @@ -722,32 +674,20 @@ def apply(wifi): # update interface description used e.g. within SNMP w.set_alias(wifi['description']) - # get DHCP config dictionary and update values - opt = w.get_dhcp_options() - if wifi['dhcp_client_id']: - opt['client_id'] = wifi['dhcp_client_id'] + w.dhcp.v4.options['client_id'] = wifi['dhcp_client_id'] if wifi['dhcp_hostname']: - opt['hostname'] = wifi['dhcp_hostname'] + w.dhcp.v4.options['hostname'] = wifi['dhcp_hostname'] if wifi['dhcp_vendor_class_id']: - opt['vendor_class_id'] = wifi['dhcp_vendor_class_id'] - - # store DHCP config dictionary - used later on when addresses are aquired - w.set_dhcp_options(opt) - - # get DHCPv6 config dictionary and update values - opt = w.get_dhcpv6_options() + w.dhcp.v4.options['vendor_class_id'] = wifi['dhcp_vendor_class_id'] if wifi['dhcpv6_prm_only']: - opt['dhcpv6_prm_only'] = True + w.dhcp.v6.options['dhcpv6_prm_only'] = True if wifi['dhcpv6_temporary']: - opt['dhcpv6_temporary'] = True - - # store DHCPv6 config dictionary - used later on when addresses are aquired - w.set_dhcpv6_options(opt) + w.dhcp.v6.options['dhcpv6_temporary'] = True # ignore link state changes w.set_link_detect(wifi['disable_link_detect']) @@ -786,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']: @@ -796,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 @@ -811,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 49445aaa4..da1855cd9 100755 --- a/src/conf_mode/interfaces-wirelessmodem.py +++ b/src/conf_mode/interfaces-wirelessmodem.py @@ -18,13 +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_file, chmod_x, cmd, run, 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': [], @@ -48,7 +52,7 @@ def check_kmod(): modules = ['option', 'usb_wwan', 'usbserial'] for module in modules: if not os.path.exists(f'/sys/module/{module}'): - if run(f'modprobe {module}') != 0: + if call(f'modprobe {module}') != 0: raise ConfigError(f'Loading Kernel module {module} failed') def get_config(): @@ -139,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'] @@ -173,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 @@ -219,7 +199,7 @@ def apply(wwan): intf = wwan['intf'] cmd(f'systemctl start ppp@{intf}.service') # make logfile owned by root / vyattacfg - chown_file(wwan['logfile'], 'root', 'vyattacfg') + chown(wwan['logfile'], 'root', 'vyattacfg') return None diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py index c2f5c8e07..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 run +from vyos.util import call +from vyos.template import render + ra_conn_name = "remote-access" charon_conf_file = "/etc/strongswan.d/charon.conf" @@ -99,7 +99,7 @@ def get_config(): ### Remove config from file by delimiter def remove_confs(delim_begin, delim_end, conf_file): - run("sed -i '/"+delim_begin+"/,/"+delim_end+"/d' "+conf_file) + call("sed -i '/"+delim_begin+"/,/"+delim_end+"/d' "+conf_file) ### Checking certificate storage and notice if certificate not in /config directory @@ -112,7 +112,7 @@ def check_cert_file_store(cert_name, file_path, dts_path): else: ### Cpy file to /etc/ipsec.d/certs/ /etc/ipsec.d/cacerts/ # todo make check - ret = run('cp -f '+file_path+' '+dts_path) + ret = call('cp -f '+file_path+' '+dts_path) if ret: raise ConfigError("L2TP VPN configuration error: Cannot copy "+file_path) @@ -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: @@ -193,12 +176,12 @@ def generate(data): remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_flie) def restart_ipsec(): - run('ipsec restart >&/dev/null') + call('ipsec restart >&/dev/null') # counter for apply swanctl config counter = 10 while counter <= 10: if os.path.exists(charon_pidfile): - run('swanctl -q >&/dev/null') + call('swanctl -q >&/dev/null') break counter -=1 sleep(1) diff --git a/src/conf_mode/le_cert.py b/src/conf_mode/le_cert.py index a4dbecbaa..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 @@ -22,8 +20,8 @@ import os import vyos.defaults from vyos.config import Config from vyos import ConfigError -from vyos.util import cmd, run - +from vyos.util import cmd +from vyos.util import call vyos_conf_scripts_dir = vyos.defaults.directories['conf_mode'] @@ -85,17 +83,17 @@ def generate(cert): # certbot will attempt to reload nginx, even with 'certonly'; # start nginx if not active - ret = run('systemctl is-active --quiet nginx.ervice') + ret = call('systemctl is-active --quiet nginx.service') if ret: - run('sudo systemctl start nginx.service') + call('systemctl start nginx.service') request_certbot(cert) def apply(cert): if cert is not None: - run('sudo systemctl restart certbot.timer') + call('systemctl restart certbot.timer') else: - run('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 c090bba83..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 run +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,25 +220,18 @@ 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): if lldp: # start/restart lldp service - run('sudo systemctl restart lldpd.service') + call('sudo systemctl restart lldpd.service') else: # LLDP service has been terminated - run('sudo systemctl stop lldpd.service') + call('sudo systemctl stop lldpd.service') os.unlink(config_file) os.unlink(vyos_config_file) diff --git a/src/conf_mode/mdns_repeater.py b/src/conf_mode/mdns_repeater.py index 2bccd9153..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 run - +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']: - run('sudo systemctl stop mdns-repeater') + call('systemctl stop mdns-repeater.service') if os.path.exists(config_file): os.unlink(config_file) else: - run('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 998022a8c..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.util import call +from vyos.template import render from vyos import ConfigError -from vyos.util import run - config_file = r'/etc/ntp.conf' @@ -100,24 +98,15 @@ 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): if ntp is not None: - run('sudo systemctl restart ntp.service') + call('sudo systemctl restart ntp.service') else: # NTP support is removed in the commit - run('sudo systemctl stop ntp.service') + call('sudo systemctl stop ntp.service') os.unlink(config_file) return None diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py index a62d2158e..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 run +from vyos.util import call +from vyos.template import render config_file = r'/tmp/bfd.frr' @@ -191,23 +190,14 @@ 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): if bfd is None: return None - run("vtysh -d bfdd -f " + config_file) + call("vtysh -d bfdd -f " + config_file) if os.path.exists(config_file): os.remove(config_file) diff --git a/src/conf_mode/protocols_igmp.py b/src/conf_mode/protocols_igmp.py index 6e819a15a..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 run +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): @@ -105,7 +95,7 @@ def apply(igmp): return None if os.path.exists(config_file): - run("sudo vtysh -d pimd -f " + config_file) + call("sudo vtysh -d pimd -f " + config_file) os.remove(config_file) return None diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py index 6e5d08397..0a241277d 100755 --- a/src/conf_mode/protocols_mpls.py +++ b/src/conf_mode/protocols_mpls.py @@ -16,18 +16,16 @@ 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 run +from vyos.util import call +from vyos.template import render config_file = r'/tmp/ldpd.frr' def sysctl(name, value): - run('sysctl -wq {}={}'.format(name, value)) + call('sysctl -wq {}={}'.format(name, value)) def get_config(): conf = Config() @@ -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): @@ -162,7 +151,7 @@ def apply(mpls): operate_mpls_on_intfc(diactive_ifaces, 0) if os.path.exists(config_file): - run("sudo vtysh -d ldpd -f " + config_file) + call("sudo vtysh -d ldpd -f " + config_file) os.remove(config_file) return None diff --git a/src/conf_mode/protocols_pim.py b/src/conf_mode/protocols_pim.py index 9b74fe992..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 run +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): @@ -132,7 +122,7 @@ def apply(pim): return None if os.path.exists(config_file): - run("vtysh -d pimd -f " + config_file) + call("vtysh -d pimd -f " + config_file) os.remove(config_file) return None diff --git a/src/conf_mode/salt-minion.py b/src/conf_mode/salt-minion.py index bd1d44bc8..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 run +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: @@ -126,10 +117,10 @@ def generate(salt): def apply(salt): if salt is not None: - run("sudo systemctl restart salt-minion") + call("sudo systemctl restart salt-minion") else: # Salt access is removed in the commit - run("sudo systemctl stop salt-minion") + call("sudo systemctl stop salt-minion") os.unlink(config_file) return None 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 0173b7242..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 run +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): @@ -158,13 +149,13 @@ def generate(rtradv): def apply(rtradv): if not rtradv['interfaces']: # bail out early - looks like removal from running config - run('systemctl stop radvd.service') + call('systemctl stop radvd.service') if os.path.exists(config_file): os.unlink(config_file) return None - run('systemctl restart radvd.service') + call('systemctl restart radvd.service') return None if __name__ == '__main__': diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index 414236c88..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 run +from vyos.util import call +from vyos.template import render config_file_client = r'/etc/snmp/snmp.conf' @@ -509,7 +508,7 @@ def generate(snmp): # # As we are manipulating the snmpd user database we have to stop it first! # This is even save if service is going to be removed - run('systemctl stop snmpd.service') + call('systemctl stop snmpd.service') config_files = [config_file_client, config_file_daemon, config_file_access, config_file_user] for file in config_files: @@ -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 @@ -554,7 +533,7 @@ def apply(snmp): return None # start SNMP daemon - run("systemctl restart snmpd.service") + call("systemctl restart snmpd.service") # Passwords are not available immediately in the configuration file, # after daemon startup - we wait until they have been processed by @@ -595,15 +574,15 @@ def apply(snmp): # Now update the running configuration # - # Currently when executing run() the environment does not + # Currently when executing call() the environment does not # have the vyos_libexec_dir variable set, see Phabricator T685. - run('/opt/vyatta/sbin/my_set service snmp v3 user "{0}" auth encrypted-key "{1}" > /dev/null'.format(cfg['user'], cfg['auth_pw'])) - run('/opt/vyatta/sbin/my_set service snmp v3 user "{0}" privacy encrypted-key "{1}" > /dev/null'.format(cfg['user'], cfg['priv_pw'])) - run('/opt/vyatta/sbin/my_delete service snmp v3 user "{0}" auth plaintext-key > /dev/null'.format(cfg['user'])) - run('/opt/vyatta/sbin/my_delete service snmp v3 user "{0}" privacy plaintext-key > /dev/null'.format(cfg['user'])) + call('/opt/vyatta/sbin/my_set service snmp v3 user "{0}" auth encrypted-key "{1}" > /dev/null'.format(cfg['user'], cfg['auth_pw'])) + call('/opt/vyatta/sbin/my_set service snmp v3 user "{0}" privacy encrypted-key "{1}" > /dev/null'.format(cfg['user'], cfg['priv_pw'])) + call('/opt/vyatta/sbin/my_delete service snmp v3 user "{0}" auth plaintext-key > /dev/null'.format(cfg['user'])) + call('/opt/vyatta/sbin/my_delete service snmp v3 user "{0}" privacy plaintext-key > /dev/null'.format(cfg['user'])) # Enable AgentX in FRR - run('vtysh -c "configure terminal" -c "agentx" >/dev/null') + call('vtysh -c "configure terminal" -c "agentx" >/dev/null') return None diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py index a85dcd7f2..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 run +from vyos.util import call +from vyos.template import render config_file = r'/etc/ssh/sshd_config' @@ -120,23 +119,15 @@ 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): if ssh is not None and 'port' in ssh.keys(): - run("sudo systemctl restart ssh.service") + call("sudo systemctl restart ssh.service") else: # SSH access is removed in the commit - run("sudo systemctl stop ssh.service") + call("sudo systemctl stop ssh.service") if os.path.isfile(config_file): os.unlink(config_file) diff --git a/src/conf_mode/system-ip.py b/src/conf_mode/system-ip.py index 66f563939..8a1ac8411 100755 --- a/src/conf_mode/system-ip.py +++ b/src/conf_mode/system-ip.py @@ -20,7 +20,7 @@ from sys import exit from copy import deepcopy from vyos.config import Config from vyos import ConfigError -from vyos.util import run +from vyos.util import call default_config_data = { @@ -31,7 +31,7 @@ default_config_data = { } def sysctl(name, value): - run('sysctl -wq {}={}'.format(name, value)) + call('sysctl -wq {}={}'.format(name, value)) def get_config(): ip_opt = deepcopy(default_config_data) diff --git a/src/conf_mode/system-ipv6.py b/src/conf_mode/system-ipv6.py index 4e3de6fe9..04a063564 100755 --- a/src/conf_mode/system-ipv6.py +++ b/src/conf_mode/system-ipv6.py @@ -21,7 +21,7 @@ from sys import exit from copy import deepcopy from vyos.config import Config from vyos import ConfigError -from vyos.util import run +from vyos.util import call ipv6_disable_file = '/etc/modprobe.d/vyos_disable_ipv6.conf' @@ -37,7 +37,7 @@ default_config_data = { } def sysctl(name, value): - run('sysctl -wq {}={}'.format(name, value)) + call('sysctl -wq {}={}'.format(name, value)) def get_config(): ip_opt = deepcopy(default_config_data) diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py index 7c99fce39..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,9 +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, run +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" @@ -207,19 +209,19 @@ def generate(login): # remove old plaintext password # and set new encrypted password - run("vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_set system login user '{}' authentication plaintext-password '' >/dev/null".format(user['name'])) - run("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'])) + 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 @@ -255,7 +257,7 @@ def apply(login): command += " {}".format(user['name']) try: - run(command) + cmd(command) uid = getpwnam(user['name']).pw_uid gid = getpwnam(user['name']).pw_gid @@ -295,10 +297,10 @@ def apply(login): # Logout user if he is logged in if user in list(set([tmp[0] for tmp in users()])): print('{} is logged in, forcing logout'.format(user)) - run('pkill -HUP -u {}'.format(user)) + call('pkill -HUP -u {}'.format(user)) # Remove user account but leave home directory to be safe - run('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)) @@ -308,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 - run("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\' \ @@ -320,15 +324,18 @@ def apply(login): -e \'/^group:[^#]*$/s/: */&mapname /\' \ /etc/nsswitch.conf" - run(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 - run("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//\' \ @@ -336,10 +343,10 @@ def apply(login): -e \'s/[ \t]*$//\' \ /etc/nsswitch.conf" - run(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-options.py b/src/conf_mode/system-options.py index 063a82463..b3dbc82fb 100755 --- a/src/conf_mode/system-options.py +++ b/src/conf_mode/system-options.py @@ -52,9 +52,9 @@ def generate(opt): def apply(opt): # Beep action if opt['beep_if_fully_booted']: - run('systemctl enable vyos-beep.service >/dev/null 2>&1') + run('systemctl enable vyos-beep.service') else: - run('systemctl disable vyos-beep.service >/dev/null 2>&1') + run('systemctl disable vyos-beep.service') # Ctrl-Alt-Delete action if opt['ctrl_alt_del'] == 'ignore': 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-timezone.py b/src/conf_mode/system-timezone.py index 2f8dc9e89..25b949a79 100755 --- a/src/conf_mode/system-timezone.py +++ b/src/conf_mode/system-timezone.py @@ -20,7 +20,7 @@ import os from copy import deepcopy from vyos.config import Config from vyos import ConfigError -from vyos.util import run +from vyos.util import call default_config_data = { @@ -42,7 +42,7 @@ def generate(tz): pass def apply(tz): - run('/usr/bin/timedatectl set-timezone {}'.format(tz['name'])) + call('/usr/bin/timedatectl set-timezone {}'.format(tz['name'])) 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 df8155084..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 run +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 - run('systemctl stop tftpd@{0..20}') + call('systemctl stop tftpd@{0..20}.service') # bail out early - e.g. service deletion if tftpd is None: @@ -140,7 +131,7 @@ def apply(tftpd): idx = 0 for listen in tftpd['listen']: - run('systemctl restart tftpd@{0}.service'.format(idx)) + call('systemctl restart tftpd@{0}.service'.format(idx)) idx = idx + 1 return 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 07466f3aa..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): @@ -195,6 +186,7 @@ def apply(vrf_config): # # - https://github.com/torvalds/linux/blob/master/Documentation/networking/vrf.txt # - https://github.com/Mellanox/mlxsw/wiki/Virtual-Routing-and-Forwarding-(VRF) + # - https://github.com/Mellanox/mlxsw/wiki/L3-Tunneling # - https://netdevconf.info/1.1/proceedings/slides/ahern-vrf-tutorial.pdf # - https://netdevconf.info/1.2/slides/oct6/02_ahern_what_is_l3mdev_slides.pdf diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py index d3e3710d1..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 run +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 @@ -242,17 +230,17 @@ def apply(data): if not vyos.keepalived.vrrp_running(): print("Starting the VRRP process") - ret = run("sudo systemctl restart keepalived.service") + ret = call("sudo systemctl restart keepalived.service") else: print("Reloading the VRRP process") - ret = run("sudo systemctl reload keepalived.service") + ret = call("sudo systemctl reload keepalived.service") if ret != 0: raise ConfigError("keepalived failed to start") else: # VRRP is removed in the commit print("Stopping the VRRP process") - run("sudo systemctl stop keepalived.service") + call("sudo systemctl stop keepalived.service") os.unlink(config_file) return None |