diff options
Diffstat (limited to 'src')
45 files changed, 1244 insertions, 4511 deletions
diff --git a/src/conf_mode/accel_l2tp.py b/src/conf_mode/accel_l2tp.py index a7af9cc68..77e1ee874 100755 --- a/src/conf_mode/accel_l2tp.py +++ b/src/conf_mode/accel_l2tp.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,19 +13,18 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# import sys import os import re import subprocess -import jinja2 import socket import time -import syslog as sl + +from jinja2 import FileSystemLoader, Environment from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError pidfile = r'/var/run/accel_l2tp.pid' @@ -34,526 +33,371 @@ 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 +# config path creation if not os.path.exists(l2tp_cnf_dir): - os.makedirs(l2tp_cnf_dir) - sl.syslog(sl.LOG_NOTICE, l2tp_cnf_dir + " created") - -l2tp_config = ''' -### generated by accel_l2tp.py ### -[modules] -log_syslog -l2tp -chap-secrets -{% for proto in authentication['auth_proto']: %} -{{proto}} -{% endfor%} -{% if authentication['mode'] == 'radius' %} -radius -{% endif -%} -ippool -shaper -ipv6pool -ipv6_nd -ipv6_dhcp - -[core] -thread-count={{thread_cnt}} - -[log] -syslog=accel-l2tp,daemon -copy=1 -level=5 - -{% if dns %} -[dns] -{% if dns[0] %} -dns1={{dns[0]}} -{% endif %} -{% if dns[1] %} -dns2={{dns[1]}} -{% endif %} -{% endif -%} - -{% if dnsv6 %} -[ipv6-dns] -{% for srv in dnsv6: %} -{{srv}} -{% endfor %} -{% endif %} - -{% if wins %} -[wins] -{% if wins[0] %} -wins1={{wins[0]}} -{% endif %} -{% if wins[1] %} -wins2={{wins[1]}} -{% endif %} -{% endif -%} - -[l2tp] -verbose=1 -ifname=l2tp%d -ppp-max-mtu={{mtu}} -mppe={{authentication['mppe']}} -{% if outside_addr %} -bind={{outside_addr}} -{% endif %} -{% if lns_shared_secret %} -secret={{lns_shared_secret}} -{% endif %} - -[client-ip-range] -0.0.0.0/0 - -{% if (client_ip_pool) or (client_ip_subnets) %} -[ip-pool] -{% if client_ip_pool %} -{{client_ip_pool}} -{% endif -%} -{% if client_ip_subnets %} -{% for sn in client_ip_subnets %} -{{sn}} -{% endfor -%} -{% endif %} -{% endif %} -{% if gateway_address %} -gw-ip-address={{gateway_address}} -{% endif %} - -{% if authentication['mode'] == 'local' %} -[chap-secrets] -chap-secrets=/etc/accel-ppp/l2tp/chap-secrets -{% if gateway_address %} -gw-ip-address={{gateway_address}} -{% endif %} -{% endif %} - -[ppp] -verbose=1 -check-ip=1 -single-session=replace -{% if idle_timeout %} -lcp-echo-timeout={{idle_timeout}} -{% endif %} -{% if ppp_options['lcp-echo-interval'] %} -lcp-echo-interval={{ppp_options['lcp-echo-interval']}} -{% else %} -lcp-echo-interval=30 -{% endif %} -{% if ppp_options['lcp-echo-failure'] %} -lcp-echo-failure={{ppp_options['lcp-echo-failure']}} -{% else %} -lcp-echo-failure=3 -{% endif %} -{% if ccp_disable %} -ccp=0 -{% endif %} -{% if client_ipv6_pool %} -ipv6=allow -{% endif %} - -{% if authentication['mode'] == 'radius' %} -[radius] -{% for rsrv in authentication['radiussrv']: %} -server={{rsrv}},{{authentication['radiussrv'][rsrv]['secret']}},\ -req-limit={{authentication['radiussrv'][rsrv]['req-limit']}},\ -fail-time={{authentication['radiussrv'][rsrv]['fail-time']}} -{% endfor %} -{% if authentication['radiusopt']['timeout'] %} -timeout={{authentication['radiusopt']['timeout']}} -{% endif %} -{% if authentication['radiusopt']['acct-timeout'] %} -acct-timeout={{authentication['radiusopt']['acct-timeout']}} -{% endif %} -{% if authentication['radiusopt']['max-try'] %} -max-try={{authentication['radiusopt']['max-try']}} -{% endif %} -{% if authentication['radiusopt']['nas-id'] %} -nas-identifier={{authentication['radiusopt']['nas-id']}} -{% endif %} -{% if authentication['radius_source_address'] %} -nas-ip-address={{authentication['radius_source_address']}} -{% endif -%} -{% if authentication['radiusopt']['dae-srv'] %} -dae-server={{authentication['radiusopt']['dae-srv']['ip-addr']}}:\ -{{authentication['radiusopt']['dae-srv']['port']}},\ -{{authentication['radiusopt']['dae-srv']['secret']}} -{% endif -%} -gw-ip-address={{gateway_address}} -verbose=1 -{% endif -%} - -{% if client_ipv6_pool %} -[ipv6-pool] -{% for prfx in client_ipv6_pool.prefix: %} -{{prfx}} -{% endfor %} -{% for prfx in client_ipv6_pool.delegate_prefix: %} -delegate={{prfx}} -{% endfor %} -{% endif %} - -{% if client_ipv6_pool['delegate_prefix'] %} -[ipv6-dhcp] -verbose=1 -{% endif %} - -{% if authentication['radiusopt']['shaper'] %} -[shaper] -verbose=1 -attr={{authentication['radiusopt']['shaper']['attr']}} -{% if authentication['radiusopt']['shaper']['vendor'] %} -vendor={{authentication['radiusopt']['shaper']['vendor']}} -{% endif -%} -{% endif %} - -[cli] -tcp=127.0.0.1:2004 -sessions-columns=ifname,username,calling-sid,ip,{{ip6_column}}{{ip6_dp_column}}rate-limit,type,comp,state,rx-bytes,tx-bytes,uptime - -''' - -### l2tp chap secrets -chap_secrets_conf = ''' -# username server password acceptable local IP addresses shaper -{% for user in authentication['local-users'] %} -{% if authentication['local-users'][user]['state'] == 'enabled' %} -{% if (authentication['local-users'][user]['upload']) and (authentication['local-users'][user]['download']) %} -{{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}}\t\ -{{authentication['local-users'][user]['download']}}/{{authentication['local-users'][user]['upload']}} -{% else %} -{{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}} -{% endif %} -{% endif %} -{% endfor %} -''' + 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 +# 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 - -### chap_secrets file if auth mode local -def write_chap_secrets(c): - tmpl = jinja2.Template(chap_secrets_conf, trim_blocks=True) - chap_secrets_txt = tmpl.render(c) - old_umask = os.umask(0o077) - open(chap_secrets,'w').write(chap_secrets_txt) - os.umask(old_umask) - sl.syslog(sl.LOG_NOTICE, chap_secrets + ' written') + 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(cmd=''): - if not cmd: - return None - try: - ret = subprocess.check_output(['/usr/bin/accel-cmd','-p','2004',cmd]).decode().strip() - return ret - except: - return 1 - -### + if not cmd: + return None + try: + ret = subprocess.check_output( + ['/usr/bin/accel-cmd', '-p', '2004', cmd]).decode().strip() + return ret + except: + return 1 + +### # 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' : { + 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' }, - '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 + '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") + 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 - - ### 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) + if c == None: + return None - tmpl = jinja2.Template(l2tp_config, trim_blocks=True) - config_text = tmpl.render(c) - open(l2tp_conf,'w').write(config_text) + # 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) - if c['authentication']['local-users']: - write_chap_secrets(c) + # 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 - 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 = subprocess.call(['/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') - sl.syslog(sl.LOG_NOTICE, "reloading config via daemon restart") + 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 = subprocess.call( + ['/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) + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/bcast_relay.py b/src/conf_mode/bcast_relay.py index 8889e701c..96576ddd4 100755 --- a/src/conf_mode/bcast_relay.py +++ b/src/conf_mode/bcast_relay.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2017 VyOS maintainers and contributors +# Copyright (C) 2017-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,42 +13,34 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import os import fnmatch -import jinja2 + +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 config_file = r'/etc/default/udp-broadcast-relay' -config_tmpl = """ -### Autogenerated by bcast_relay.py ### - -# UDP broadcast relay configuration for instance {{ id }} -{%- if description %} -# Comment: {{ description }} -{% endif %} -DAEMON_ARGS="{% if address %}-s {{ address }} {% endif %}{{ id }} {{ port }} {{ interfaces | join(' ') }}" - -""" - default_config_data = { 'disabled': False, 'instances': [] } def get_config(): - relay = default_config_data + relay = deepcopy(default_config_data) conf = Config() - if not conf.exists('service broadcast-relay'): + base = ['service', 'broadcast-relay'] + + if not conf.exists(base): return None else: - conf.set_level('service broadcast-relay') + conf.set_level(base) # Service can be disabled by user if conf.exists('disable'): @@ -58,7 +50,7 @@ def get_config(): # Parse configuration of each individual instance if conf.exists('id'): for id in conf.list_nodes('id'): - conf.set_level('service broadcast-relay id {0}'.format(id)) + conf.set_level(base + ['id', id]) config = { 'id': id, 'disabled': False, @@ -69,24 +61,24 @@ def get_config(): } # Check if individual broadcast relay service is disabled - if conf.exists('disable'): + if conf.exists(['disable']): config['disabled'] = True # Source IP of forwarded packets, if empty original senders address is used - if conf.exists('address'): - config['address'] = conf.return_value('address') + if conf.exists(['address']): + config['address'] = conf.return_value(['address']) # A description for each individual broadcast relay service - if conf.exists('description'): - config['description'] = conf.return_value('description') + if conf.exists(['description']): + config['description'] = conf.return_value(['description']) # UDP port to listen on for broadcast frames - if conf.exists('port'): - config['port'] = conf.return_value('port') + if conf.exists(['port']): + config['port'] = conf.return_value(['port']) # Network interfaces to listen on for broadcast frames to be relayed - if conf.exists('interface'): - config['interfaces'] = conf.return_values('interface') + if conf.exists(['interface']): + config['interfaces'] = conf.return_values(['interface']) relay['instances'].append(config) @@ -119,6 +111,11 @@ 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 = [] @@ -148,7 +145,7 @@ def generate(relay): # configuration filename contains instance id file = config_file + str(r['id']) - tmpl = jinja2.Template(config_tmpl) + tmpl = env.get_template('udp-broadcast-relay.tmpl') config_text = tmpl.render(r) with open(file, 'w') as f: f.write(config_text) @@ -179,4 +176,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/dhcp_relay.py b/src/conf_mode/dhcp_relay.py index a1af2575f..6f8d66e7b 100755 --- a/src/conf_mode/dhcp_relay.py +++ b/src/conf_mode/dhcp_relay.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,39 +13,18 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import os -import jinja2 + +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 config_file = r'/etc/default/isc-dhcp-relay' -# Please be careful if you edit the template. -config_tmpl = """ -### Autogenerated by dhcp_relay.py ### - -# Defaults for isc-dhcp-relay initscript -# sourced by /etc/init.d/isc-dhcp-relay - -# -# This is a POSIX shell fragment -# - -# What servers should the DHCP relay forward requests to? -SERVERS="{{ server | join(' ') }}" - -# On what interfaces should the DHCP relay (dhrelay) serve DHCP requests? -INTERFACES="{{ interface | join(' ') }}" - -# Additional options that are passed to the DHCP relay daemon? -OPTIONS="-4 {{ options | join(' ') }}" -""" - default_config_data = { 'interface': [], 'server': [], @@ -57,23 +36,23 @@ default_config_data = { def get_config(): relay = default_config_data conf = Config() - if not conf.exists('service dhcp-relay'): + if not conf.exists(['service', 'dhcp-relay']): return None else: - conf.set_level('service dhcp-relay') + conf.set_level(['service', 'dhcp-relay']) # Network interfaces to listen on - if conf.exists('interface'): - relay['interface'] = conf.return_values('interface') + if conf.exists(['interface']): + relay['interface'] = conf.return_values(['interface']) # Servers equal to the address of the DHCP server(s) - if conf.exists('server'): - relay['server'] = conf.return_values('server') + if conf.exists(['server']): + relay['server'] = conf.return_values(['server']) - conf.set_level('service dhcp-relay relay-options') + conf.set_level(['service', 'dhcp-relay', 'relay-options']) - if conf.exists('hop-count'): - count = '-c ' + conf.return_value('hop-count') + if conf.exists(['hop-count']): + count = '-c ' + conf.return_value(['hop-count']) relay['options'].append(count) # Specify the maximum packet size to send to a DHCPv4/BOOTP server. @@ -81,8 +60,8 @@ def get_config(): # options while still fitting into the Ethernet MTU size. # # Available in DHCPv4 mode only: - if conf.exists('max-size'): - size = '-A ' + conf.return_value('max-size') + if conf.exists(['max-size']): + size = '-A ' + conf.return_value(['max-size']) relay['options'].append(size) # Control the handling of incoming DHCPv4 packets which already contain @@ -94,8 +73,8 @@ def get_config(): # field; it may forward the packet unchanged; or, it may discard it. # # Available in DHCPv4 mode only: - if conf.exists('relay-agents-packets'): - pkt = '-a -m ' + conf.return_value('relay-agents-packets') + if conf.exists(['relay-agents-packets']): + pkt = '-a -m ' + conf.return_value(['relay-agents-packets']) relay['options'].append(pkt) return relay @@ -119,7 +98,12 @@ def generate(relay): if relay is None: return None - tmpl = jinja2.Template(config_tmpl) + # 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) @@ -144,4 +128,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index 39cc72574..3d75414f5 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2019 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,16 +14,17 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import sys import os -import jinja2 -import socket -import struct - -import vyos.validate 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 config_file = r'/etc/dhcp/dhcpd.conf' @@ -31,215 +32,6 @@ lease_file = r'/config/dhcpd.leases' pid_file = r'/var/run/dhcpd.pid' daemon_config_file = r'/etc/default/isc-dhcpv4-server' -# Please be careful if you edit the template. -config_tmpl = """ -### Autogenerated by dhcp_server.py ### - -# For options please consult the following website: -# https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html -# -# log-facility local7; - -{% if hostfile_update %} -on release { - set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); - set ClientIp = binary-to-ascii(10, 8, ".",leased-address); - set ClientMac = binary-to-ascii(16, 8, ":",substring(hardware, 1, 6)); - set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!"); - execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", ClientName, ClientIp, ClientMac, ClientDomain); -} - -on expiry { - set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); - set ClientIp = binary-to-ascii(10, 8, ".",leased-address); - set ClientMac = binary-to-ascii(16, 8, ":",substring(hardware, 1, 6)); - set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!"); - execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", ClientName, ClientIp, ClientMac, ClientDomain); -} -{% endif %} -{%- if host_decl_name %} -use-host-decl-names on; -{%- endif %} -ddns-update-style {% if ddns_enable -%} interim {%- else -%} none {%- endif %}; -{% if static_route -%} -option rfc3442-static-route code 121 = array of integer 8; -option windows-static-route code 249 = array of integer 8; -{%- endif %} -{% if wpad -%} -option wpad-url code 252 = text; -{% endif %} - -{%- if global_parameters %} -# The following {{ global_parameters | length }} line(s) were added as global-parameters in the CLI and have not been validated -{%- for param in global_parameters %} -{{ param }} -{%- endfor -%} -{%- endif %} - -# Failover configuration -{% for network in shared_network %} -{%- if not network.disabled -%} -{%- for subnet in network.subnet %} -{%- if subnet.failover_name -%} -failover peer "{{ subnet.failover_name }}" { -{%- if subnet.failover_status == 'primary' %} - primary; - mclt 1800; - split 128; -{%- elif subnet.failover_status == 'secondary' %} - secondary; -{%- endif %} - address {{ subnet.failover_local_addr }}; - port 520; - peer address {{ subnet.failover_peer_addr }}; - peer port 520; - max-response-delay 30; - max-unacked-updates 10; - load balance max seconds 3; -} -{% endif -%} -{% endfor -%} -{% endif -%} -{% endfor %} - -# Shared network configration(s) -{% for network in shared_network %} -{%- if not network.disabled -%} -shared-network {{ network.name }} { - {%- if network.authoritative %} - authoritative; - {%- endif %} - {%- if network.network_parameters %} - # The following {{ network.network_parameters | length }} line(s) were added as shared-network-parameters in the CLI and have not been validated - {%- for param in network.network_parameters %} - {{ param }} - {%- endfor %} - {%- endif %} - {%- for subnet in network.subnet %} - subnet {{ subnet.address }} netmask {{ subnet.netmask }} { - {%- if subnet.dns_server %} - option domain-name-servers {{ subnet.dns_server | join(', ') }}; - {%- endif %} - {%- if subnet.domain_search %} - option domain-search {{ subnet.domain_search | join(', ') }}; - {%- endif %} - {%- if subnet.ntp_server %} - option ntp-servers {{ subnet.ntp_server | join(', ') }}; - {%- endif %} - {%- if subnet.pop_server %} - option pop-server {{ subnet.pop_server | join(', ') }}; - {%- endif %} - {%- if subnet.smtp_server %} - option smtp-server {{ subnet.smtp_server | join(', ') }}; - {%- endif %} - {%- if subnet.time_server %} - option time-servers {{ subnet.time_server | join(', ') }}; - {%- endif %} - {%- if subnet.wins_server %} - option netbios-name-servers {{ subnet.wins_server | join(', ') }}; - {%- endif %} - {%- if subnet.static_route %} - option rfc3442-static-route {{ subnet.static_route }}{% if subnet.rfc3442_default_router %}, {{ subnet.rfc3442_default_router }}{% endif %}; - option windows-static-route {{ subnet.static_route }}; - {%- endif %} - {%- if subnet.ip_forwarding %} - option ip-forwarding true; - {%- endif -%} - {%- if subnet.default_router %} - option routers {{ subnet.default_router }}; - {%- endif -%} - {%- if subnet.server_identifier %} - option dhcp-server-identifier {{ subnet.server_identifier }}; - {%- endif -%} - {%- if subnet.domain_name %} - option domain-name "{{ subnet.domain_name }}"; - {%- endif -%} - {%- if subnet.subnet_parameters %} - # The following {{ subnet.subnet_parameters | length }} line(s) were added as subnet-parameters in the CLI and have not been validated - {%- for param in subnet.subnet_parameters %} - {{ param }} - {%- endfor -%} - {%- endif %} - {%- if subnet.tftp_server %} - option tftp-server-name "{{ subnet.tftp_server }}"; - {%- endif -%} - {%- if subnet.bootfile_name %} - option bootfile-name "{{ subnet.bootfile_name }}"; - filename "{{ subnet.bootfile_name }}"; - {%- endif -%} - {%- if subnet.bootfile_server %} - next-server {{ subnet.bootfile_server }}; - {%- endif -%} - {%- if subnet.time_offset %} - option time-offset {{ subnet.time_offset }}; - {%- endif -%} - {%- if subnet.wpad_url %} - option wpad-url "{{ subnet.wpad_url }}"; - {%- endif -%} - {%- if subnet.client_prefix_length %} - option subnet-mask {{ subnet.client_prefix_length }}; - {%- endif -%} - {% if subnet.lease %} - default-lease-time {{ subnet.lease }}; - max-lease-time {{ subnet.lease }}; - {%- endif -%} - {%- for host in subnet.static_mapping %} - {% if not host.disabled -%} - host {% if host_decl_name -%} {{ host.name }} {%- else -%} {{ network.name }}_{{ host.name }} {%- endif %} { - {%- if host.ip_address %} - fixed-address {{ host.ip_address }}; - {%- endif %} - hardware ethernet {{ host.mac_address }}; - {%- if host.static_parameters %} - # The following {{ host.static_parameters | length }} line(s) were added as static-mapping-parameters in the CLI and have not been validated - {%- for param in host.static_parameters %} - {{ param }} - {%- endfor -%} - {%- endif %} - } - {%- endif %} - {%- endfor %} - {%- if subnet.failover_name %} - pool { - failover peer "{{ subnet.failover_name }}"; - deny dynamic bootp clients; - {%- for range in subnet.range %} - range {{ range.start }} {{ range.stop }}; - {%- endfor %} - } - {%- else %} - {%- for range in subnet.range %} - range {{ range.start }} {{ range.stop }}; - {%- endfor %} - {%- endif %} - } - {%- endfor %} - on commit { - set shared-networkname = "{{ network.name }}"; - {% if hostfile_update -%} - set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); - set ClientIp = binary-to-ascii(10, 8, ".", leased-address); - set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 6)); - set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!"); - execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "commit", ClientName, ClientIp, ClientMac, ClientDomain); - {%- endif %} - } -} -{%- endif %} -{% endfor %} -""" - -daemon_tmpl = """ -### Autogenerated by dhcp_server.py ### - -# sourced by /etc/init.d/isc-dhcpv4-server - -DHCPD_CONF={{ config_file }} -DHCPD_PID={{ pid_file }} -OPTIONS="-4 -lf {{ lease_file }}" -INTERFACES="" -""" - default_config_data = { 'lease_file': lease_file, 'disabled': False, @@ -459,7 +251,7 @@ def get_config(): if conf.exists('client-prefix-length'): # snippet borrowed from https://stackoverflow.com/questions/33750233/convert-cidr-to-subnet-mask-in-python host_bits = 32 - int(conf.return_value('client-prefix-length')) - subnet['client_prefix_length'] = socket.inet_ntoa(struct.pack('!I', (1 << 32) - (1 << host_bits))) + subnet['client_prefix_length'] = inet_ntoa(pack('!I', (1 << 32) - (1 << host_bits))) # Default router IP address on the client's subnet if conf.exists('default-router'): @@ -778,7 +570,7 @@ def verify(dhcp): # There must be one subnet connected to a listen interface. # This only counts if the network itself is not disabled! if not network['disabled']: - if vyos.validate.is_subnet_connected(subnet['network'], primary=True): + if is_subnet_connected(subnet['network'], primary=True): listen_ok = True # Subnets must be non overlapping @@ -810,9 +602,13 @@ def generate(dhcp): print('Warning: DHCP server will be deactivated because it is disabled') return None - tmpl = jinja2.Template(config_tmpl) - config_text = tmpl.render(dhcp) + # 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) + 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(""",'"') @@ -820,7 +616,7 @@ def generate(dhcp): with open(config_file, 'w') as f: f.write(config_text) - tmpl = jinja2.Template(daemon_tmpl) + tmpl = env.get_template('daemon.tmpl') config_text = tmpl.render(dhcp) with open(daemon_config_file, 'w') as f: f.write(config_text) @@ -852,4 +648,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/dhcpv6_relay.py b/src/conf_mode/dhcpv6_relay.py index ccabc901d..d942daf37 100755 --- a/src/conf_mode/dhcpv6_relay.py +++ b/src/conf_mode/dhcpv6_relay.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,27 +13,19 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import os -import jinja2 + +from 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 config_file = r'/etc/default/isc-dhcpv6-relay' -# Please be careful if you edit the template. -config_tmpl = """ -### Autogenerated by dhcpv6_relay.py ### - -# Defaults for isc-dhcpv6-relay initscript sourced by /etc/init.d/isc-dhcpv6-relay -OPTIONS="-6 -l {{ listen_addr | join(' -l ') }} -u {{ upstream_addr | join(' -u ') }} {{ options | join(' ') }}" - -""" - default_config_data = { 'listen_addr': [], 'upstream_addr': [], @@ -41,7 +33,7 @@ default_config_data = { } def get_config(): - relay = default_config_data + relay = deepcopy(default_config_data) conf = Config() if not conf.exists('service dhcpv6-relay'): return None @@ -92,7 +84,12 @@ def generate(relay): if relay is None: return None - tmpl = jinja2.Template(config_tmpl) + # 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) @@ -117,4 +114,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py index 44a927789..10b40baa4 100755 --- a/src/conf_mode/dhcpv6_server.py +++ b/src/conf_mode/dhcpv6_server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2019 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,18 +13,17 @@ # # 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 ipaddress -import jinja2 - -import vyos.validate +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_subnet_connected from vyos import ConfigError config_file = r'/etc/dhcp/dhcpdv6.conf' @@ -32,100 +31,6 @@ lease_file = r'/config/dhcpdv6.leases' pid_file = r'/var/run/dhcpdv6.pid' daemon_config_file = r'/etc/default/isc-dhcpv6-server' -# Please be careful if you edit the template. -config_tmpl = """ -### Autogenerated by dhcpv6_server.py ### - -# For options please consult the following website: -# https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html - -log-facility local7; -{%- if preference %} -option dhcp6.preference {{ preference }}; -{%- endif %} - -# Shared network configration(s) -{% for network in shared_network %} -{%- if not network.disabled -%} -shared-network {{ network.name }} { - {%- for subnet in network.subnet %} - subnet6 {{ subnet.network }} { - {%- for range in subnet.range6_prefix %} - range6 {{ range.prefix }}{{ " temporary" if range.temporary }}; - {%- endfor %} - {%- for range in subnet.range6 %} - range6 {{ range.start }} {{ range.stop }}; - {%- endfor %} - {%- if subnet.domain_search %} - option dhcp6.domain-search {{ subnet.domain_search | join(', ') }}; - {%- endif %} - {%- if subnet.lease_def %} - default-lease-time {{ subnet.lease_def }}; - {%- endif %} - {%- if subnet.lease_max %} - max-lease-time {{ subnet.lease_max }}; - {%- endif %} - {%- if subnet.lease_min %} - min-lease-time {{ subnet.lease_min }}; - {%- endif %} - {%- if subnet.dns_server %} - option dhcp6.name-servers {{ subnet.dns_server | join(', ') }}; - {%- endif %} - {%- if subnet.nis_domain %} - option dhcp6.nis-domain-name "{{ subnet.nis_domain }}"; - {%- endif %} - {%- if subnet.nis_server %} - option dhcp6.nis-servers {{ subnet.nis_server | join(', ') }}; - {%- endif %} - {%- if subnet.nisp_domain %} - option dhcp6.nisp-domain-name "{{ subnet.nisp_domain }}"; - {%- endif %} - {%- if subnet.nisp_server %} - option dhcp6.nisp-servers {{ subnet.nisp_server | join(', ') }}; - {%- endif %} - {%- if subnet.sip_address %} - option dhcp6.sip-servers-addresses {{ subnet.sip_address | join(', ') }}; - {%- endif %} - {%- if subnet.sip_hostname %} - option dhcp6.sip-servers-names {{ subnet.sip_hostname | join(', ') }}; - {%- endif %} - {%- if subnet.sntp_server %} - option dhcp6.sntp-servers {{ subnet.sntp_server | join(', ') }}; - {%- endif %} - {%- for host in subnet.static_mapping %} - {% if not host.disabled -%} - host {{ network.name }}_{{ host.name }} { - {%- if host.client_identifier %} - host-identifier option dhcp6.client-id {{ host.client_identifier }}; - {%- endif %} - {%- if host.ipv6_address %} - fixed-address6 {{ host.ipv6_address }}; - {%- endif %} - } - {%- endif %} - {%- endfor %} - } - {%- endfor %} - on commit { - set shared-networkname = "{{ network.name }}"; - } -} -{%- endif %} -{% endfor %} - -""" - -daemon_tmpl = """ -### Autogenerated by dhcpv6_server.py ### - -# sourced by /etc/init.d/isc-dhcpv6-server - -DHCPD_CONF={{ config_file }} -DHCPD_PID={{ pid_file }} -OPTIONS="-6 -lf {{ lease_file }}" -INTERFACES="" -""" - default_config_data = { 'lease_file': lease_file, 'preference': '', @@ -134,7 +39,7 @@ default_config_data = { } def get_config(): - dhcpv6 = default_config_data + dhcpv6 = deepcopy(default_config_data) conf = Config() if not conf.exists('service dhcpv6-server'): return None @@ -409,7 +314,7 @@ def verify(dhcpv6): # There must be one subnet connected to a listen interface if network is not disabled. if not network['disabled']: - if vyos.validate.is_subnet_connected(subnet['network']): + if is_subnet_connected(subnet['network']): listen_ok = True # DHCPv6 subnet must not overlap. ISC DHCP also complains about overlapping @@ -437,12 +342,17 @@ def generate(dhcpv6): print('Warning: DHCPv6 server will be deactivated because it is disabled') return None - tmpl = jinja2.Template(config_tmpl) + # 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 = jinja2.Template(daemon_tmpl) + tmpl = env.get_template('daemon.tmpl') config_text = tmpl.render(dhcpv6) with open(daemon_config_file, 'w') as f: f.write(config_text) @@ -474,4 +384,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index 38f3cb4de..bbb69cdf7 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,76 +13,26 @@ # # 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 argparse -import jinja2 -import netifaces -import vyos.util -import vyos.hostsd_client +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 - parser = argparse.ArgumentParser() parser.add_argument("--dhclient", action="store_true", help="Started from dhclient-script") config_file = r'/etc/powerdns/recursor.conf' -# XXX: pdns recursor doesn't like whitespace near entry separators, -# especially in the semicolon-separated lists of name servers. -# Please be careful if you edit the template. -config_tmpl = """ -### Autogenerated by dns_forwarding.py ### - -# Non-configurable defaults -daemon=yes -threads=1 -allow-from={{ allow_from | join(',') }} -log-common-errors=yes -non-local-bind=yes -query-local-address=0.0.0.0 -query-local-address6=:: - -# cache-size -max-cache-entries={{ cache_size }} - -# negative TTL for NXDOMAIN -max-negative-ttl={{ negative_ttl }} - -# ignore-hosts-file -export-etc-hosts={{ export_hosts_file }} - -# listen-on -local-address={{ listen_on | join(',') }} - -# dnssec -dnssec={{ dnssec }} - -# forward-zones / recursion -# -# statement is only inserted if either one forwarding domain or nameserver is configured -# if nothing is given at all, powerdns will act as a real recursor and resolve all requests by its own -# -{% if name_servers or domains %}forward-zones-recurse= -{%- for d in domains %} -{{ d.name }}={{ d.servers | join(";") }} -{{- ", " if not loop.last -}} -{%- endfor -%} -{%- if name_servers -%} -{%- if domains -%}, {% endif -%}.={{ name_servers | join(';') }} -{% endif %} -{% endif %} - -""" - default_config_data = { 'allow_from': [], 'cache_size': 10000, @@ -96,74 +46,74 @@ default_config_data = { def get_config(arguments): - dns = default_config_data + dns = deepcopy(default_config_data) conf = Config() + base = ['service', 'dns', 'forwarding'] if arguments.dhclient: conf.exists = conf.exists_effective conf.return_value = conf.return_effective_value conf.return_values = conf.return_effective_values - if not conf.exists('service dns forwarding'): + if not conf.exists(base): return None - conf.set_level('service dns forwarding') + conf.set_level(base) - if conf.exists('allow-from'): - dns['allow_from'] = conf.return_values('allow-from') + if conf.exists(['allow-from']): + dns['allow_from'] = conf.return_values(['allow-from']) - if conf.exists('cache-size'): - cache_size = conf.return_value('cache-size') + if conf.exists(['cache-size']): + cache_size = conf.return_value(['cache-size']) dns['cache_size'] = cache_size if conf.exists('negative-ttl'): - negative_ttl = conf.return_value('negative-ttl') + negative_ttl = conf.return_value(['negative-ttl']) dns['negative_ttl'] = negative_ttl - if conf.exists('domain'): - for node in conf.list_nodes('domain'): - servers = conf.return_values("domain {0} server".format(node)) + if conf.exists(['domain']): + for node in conf.list_nodes(['domain']): + servers = conf.return_values(['domain', node, 'server']) domain = { "name": node, "servers": bracketize_ipv6_addrs(servers) } dns['domains'].append(domain) - if conf.exists('ignore-hosts-file'): + if conf.exists(['ignore-hosts-file']): dns['export_hosts_file'] = "no" - if conf.exists('name-server'): - name_servers = conf.return_values('name-server') + if conf.exists(['name-server']): + name_servers = conf.return_values(['name-server']) dns['name_servers'] = dns['name_servers'] + name_servers - if conf.exists('system'): - conf.set_level('system') + if conf.exists(['system']): + conf.set_level(['system']) system_name_servers = [] - system_name_servers = conf.return_values('name-server') + system_name_servers = conf.return_values(['name-server']) if not system_name_servers: - print( - "DNS forwarding warning: No name-servers set under 'system name-server'\n") + print("DNS forwarding warning: No name-servers set under 'system name-server'\n") else: dns['name_servers'] = dns['name_servers'] + system_name_servers - conf.set_level('service dns forwarding') + conf.set_level(base) dns['name_servers'] = bracketize_ipv6_addrs(dns['name_servers']) - if conf.exists('listen-address'): - dns['listen_on'] = conf.return_values('listen-address') + if conf.exists(['listen-address']): + dns['listen_on'] = conf.return_values(['listen-address']) - if conf.exists('dnssec'): - dns['dnssec'] = conf.return_value('dnssec') + if conf.exists(['dnssec']): + dns['dnssec'] = conf.return_value(['dnssec']) # Add name servers received from DHCP - if conf.exists('dhcp'): + if conf.exists(['dhcp']): interfaces = [] - interfaces = conf.return_values('dhcp') - hc = vyos.hostsd_client.Client() + interfaces = conf.return_values(['dhcp']) + hc = hostsd_client() for interface in interfaces: - dhcp_resolvers = hc.get_name_servers("dhcp-{0}".format(interface)) - dhcpv6_resolvers = hc.get_name_servers("dhcpv6-{0}".format(interface)) + dhcp_resolvers = hc.get_name_servers(f'dhcp-{interface}') + dhcpv6_resolvers = hc.get_name_servers(f'dhcpv6-{interface}') if dhcp_resolvers: dns['name_servers'] = dns['name_servers'] + dhcp_resolvers @@ -202,7 +152,12 @@ def generate(dns): if dns is None: return None - tmpl = jinja2.Template(config_tmpl, trim_blocks=True) + # 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) + + tmpl = env.get_template('recursor.conf.tmpl') config_text = tmpl.render(dns) with open(config_file, 'w') as f: f.write(config_text) @@ -223,7 +178,7 @@ if __name__ == '__main__': if args.dhclient: # There's a big chance it was triggered by a commit still in progress # so we need to wait until the new values are in the running config - vyos.util.wait_for_commit_lock() + wait_for_commit_lock() try: c = get_config(args) @@ -232,4 +187,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py index faf0663ab..56ce4fedc 100755 --- a/src/conf_mode/dynamic_dns.py +++ b/src/conf_mode/dynamic_dns.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2019 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -15,68 +15,20 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import sys -import jinja2 +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 config_file = r'/etc/ddclient/ddclient.conf' cache_file = r'/var/cache/ddclient/ddclient.cache' pid_file = r'/var/run/ddclient/ddclient.pid' -config_tmpl = """ -### Autogenerated by dynamic_dns.py ### -daemon=1m -syslog=yes -ssl=yes -pid={{ pid_file }} -cache={{ cache_file }} - -{% for interface in interfaces -%} - -# -# ddclient configuration for interface "{{ interface.interface }}": -# -{% if interface.web_url -%} -use=web, web='{{ interface.web_url}}' {%- if interface.web_skip %}, web-skip='{{ interface.web_skip }}'{% endif %} -{% else -%} -use=if, if={{ interface.interface }} -{% endif -%} - -{% for rfc in interface.rfc2136 -%} -{% for record in rfc.record %} -# RFC2136 dynamic DNS configuration for {{ record }}.{{ rfc.zone }} -server={{ rfc.server }} -protocol=nsupdate -password={{ rfc.keyfile }} -ttl={{ rfc.ttl }} -zone={{ rfc.zone }} -{{ record }} -{% endfor -%} -{% endfor -%} - -{% for srv in interface.service %} -{% for host in srv.host %} -# DynDNS provider configuration for {{ host }} -protocol={{ srv.protocol }}, -max-interval=28d, -login={{ srv.login }}, -password='{{ srv.password }}', -{% if srv.server -%} -server={{ srv.server }}, -{% endif -%} -{% if srv.zone -%} -zone={{ srv.zone }}, -{% endif -%} -{{ host }} -{% endfor %} -{% endfor %} - -{% endfor %} -""" - # Mapping of service name to service protocol default_service_protocol = { 'afraid': 'freedns', @@ -100,7 +52,7 @@ default_config_data = { } def get_config(): - dyndns = default_config_data + dyndns = deepcopy(default_config_data) conf = Config() base_level = ['service', 'dns', 'dynamic'] @@ -200,17 +152,17 @@ def get_config(): # Set config back to appropriate level for these options conf.set_level(base_level + ['interface', interface]) - + # Additional settings in CLI if conf.exists(['use-web', 'skip']): node['web_skip'] = conf.return_value(['use-web', 'skip']) if conf.exists(['use-web', 'url']): node['web_url'] = conf.return_value(['use-web', 'url']) - + # set config level back to top level conf.set_level(base_level) - + dyndns['interfaces'].append(node) return dyndns @@ -272,6 +224,11 @@ def generate(dyndns): 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) @@ -280,7 +237,7 @@ def generate(dyndns): if not os.path.exists(dirname): os.mkdir(dirname) - tmpl = jinja2.Template(config_tmpl) + tmpl = env.get_template('ddclient.conf.tmpl') config_text = tmpl.render(dyndns) with open(config_file, 'w') as f: f.write(config_text) @@ -314,4 +271,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py index 2e941de0a..b040c8b64 100755 --- a/src/conf_mode/flow_accounting_conf.py +++ b/src/conf_mode/flow_accounting_conf.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,17 +13,19 @@ # # 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 ipaddress import subprocess +from ipaddress import ip_address +from jinja2 import FileSystemLoader, Environment +from sys import exit + +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.ifconfig import Interface -from jinja2 import Template # default values default_sflow_server_port = 6343 @@ -36,78 +38,6 @@ uacctd_conf_path = '/etc/pmacct/uacctd.conf' iptables_nflog_table = 'raw' iptables_nflog_chain = 'VYATTA_CT_PREROUTING_HOOK' -# pmacct config template -uacct_conf_jinja = '''# Genereated from VyOS configuration -daemonize: true -promisc: false -pidfile: /var/run/uacctd.pid -uacctd_group: 2 -uacctd_nl_size: 2097152 -snaplen: {{ snaplen }} -aggregate: in_iface,src_mac,dst_mac,vlan,src_host,dst_host,src_port,dst_port,proto,tos,flows -plugin_pipe_size: {{ templatecfg['plugin_pipe_size'] }} -plugin_buffer_size: {{ templatecfg['plugin_buffer_size'] }} -{%- if templatecfg['syslog-facility'] != none %} -syslog: {{ templatecfg['syslog-facility'] }} -{%- endif %} -{%- if templatecfg['disable-imt'] == none %} -imt_path: /tmp/uacctd.pipe -imt_mem_pools_number: 169 -{%- endif %} -plugins: -{%- if templatecfg['netflow']['servers'] != none -%} - {% for server in templatecfg['netflow']['servers'] %} - {%- if loop.last -%}nfprobe[nf_{{ server['address'] }}]{%- else %}nfprobe[nf_{{ server['address'] }}],{%- endif %} - {%- endfor -%} - {% set plugins_presented = true %} -{%- endif %} -{%- if templatecfg['sflow']['servers'] != none -%} - {% if plugins_presented -%} - {%- for server in templatecfg['sflow']['servers'] -%} - ,sfprobe[sf_{{ server['address'] }}] - {%- endfor %} - {%- else %} - {%- for server in templatecfg['sflow']['servers'] %} - {%- if loop.last -%}sfprobe[sf_{{ server['address'] }}]{%- else %}sfprobe[sf_{{ server['address'] }}],{%- endif %} - {%- endfor %} - {%- endif -%} - {% set plugins_presented = true %} -{%- endif %} -{%- if templatecfg['disable-imt'] == none %} - {%- if plugins_presented -%},memory{%- else %}memory{%- endif %} -{%- endif %} -{%- if templatecfg['netflow']['servers'] != none %} -{%- for server in templatecfg['netflow']['servers'] %} -nfprobe_receiver[nf_{{ server['address'] }}]: {{ server['address'] }}:{{ server['port'] }} -nfprobe_version[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['version'] }} -{%- if templatecfg['netflow']['engine-id'] != none %} -nfprobe_engine[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['engine-id'] }} -{%- endif %} -{%- if templatecfg['netflow']['max-flows'] != none %} -nfprobe_maxflows[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['max-flows'] }} -{%- endif %} -{%- if templatecfg['netflow']['sampling-rate'] != none %} -sampling_rate[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['sampling-rate'] }} -{%- endif %} -{%- if templatecfg['netflow']['source-ip'] != none %} -nfprobe_source_ip[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['source-ip'] }} -{%- endif %} -{%- if templatecfg['netflow']['timeout_string'] != '' %} -nfprobe_timeouts[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['timeout_string'] }} -{%- endif %} -{%- endfor %} -{%- endif %} -{%- if templatecfg['sflow']['servers'] != none %} -{%- for server in templatecfg['sflow']['servers'] %} -sfprobe_receiver[sf_{{ server['address'] }}]: {{ server['address'] }}:{{ server['port'] }} -sfprobe_agentip[sf_{{ server['address'] }}]: {{ templatecfg['sflow']['agent-address'] }} -{%- if templatecfg['sflow']['sampling-rate'] != none %} -sampling_rate[sf_{{ server['address'] }}]: {{ templatecfg['sflow']['sampling-rate'] }} -{%- endif %} -{%- endfor %} -{% endif %} -''' - # helper functions # check if node exists and return True if this is true def _node_exists(path): @@ -154,7 +84,7 @@ def _iptables_get_nflog(): stdout, stderr = process.communicate() if not process.returncode == 0: print("Failed to get flows list: command \"{}\" returned exit code: {}\nError: {}".format(command, process.returncode, stderr)) - sys.exit(1) + exit(1) iptables_out = stdout.splitlines() # parse each line and add information to list @@ -179,7 +109,7 @@ def _iptables_config(configured_ifaces): # get currently configured interfaces with iptables rules active_nflog_rules = _iptables_get_nflog() - + # compare current active list with configured one and delete excessive interfaces, add missed active_nflog_ifaces = [] for rule in active_nflog_rules: @@ -314,15 +244,15 @@ def verify(config): sflow_collector_ipver = None for sflow_collector in config['sflow']['servers']: if sflow_collector_ipver: - if sflow_collector_ipver != ipaddress.ip_address(sflow_collector['address']).version: + if sflow_collector_ipver != ip_address(sflow_collector['address']).version: raise ConfigError("All sFlow servers must use the same IP protocol") else: - sflow_collector_ipver = ipaddress.ip_address(sflow_collector['address']).version + sflow_collector_ipver = ip_address(sflow_collector['address']).version # check agent-id for sFlow: we should avoid mixing IPv4 agent-id with IPv6 collectors and vice-versa for sflow_collector in config['sflow']['servers']: - if ipaddress.ip_address(sflow_collector['address']).version != ipaddress.ip_address(config['sflow']['agent-address']).version: + if ip_address(sflow_collector['address']).version != ip_address(config['sflow']['agent-address']).version: raise ConfigError("Different IP address versions cannot be mixed in \"sflow agent-address\" and \"sflow server\". You need to set manually the same IP version for \"agent-address\" as for all sFlow servers") # check if configured sFlow agent-id exist in the system @@ -378,7 +308,7 @@ def generate(config): # skip all checks if flow-accounting was removed if not config['flow-accounting-configured']: return True - + # Calculate all necessary values if config['buffer-size']: # circular queue size @@ -399,13 +329,16 @@ def generate(config): timeout_string = "{}:{}={}".format(timeout_string, timeout_type, timeout_value) config['netflow']['timeout_string'] = timeout_string - # Generate daemon configs - uacct_conf_template = Template(uacct_conf_jinja) - uacct_conf_file_data = uacct_conf_template.render(templatecfg = config, snaplen = default_captured_packet_size) + # 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) - # save generated config to uacctd.conf + # 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(uacct_conf_file_data) + file.write(config_text) def apply(config): @@ -436,4 +369,4 @@ if __name__ == '__main__': apply(config) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index 889b62cf4..83a5f3602 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.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,88 +13,22 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import os -from copy import deepcopy -import jinja2 +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 config_file = '/etc/nginx/sites-available/default' -# Please be careful if you edit the template. -config_tmpl = """ - -### Autogenerated by https.py ### -# Default server configuration -# -server { - listen 80 default_server; - listen [::]:80 default_server; - server_name _; - return 301 https://$server_name$request_uri; -} - -{% for server in server_block_list %} -server { - - # SSL configuration - # -{% if server.address == '*' %} - listen {{ server.port }} ssl; - listen [::]:{{ server.port }} ssl; -{% else %} - listen {{ server.address }}:{{ server.port }} ssl; -{% endif %} - -{% for name in server.name %} - server_name {{ name }}; -{% endfor %} - -{% if server.certbot %} - ssl_certificate /etc/letsencrypt/live/{{ server.certbot_dir }}/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/{{ server.certbot_dir }}/privkey.pem; - include /etc/letsencrypt/options-ssl-nginx.conf; - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; -{% elif server.vyos_cert %} - include {{ server.vyos_cert.conf }}; -{% else %} - # - # Self signed certs generated by the ssl-cert package - # Don't use them in a production server! - # - include snippets/snakeoil.conf; -{% endif %} - - # proxy settings for HTTP API, if enabled; 503, if not - location ~ /(retrieve|configure|config-file|image|generate|show) { -{% if server.api %} - proxy_pass http://localhost:{{ server.api.port }}; - proxy_buffering off; -{% else %} - return 503; -{% endif %} - } - - error_page 501 502 503 =200 @50*_json; - - location @50*_json { - default_type application/json; - return 200 '{"error": "Start service in configuration mode: set service https api"}'; - } - -} - -{% endfor %} -""" - default_server_block = { 'id' : '', 'address' : '*', @@ -193,10 +127,15 @@ 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 = jinja2.Template(config_tmpl, trim_blocks=True) + tmpl = env.get_template('nginx.default.tmpl') config_text = tmpl.render(https) with open(config_file, 'w') as f: f.write(config_text) @@ -217,4 +156,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/igmp_proxy.py b/src/conf_mode/igmp_proxy.py index cd0704124..aa46f2c4e 100755 --- a/src/conf_mode/igmp_proxy.py +++ b/src/conf_mode/igmp_proxy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,60 +13,20 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import os -import jinja2 + +from 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 config_file = r'/etc/igmpproxy.conf' -# Please be careful if you edit the template. -config_tmpl = """ -######################################################## -# -# autogenerated by igmp_proxy.py -# -# The configuration file must define one upstream -# interface, and one or more downstream interfaces. -# -# If multicast traffic originates outside the -# upstream subnet, the "altnet" option can be -# used in order to define legal multicast sources. -# (Se example...) -# -# The "quickleave" should be used to avoid saturation -# of the upstream link. The option should only -# be used if it's absolutely nessecary to -# accurately imitate just one Client. -# -######################################################## - -{% if not disable_quickleave -%} -quickleave -{% endif -%} - -{% for interface in interfaces %} -# Configuration for {{ interface.name }} ({{ interface.role }} interface) -{% if interface.role == 'disabled' -%} -phyint {{ interface.name }} disabled -{%- else -%} -phyint {{ interface.name }} {{ interface.role }} ratelimit 0 threshold {{ interface.threshold }} -{%- endif -%} -{%- for subnet in interface.alt_subnet %} - altnet {{ subnet }} -{%- endfor %} -{%- for subnet in interface.whitelist %} - whitelist {{ subnet }} -{%- endfor %} -{% endfor %} -""" - default_config_data = { 'disable': False, 'disable_quickleave': False, @@ -74,23 +34,24 @@ default_config_data = { } def get_config(): - igmp_proxy = default_config_data + igmp_proxy = deepcopy(default_config_data) conf = Config() - if not conf.exists('protocols igmp-proxy'): + base = ['protocols', 'igmp-proxy'] + if not conf.exists(base): return None else: - conf.set_level('protocols igmp-proxy') + conf.set_level(base) # Network interfaces to listen on - if conf.exists('disable'): + if conf.exists(['disable']): igmp_proxy['disable'] = True # Option to disable "quickleave" - if conf.exists('disable-quickleave'): + if conf.exists(['disable-quickleave']): igmp_proxy['disable_quickleave'] = True - for intf in conf.list_nodes('interface'): - conf.set_level('protocols igmp-proxy interface {0}'.format(intf)) + for intf in conf.list_nodes(['interface']): + conf.set_level(base + ['interface', intf]) interface = { 'name': intf, 'alt_subnet': [], @@ -99,17 +60,17 @@ def get_config(): 'whitelist': [] } - if conf.exists('alt-subnet'): - interface['alt_subnet'] = conf.return_values('alt-subnet') + if conf.exists(['alt-subnet']): + interface['alt_subnet'] = conf.return_values(['alt-subnet']) - if conf.exists('role'): - interface['role'] = conf.return_value('role') + if conf.exists(['role']): + interface['role'] = conf.return_value(['role']) - if conf.exists('threshold'): - interface['threshold'] = conf.return_value('threshold') + if conf.exists(['threshold']): + interface['threshold'] = conf.return_value(['threshold']) - if conf.exists('whitelist'): - interface['whitelist'] = conf.return_values('whitelist') + if conf.exists(['whitelist']): + interface['whitelist'] = conf.return_values(['whitelist']) # Append interface configuration to global configuration list igmp_proxy['interfaces'].append(interface) @@ -153,7 +114,12 @@ def generate(igmp_proxy): print('Warning: IGMP Proxy will be deactivated because it is disabled') return None - tmpl = jinja2.Template(config_tmpl) + # 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) @@ -167,7 +133,7 @@ def apply(igmp_proxy): if os.path.exists(config_file): os.unlink(config_file) else: - os.system('sudo systemctl restart igmpproxy.service') + os.system('systemctl restart igmpproxy.service') return None @@ -179,4 +145,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index fb2d6e6d9..faaee9ac0 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.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 @@ -17,7 +17,7 @@ import os import re -from jinja2 import Template +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 @@ -29,257 +29,16 @@ from subprocess import Popen, PIPE from time import sleep from shutil import rmtree -from vyos import ConfigError 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 from vyos.validate import is_addr_assigned +from vyos import ConfigError user = 'openvpn' group = 'openvpn' -# Please be careful if you edit the template. -config_tmpl = """ -### Autogenerated by interfaces-openvpn.py ### -# -# See https://community.openvpn.net/openvpn/wiki/Openvpn24ManPage -# for individual keyword definition - -{% if description %} -# {{ description }} -{% endif %} - -verb 3 -status /opt/vyatta/etc/openvpn/status/{{ intf }}.status 30 -writepid /var/run/openvpn/{{ intf }}.pid - -dev-type {{ type }} -dev {{ intf }} -user {{ uid }} -group {{ gid }} -persist-key -iproute /usr/libexec/vyos/system/unpriv-ip - -proto {% if 'tcp-active' in protocol -%}tcp-client{% elif 'tcp-passive' in protocol -%}tcp-server{% else %}udp{% endif %} - -{%- if local_host %} -local {{ local_host }} -{% endif %} - -{%- if local_port %} -lport {{ local_port }} -{% endif %} - -{%- if remote_port %} -rport {{ remote_port }} -{% endif %} - -{%- if remote_host %} -{% for remote in remote_host -%} -remote {{ remote }} -{% endfor -%} -{% endif %} - -{%- if shared_secret_file %} -secret {{ shared_secret_file }} -{% endif %} - -{%- if persistent_tunnel %} -persist-tun -{% endif %} - -{%- if mode %} -{%- if 'client' in mode %} -# -# OpenVPN Client mode -# -client -nobind -{%- elif 'server' in mode %} -# -# OpenVPN Server mode -# -mode server -tls-server -keepalive {{ ping_interval }} {{ ping_restart }} -management /tmp/openvpn-mgmt-intf unix - -{%- if server_topology %} -topology {% if 'point-to-point' in server_topology %}p2p{% else %}subnet{% endif %} -{% endif %} - -{% for ns in server_dns_nameserver -%} -push "dhcp-option DNS {{ ns }}" -{% endfor -%} - -{% for route in server_push_route -%} -push "route {{ route }}" -{% endfor -%} - -{%- if server_domain %} -push "dhcp-option DOMAIN {{ server_domain }}" -{% endif %} - -{%- if server_max_conn %} -max-clients {{ server_max_conn }} -{% endif %} - -{%- if bridge_member %} -server-bridge nogw -{%- else %} -server {{ server_subnet }} -{% endif %} - -{%- if server_reject_unconfigured %} -ccd-exclusive -{% endif %} - -{%- else %} -# -# OpenVPN site-2-site mode -# -ping {{ ping_interval }} -ping-restart {{ ping_restart }} - -{%- if local_address_subnet %} -ifconfig {{ local_address }} {{ local_address_subnet }} -{% elif remote_address %} -ifconfig {{ local_address }} {{ remote_address }} -{% endif %} - -{% endif %} -{% endif %} - -{%- if tls_ca_cert %} -ca {{ tls_ca_cert }} -{% endif %} - -{%- if tls_cert %} -cert {{ tls_cert }} -{% endif %} - -{%- if tls_key %} -key {{ tls_key }} -{% endif %} - -{%- if tls_crypt %} -tls-crypt {{ tls_crypt }} -{% endif %} - -{%- if tls_crl %} -crl-verify {{ tls_crl }} -{% endif %} - -{%- if tls_version_min %} -tls-version-min {{tls_version_min}} -{% endif %} - -{%- if tls_dh %} -dh {{ tls_dh }} -{% endif %} - -{%- if tls_auth %} -tls-auth {{tls_auth}} -{% endif %} - -{%- if 'active' in tls_role %} -tls-client -{%- elif 'passive' in tls_role %} -tls-server -{% endif %} - -{%- if redirect_gateway %} -push "redirect-gateway {{ redirect_gateway }}" -{% endif %} - -{%- if compress_lzo %} -compress lzo -{% endif %} - -{%- if hash %} -auth {{ hash }} -{% endif %} - -{%- if encryption %} -{%- if 'des' in encryption %} -cipher des-cbc -{%- elif '3des' in encryption %} -cipher des-ede3-cbc -{%- elif 'bf128' in encryption %} -cipher bf-cbc -keysize 128 -{%- elif 'bf256' in encryption %} -cipher bf-cbc -keysize 25 -{%- elif 'aes128gcm' in encryption %} -cipher aes-128-gcm -{%- elif 'aes128' in encryption %} -cipher aes-128-cbc -{%- elif 'aes192gcm' in encryption %} -cipher aes-192-gcm -{%- elif 'aes192' in encryption %} -cipher aes-192-cbc -{%- elif 'aes256gcm' in encryption %} -cipher aes-256-gcm -{%- elif 'aes256' in encryption %} -cipher aes-256-cbc -{% endif %} -{% endif %} - -{%- if ncp_ciphers %} -ncp-ciphers {{ncp_ciphers}} -{% endif %} -{%- if disable_ncp %} -ncp-disable -{% endif %} - -{%- if auth %} -auth-user-pass /tmp/openvpn-{{ intf }}-pw -auth-retry nointeract -{% endif %} - -{%- if client %} -client-config-dir /opt/vyatta/etc/openvpn/ccd/{{ intf }} -{% endif %} - -# DEPRECATED This option will be removed in OpenVPN 2.5 -# Until OpenVPN v2.3 the format of the X.509 Subject fields was formatted like this: -# /C=US/L=Somewhere/CN=John Doe/emailAddress=john@example.com In addition the old -# behaviour was to remap any character other than alphanumeric, underscore ('_'), -# dash ('-'), dot ('.'), and slash ('/') to underscore ('_'). The X.509 Subject -# string as returned by the tls_id environmental variable, could additionally -# contain colon (':') or equal ('='). When using the --compat-names option, this -# old formatting and remapping will be re-enabled again. This is purely implemented -# for compatibility reasons when using older plug-ins or scripts which does not -# handle the new formatting or UTF-8 characters. -# -# See https://phabricator.vyos.net/T1512 -compat-names - -{% for option in options -%} -{{ option }} -{% endfor -%} -""" - -client_tmpl = """ -### Autogenerated by interfaces-openvpn.py ### - -{% if ip -%} -ifconfig-push {{ ip }} {{ remote_netmask }} -{% endif -%} -{% for route in push_route -%} -push "route {{ route }}" -{% endfor -%} - -{% for net in subnet -%} -iroute {{ net }} -{% endfor -%} - -{% if disable -%} -disable -{% endif -%} -""" - default_config_data = { 'address': [], 'auth_user': '', @@ -308,7 +67,7 @@ default_config_data = { 'ncp_ciphers': '', 'options': [], 'persistent_tunnel': False, - 'protocol': '', + 'protocol': 'udp', 'redirect_gateway': '', 'remote_address': '', 'remote_host': [], @@ -512,8 +271,7 @@ def get_config(): # OpenVPN operation mode if conf.exists('mode'): - mode = conf.return_value('mode') - openvpn['mode'] = mode + openvpn['mode'] = conf.return_value('mode') # Additional OpenVPN options if conf.exists('openvpn-option'): @@ -917,6 +675,11 @@ 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)) @@ -957,19 +720,17 @@ def generate(openvpn): # Generate client specific configuration for client in openvpn['client']: client_file = directory + '/ccd/' + interface + '/' + client['name'] - tmpl = Template(client_tmpl) + 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 = Template(config_tmpl) + tmpl = env.get_template('server.conf.tmpl') config_text = tmpl.render(openvpn) - # 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) diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py index 9c045534c..a396af4ea 100755 --- a/src/conf_mode/interfaces-pppoe.py +++ b/src/conf_mode/interfaces-pppoe.py @@ -18,153 +18,14 @@ import os from sys import exit from copy import deepcopy -from jinja2 import Template -from subprocess import Popen, PIPE +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_file +from vyos.util import chown_file, chmod_x, subprocess_cmd from vyos import ConfigError -from netifaces import interfaces - -# Please be careful if you edit the template. -config_pppoe_tmpl = """### Autogenerated by interfaces-pppoe.py ### -{% if description %} -# {{ description }} -{% endif %} - -# Require peer to provide the local IP address if it is not -# specified explicitly in the config file. -noipdefault - -# Don't show the password in logfiles: -hide-password - -# Standard Link Control Protocol (LCP) parameters: -lcp-echo-interval 20 -lcp-echo-failure 3 - -# RFC 2516, paragraph 7 mandates that the following options MUST NOT be -# requested and MUST be rejected if requested by the peer: -# Address-and-Control-Field-Compression (ACFC) -noaccomp - -# Asynchronous-Control-Character-Map (ACCM) -default-asyncmap - -# Override any connect script that may have been set in /etc/ppp/options. -connect /bin/true - -# Don't try to authenticate the remote node -noauth - -# Don't try to proxy ARP for the remote endpoint. User can set proxy -# arp entries up manually if they wish. More importantly, having -# the "proxyarp" parameter set disables the "defaultroute" option. -noproxyarp - -# Unlimited connection attempts -maxfail 0 - -plugin rp-pppoe.so -{{ source_interface }} -persist -ifname {{ intf }} -ipparam {{ intf }} -debug -logfile {{ logfile }} -{% if 'auto' in default_route -%} -defaultroute -{% elif 'force' in default_route -%} -defaultroute -replacedefaultroute -{% endif %} -mtu {{ mtu }} -mru {{ mtu }} -user "{{ auth_username }}" -password "{{ auth_password }}" -{% if name_server -%} -usepeerdns -{% endif %} -{% if ipv6_enable -%} -+ipv6 -ipv6cp-use-ipaddr -{% endif %} -{% if service_name -%} -rp_pppoe_service "{{ service_name }}" -{% endif %} -{% if on_demand %} -demand -{% endif %} - -""" - -# Please be careful if you edit the template. -# There must be no blank line at the top pf the script file -config_pppoe_ipv6_up_tmpl = """#!/bin/sh - -# As PPPoE is an "on demand" interface we need to re-configure it when it -# becomes up - -if [ "$6" != "{{ intf }}" ]; then - exit -fi - -{% if ipv6_autoconf -%} -# add some info to syslog -DIALER_PID=$(cat /var/run/{{ intf }}.pid) -logger -t pppd[$DIALER_PID] "executing $0" -logger -t pppd[$DIALER_PID] "configuring interface {{ intf }} via $2" - -# Configure interface-specific Host/Router behaviour. -# Note: It is recommended to have the same setting on all interfaces; mixed -# router/host scenarios are rather uncommon. Possible values are: -# -# 0 Forwarding disabled -# 1 Forwarding enabled -# -echo 1 > /proc/sys/net/ipv6/conf/{{ intf }}/forwarding - -# Accept Router Advertisements; autoconfigure using them. -# -# It also determines whether or not to transmit Router -# Solicitations. If and only if the functional setting is to -# accept Router Advertisements, Router Solicitations will be -# transmitted. Possible values are: -# -# 0 Do not accept Router Advertisements. -# 1 Accept Router Advertisements if forwarding is disabled. -# 2 Overrule forwarding behaviour. Accept Router Advertisements -# even if forwarding is enabled. -# -echo 2 > /proc/sys/net/ipv6/conf/{{ intf }}/accept_ra - -# Autoconfigure addresses using Prefix Information in Router Advertisements. -echo 1 > /proc/sys/net/ipv6/conf/{{ intf }}/autoconfigure -{% endif %} -""" - -config_pppoe_ip_up_tmpl = """#!/bin/sh - -# As PPPoE is an "on demand" interface we need to re-configure it when it -# becomes up - -if [ "$6" != "{{ intf }}" ]; then - exit -fi - -# add some info to syslog -DIALER_PID=$(cat /var/run/{{ intf }}.pid) -logger -t pppd[$DIALER_PID] "executing $0" - -echo "{{ description }}" > /sys/class/net/{{ intf }}/ifalias - -{% if vrf -%} -logger -t pppd[$DIALER_PID] "configuring dialer interface $6 for VRF {{ vrf }}" -ip link set dev {{ intf }} master {{ vrf }} -{% endif %} - -""" default_config_data = { 'access_concentrator': '', @@ -189,10 +50,6 @@ default_config_data = { 'vrf': '' } -def subprocess_cmd(command): - p = Popen(command, stdout=PIPE, shell=True) - p.communicate() - def get_config(): pppoe = deepcopy(default_config_data) conf = Config() @@ -301,12 +158,22 @@ def verify(pppoe): return None def generate(pppoe): + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir["data"], "templates", "pppoe") + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader) + + # set up configuration file path variables where our templates will be + # rendered into intf = pppoe['intf'] - config_file_pppoe = f'/etc/ppp/peers/{intf}' - ip_up_script_file = f'/etc/ppp/ip-up.d/9990-vyos-vrf-{intf}' - ipv6_if_up_script_file = f'/etc/ppp/ipv6-up.d/9990-vyos-autoconf-{intf}' + config_pppoe = f'/etc/ppp/peers/{intf}' + script_pppoe_pre_up = f'/etc/ppp/ip-pre-up.d/1000-vyos-pppoe-{intf}' + script_pppoe_ip_up = f'/etc/ppp/ip-up.d/1000-vyos-pppoe-{intf}' + script_pppoe_ip_down = f'/etc/ppp/ip-down.d/1000-vyos-pppoe-{intf}' + script_pppoe_ipv6_up = f'/etc/ppp/ipv6-up.d/1000-vyos-pppoe-{intf}' - config_files = [config_file_pppoe, ip_up_script_file, ipv6_if_up_script_file] + config_files = [config_pppoe, script_pppoe_pre_up, script_pppoe_ip_up, + script_pppoe_ip_down, script_pppoe_ipv6_up] # Ensure directories for config files exist - otherwise create them on demand for file in config_files: @@ -326,24 +193,40 @@ def generate(pppoe): else: # Create PPP configuration files - tmpl = Template(config_pppoe_tmpl) + tmpl = env.get_template('peer.tmpl') + config_text = tmpl.render(pppoe) + with open(config_pppoe, 'w') as f: + f.write(config_text) + + # 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) + + # Create script for ip-up.d + tmpl = env.get_template('ip-up.script.tmpl') config_text = tmpl.render(pppoe) - with open(config_file_pppoe, 'w') as f: + with open(script_pppoe_ip_up, 'w') as f: f.write(config_text) - tmpl = Template(config_pppoe_ip_up_tmpl) + # Create script for ip-down.d + tmpl = env.get_template('ip-down.script.tmpl') config_text = tmpl.render(pppoe) - with open(ip_up_script_file, 'w') as f: + with open(script_pppoe_ip_down, 'w') as f: f.write(config_text) - tmpl = Template(config_pppoe_ipv6_up_tmpl) + # Create script for ipv6-up.d + tmpl = env.get_template('ipv6-up.script.tmpl') config_text = tmpl.render(pppoe) - with open(ipv6_if_up_script_file, 'w') as f: + with open(script_pppoe_ipv6_up, 'w') as f: f.write(config_text) # make generated script file executable - chmod_x_file(ip_up_script_file) - chmod_x_file(ipv6_if_up_script_file) + chmod_x(script_pppoe_pre_up) + chmod_x(script_pppoe_ip_up) + chmod_x(script_pppoe_ip_down) + chmod_x(script_pppoe_ipv6_up) return None diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index 3d2638c6f..1f9636729 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -160,9 +160,6 @@ def verify(vxlan): if not vxlan['link'] in interfaces(): raise ConfigError('VXLAN source interface does not exist') - if not (vxlan['group'] or vxlan['remote']): - raise ConfigError('Group or remote must be configured') - if not vxlan['vni']: raise ConfigError('Must configure VNI for VXLAN') diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index d8c327e19..5c0c07dc4 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,13 +13,12 @@ # # 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 subprocess + from copy import deepcopy from netifaces import interfaces @@ -30,10 +29,9 @@ from vyos.ifconfig import WireGuardIf kdir = r'/config/auth/wireguard' - def _check_kmod(): if not os.path.exists('/sys/module/wireguard'): - if os.system('sudo modprobe wireguard') != 0: + if os.system('modprobe wireguard') != 0: raise ConfigError("modprobe wireguard failed") @@ -135,7 +133,8 @@ def get_config(): { p: { 'allowed-ips': [], - 'endpoint': '', + 'address': '', + 'port': '', 'pubkey': '' } } @@ -144,10 +143,14 @@ def get_config(): if c.exists(['peer', p, 'allowed-ips']): wg['peer'][p]['allowed-ips'] = c.return_values( ['peer', p, 'allowed-ips']) - # peer endpoint - if c.exists(['peer', p, 'endpoint']): - wg['peer'][p]['endpoint'] = c.return_value( - ['peer', p, 'endpoint']) + # 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( @@ -251,8 +254,8 @@ def apply(c): if c['fwmark']: intfc.config['fwmark'] = c['fwmark'] # endpoint - if c['peer'][p]['endpoint']: - intfc.config['endpoint'] = c['peer'][p]['endpoint'] + if c['peer'][p]['address'] and c['peer'][p]['port']: + intfc.config['endpoint'] = "{}:{}".format(c['peer'][p]['address'], c['peer'][p]['port']) # persistent-keepalive if 'persistent-keepalive' in c['peer'][p]: diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index b6e62b0aa..da8470f7e 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -15,763 +15,27 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os - -from jinja2 import Template -from copy import deepcopy from sys import exit -from stat import S_IRWXU,S_IRGRP,S_IXGRP,S_IROTH,S_IXOTH -from pwd import getpwnam -from grp import getgrnam from re import findall +from copy import deepcopy +from jinja2 import FileSystemLoader, Environment + from subprocess import Popen, PIPE from netifaces import interfaces -from netaddr import * +from netaddr import EUI, mac_unix_expanded -from vyos import ConfigError -from vyos.configdict import list_diff, vlan_to_dict 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 +from vyos.util import process_running, chmod_x, chown_file +from vyos import ConfigError user = 'root' group = 'vyattacfg' -# Please be careful if you edit the template. -config_hostapd_tmpl = """ -### Autogenerated by interfaces-wireless.py ### -{% if description %} -# Description: {{ description }} -# User-friendly description of device; up to 32 octets encoded in UTF-8 -device_name={{ description | truncate(32, True) }} -{% endif %} - -# AP netdevice name (without 'ap' postfix, i.e., wlan0 uses wlan0ap for -# management frames with the Host AP driver); wlan0 with many nl80211 drivers -# Note: This attribute can be overridden by the values supplied with the '-i' -# command line parameter. -interface={{ intf }} - -# Driver interface type (hostap/wired/none/nl80211/bsd); -# default: hostap). nl80211 is used with all Linux mac80211 drivers. -# Use driver=none if building hostapd as a standalone RADIUS server that does -# not control any wireless/wired driver. -driver=nl80211 - -# Levels (minimum value for logged events): -# 0 = verbose debugging -# 1 = debugging -# 2 = informational messages -# 3 = notification -# 4 = warning -logger_syslog=-1 -logger_syslog_level=0 -logger_stdout=-1 -logger_stdout_level=0 - -{%- if country_code %} - -# Country code (ISO/IEC 3166-1). Used to set regulatory domain. -# Set as needed to indicate country in which device is operating. -# This can limit available channels and transmit power. -country_code={{ country_code }} - -# Enable IEEE 802.11d. This advertises the country_code and the set of allowed -# channels and transmit power levels based on the regulatory limits. The -# country_code setting must be configured with the correct country for -# IEEE 802.11d functions. -ieee80211d=1 -{% endif %} - -{%- if ssid %} - -# SSID to be used in IEEE 802.11 management frames -ssid={{ ssid }} -{% endif %} - -{%- if channel %} - -# Channel number (IEEE 802.11) -# (default: 0, i.e., not set) -# Please note that some drivers do not use this value from hostapd and the -# channel will need to be configured separately with iwconfig. -# -# If CONFIG_ACS build option is enabled, the channel can be selected -# automatically at run time by setting channel=acs_survey or channel=0, both of -# which will enable the ACS survey based algorithm. -channel={{ channel }} -{% endif %} - -{%- if mode %} - -# Operation mode (a = IEEE 802.11a (5 GHz), b = IEEE 802.11b (2.4 GHz), -# g = IEEE 802.11g (2.4 GHz), ad = IEEE 802.11ad (60 GHz); a/g options are used -# with IEEE 802.11n (HT), too, to specify band). For IEEE 802.11ac (VHT), this -# needs to be set to hw_mode=a. For IEEE 802.11ax (HE) on 6 GHz this needs -# to be set to hw_mode=a. When using ACS (see channel parameter), a -# special value "any" can be used to indicate that any support band can be used. -# This special case is currently supported only with drivers with which -# offloaded ACS is used. -{% if 'n' in mode -%} -hw_mode=g -ieee80211n=1 -{% elif 'ac' in mode -%} -hw_mode=a -ieee80211h=1 -ieee80211ac=1 -{% else -%} -hw_mode={{ mode }} -{% endif %} -{% endif %} - -# ieee80211w: Whether management frame protection (MFP) is enabled -# 0 = disabled (default) -# 1 = optional -# 2 = required -{% if 'disabled' in mgmt_frame_protection -%} -ieee80211w=0 -{% elif 'optional' in mgmt_frame_protection -%} -ieee80211w=1 -{% elif 'required' in mgmt_frame_protection -%} -ieee80211w=2 -{% endif %} - -# ht_capab: HT capabilities (list of flags) -# LDPC coding capability: [LDPC] = supported -# Supported channel width set: [HT40-] = both 20 MHz and 40 MHz with secondary -# channel below the primary channel; [HT40+] = both 20 MHz and 40 MHz -# with secondary channel above the primary channel -# (20 MHz only if neither is set) -# Note: There are limits on which channels can be used with HT40- and -# HT40+. Following table shows the channels that may be available for -# HT40- and HT40+ use per IEEE 802.11n Annex J: -# freq HT40- HT40+ -# 2.4 GHz 5-13 1-7 (1-9 in Europe/Japan) -# 5 GHz 40,48,56,64 36,44,52,60 -# (depending on the location, not all of these channels may be available -# for use) -# Please note that 40 MHz channels may switch their primary and secondary -# channels if needed or creation of 40 MHz channel maybe rejected based -# on overlapping BSSes. These changes are done automatically when hostapd -# is setting up the 40 MHz channel. -# Spatial Multiplexing (SM) Power Save: [SMPS-STATIC] or [SMPS-DYNAMIC] -# (SMPS disabled if neither is set) -# HT-greenfield: [GF] (disabled if not set) -# Short GI for 20 MHz: [SHORT-GI-20] (disabled if not set) -# Short GI for 40 MHz: [SHORT-GI-40] (disabled if not set) -# Tx STBC: [TX-STBC] (disabled if not set) -# Rx STBC: [RX-STBC1] (one spatial stream), [RX-STBC12] (one or two spatial -# streams), or [RX-STBC123] (one, two, or three spatial streams); Rx STBC -# disabled if none of these set -# HT-delayed Block Ack: [DELAYED-BA] (disabled if not set) -# Maximum A-MSDU length: [MAX-AMSDU-7935] for 7935 octets (3839 octets if not -# set) -# DSSS/CCK Mode in 40 MHz: [DSSS_CCK-40] = allowed (not allowed if not set) -# 40 MHz intolerant [40-INTOLERANT] (not advertised if not set) -# L-SIG TXOP protection support: [LSIG-TXOP-PROT] (disabled if not set) -{% if cap_ht %} -ht_capab= -{%- endif -%} - -{%- if cap_ht_40mhz_incapable -%} -[40-INTOLERANT] -{%- endif -%} - -{%- if cap_ht_delayed_block_ack -%} -[DELAYED-BA] -{%- endif -%} - -{%- if cap_ht_dsss_cck_40 -%} -[DSSS_CCK-40] -{%- endif -%} - -{%- if cap_ht_greenfield -%} -[GF] -{%- endif -%} - -{%- if cap_ht_ldpc -%} -[LDPC] -{%- endif -%} - -{%- if cap_ht_lsig_protection -%} -[LSIG-TXOP-PROT] -{%- endif -%} - -{%- if cap_ht_max_amsdu -%} -[MAX-AMSDU-{{ cap_ht_max_amsdu }}] -{%- endif -%} - -{%- if cap_ht_smps -%} -[SMPS-{{ cap_ht_smps | upper }}] -{%- endif -%} - -{%- if cap_ht_chan_set_width -%} -{%- for csw in cap_ht_chan_set_width -%} -[{{ csw | upper }}] -{%- endfor -%} -{%- endif -%} - -{%- if cap_ht_short_gi -%} -{%- for gi in cap_ht_short_gi -%} -[SHORT-GI-{{ gi }}] -{%- endfor -%} -{%- endif -%} - -{%- if cap_ht_stbc_tx -%} -[TX-STBC] -{%- endif -%} -{%- if cap_ht_stbc_rx -%} -[RX-STBC{{ cap_ht_stbc_rx }}] -{%- endif %} - -# Required for full HT and VHT functionality -wme_enabled=1 - -{% if cap_ht_powersave -%} -# WMM-PS Unscheduled Automatic Power Save Delivery [U-APSD] -# Enable this flag if U-APSD supported outside hostapd (eg., Firmware/driver) -uapsd_advertisement_enabled=1 -{%- endif %} - -{% if cap_req_ht -%} -# Require stations to support HT PHY (reject association if they do not) -require_ht=1 -{% endif %} - -# vht_capab: VHT capabilities (list of flags) -# -# vht_max_mpdu_len: [MAX-MPDU-7991] [MAX-MPDU-11454] -# Indicates maximum MPDU length -# 0 = 3895 octets (default) -# 1 = 7991 octets -# 2 = 11454 octets -# 3 = reserved -# -# supported_chan_width: [VHT160] [VHT160-80PLUS80] -# Indicates supported Channel widths -# 0 = 160 MHz & 80+80 channel widths are not supported (default) -# 1 = 160 MHz channel width is supported -# 2 = 160 MHz & 80+80 channel widths are supported -# 3 = reserved -# -# Rx LDPC coding capability: [RXLDPC] -# Indicates support for receiving LDPC coded pkts -# 0 = Not supported (default) -# 1 = Supported -# -# Short GI for 80 MHz: [SHORT-GI-80] -# Indicates short GI support for reception of packets transmitted with TXVECTOR -# params format equal to VHT and CBW = 80Mhz -# 0 = Not supported (default) -# 1 = Supported -# -# Short GI for 160 MHz: [SHORT-GI-160] -# Indicates short GI support for reception of packets transmitted with TXVECTOR -# params format equal to VHT and CBW = 160Mhz -# 0 = Not supported (default) -# 1 = Supported -# -# Tx STBC: [TX-STBC-2BY1] -# Indicates support for the transmission of at least 2x1 STBC -# 0 = Not supported (default) -# 1 = Supported -# -# Rx STBC: [RX-STBC-1] [RX-STBC-12] [RX-STBC-123] [RX-STBC-1234] -# Indicates support for the reception of PPDUs using STBC -# 0 = Not supported (default) -# 1 = support of one spatial stream -# 2 = support of one and two spatial streams -# 3 = support of one, two and three spatial streams -# 4 = support of one, two, three and four spatial streams -# 5,6,7 = reserved -# -# SU Beamformer Capable: [SU-BEAMFORMER] -# Indicates support for operation as a single user beamformer -# 0 = Not supported (default) -# 1 = Supported -# -# SU Beamformee Capable: [SU-BEAMFORMEE] -# Indicates support for operation as a single user beamformee -# 0 = Not supported (default) -# 1 = Supported -# -# Compressed Steering Number of Beamformer Antennas Supported: -# [BF-ANTENNA-2] [BF-ANTENNA-3] [BF-ANTENNA-4] -# Beamformee's capability indicating the maximum number of beamformer -# antennas the beamformee can support when sending compressed beamforming -# feedback -# If SU beamformer capable, set to maximum value minus 1 -# else reserved (default) -# -# Number of Sounding Dimensions: -# [SOUNDING-DIMENSION-2] [SOUNDING-DIMENSION-3] [SOUNDING-DIMENSION-4] -# Beamformer's capability indicating the maximum value of the NUM_STS parameter -# in the TXVECTOR of a VHT NDP -# If SU beamformer capable, set to maximum value minus 1 -# else reserved (default) -# -# MU Beamformer Capable: [MU-BEAMFORMER] -# Indicates support for operation as an MU beamformer -# 0 = Not supported or sent by Non-AP STA (default) -# 1 = Supported -# -# VHT TXOP PS: [VHT-TXOP-PS] -# Indicates whether or not the AP supports VHT TXOP Power Save Mode -# or whether or not the STA is in VHT TXOP Power Save mode -# 0 = VHT AP doesn't support VHT TXOP PS mode (OR) VHT STA not in VHT TXOP PS -# mode -# 1 = VHT AP supports VHT TXOP PS mode (OR) VHT STA is in VHT TXOP power save -# mode -# -# +HTC-VHT Capable: [HTC-VHT] -# Indicates whether or not the STA supports receiving a VHT variant HT Control -# field. -# 0 = Not supported (default) -# 1 = supported -# -# Maximum A-MPDU Length Exponent: [MAX-A-MPDU-LEN-EXP0]..[MAX-A-MPDU-LEN-EXP7] -# Indicates the maximum length of A-MPDU pre-EOF padding that the STA can recv -# This field is an integer in the range of 0 to 7. -# The length defined by this field is equal to -# 2 pow(13 + Maximum A-MPDU Length Exponent) -1 octets -# -# VHT Link Adaptation Capable: [VHT-LINK-ADAPT2] [VHT-LINK-ADAPT3] -# Indicates whether or not the STA supports link adaptation using VHT variant -# HT Control field -# If +HTC-VHTcapable is 1 -# 0 = (no feedback) if the STA does not provide VHT MFB (default) -# 1 = reserved -# 2 = (Unsolicited) if the STA provides only unsolicited VHT MFB -# 3 = (Both) if the STA can provide VHT MFB in response to VHT MRQ and if the -# STA provides unsolicited VHT MFB -# Reserved if +HTC-VHTcapable is 0 -# -# Rx Antenna Pattern Consistency: [RX-ANTENNA-PATTERN] -# Indicates the possibility of Rx antenna pattern change -# 0 = Rx antenna pattern might change during the lifetime of an association -# 1 = Rx antenna pattern does not change during the lifetime of an association -# -# Tx Antenna Pattern Consistency: [TX-ANTENNA-PATTERN] -# Indicates the possibility of Tx antenna pattern change -# 0 = Tx antenna pattern might change during the lifetime of an association -# 1 = Tx antenna pattern does not change during the lifetime of an association -{% if cap_vht %} -vht_capab= -{%- endif -%} - -{%- if cap_vht_max_mpdu -%} -[MAX-MPDU-{{ cap_vht_max_mpdu }}] -{%- endif -%} - -{%- if cap_vht_max_mpdu_exp -%} -[MAX-A-MPDU-LEN-EXP{{ cap_vht_max_mpdu_exp }}] -{%- endif -%} - -{%- if cap_vht_chan_set_width -%} -[MAX-A-MPDU-LEN-EXP{{ cap_vht_max_mpdu_exp }}] -{%- endif -%} - -{%- if cap_vht_chan_set_width -%} -{%- if '2' in cap_vht_chan_set_width -%} -[VHT160] -{%- elif '3' in cap_vht_chan_set_width -%} -[VHT160-80PLUS80] -{%- endif -%} -{%- endif -%} - -{%- if cap_vht_stbc_tx -%} -[TX-STBC-2BY1] -{%- endif -%} - -{%- if cap_vht_stbc_rx -%} -[RX-STBC-{{ cap_vht_stbc_rx }}] -{%- endif -%} - -{%- if cap_vht_link_adaptation -%} -{%- if 'unsolicited' in cap_vht_link_adaptation -%} -[VHT-LINK-ADAPT2] -{%- elif 'both' in cap_vht_link_adaptation -%} -[VHT-LINK-ADAPT3] -{%- endif -%} -{%- endif -%} - -{%- if cap_vht_short_gi -%} -{%- for gi in cap_vht_short_gi -%} -[SHORT-GI-{{ gi }}] -{%- endfor -%} -{%- endif -%} - -{%- if cap_vht_ldpc -%} -[RXLDPC] -{%- endif -%} - -{%- if cap_vht_tx_powersave -%} -[VHT-TXOP-PS] -{%- endif -%} - -{%- if cap_vht_vht_cf -%} -[HTC-VHT] -{%- endif -%} - -{%- if cap_vht_beamform -%} -{%- for beamform in cap_vht_beamform -%} -{%- if 'single-user-beamformer' in beamform -%} -[SU-BEAMFORMER] -{%- elif 'single-user-beamformee' in beamform -%} -[SU-BEAMFORMEE] -{%- elif 'multi-user-beamformer' in beamform -%} -[MU-BEAMFORMER] -{%- elif 'multi-user-beamformee' in beamform -%} -[MU-BEAMFORMEE] -{%- endif -%} -{%- endfor -%} -{%- endif -%} - -{%- if cap_vht_antenna_fixed -%} -[RX-ANTENNA-PATTERN][TX-ANTENNA-PATTERN] -{%- endif -%} - -{%- if cap_vht_antenna_cnt -%} -{%- for beamform in cap_vht_beamform -%} -{%- if 'single-user-beamformer' in beamform -%} -[BF-ANTENNA-{{ cap_vht_antenna_cnt|int -1 }}][SOUNDING-DIMENSION-{{ cap_vht_antenna_cnt|int -1}}] -{%- else -%} -[BF-ANTENNA-{{ cap_vht_antenna_cnt }}][SOUNDING-DIMENSION-{{ cap_vht_antenna_cnt }}] -{%- endif -%} -{%- endfor -%} -{%- endif %} - -# ieee80211n: Whether IEEE 802.11n (HT) is enabled -# 0 = disabled (default) -# 1 = enabled -# Note: You will also need to enable WMM for full HT functionality. -# Note: hw_mode=g (2.4 GHz) and hw_mode=a (5 GHz) is used to specify the band. -{% if cap_req_vht -%} -ieee80211n=0 -# Require stations to support VHT PHY (reject association if they do not) -require_vht=1 -{% endif %} - -{% if cap_vht_center_freq_1 -%} -# center freq = 5 GHz + (5 * index) -# So index 42 gives center freq 5.210 GHz -# which is channel 42 in 5G band -vht_oper_centr_freq_seg0_idx={{ cap_vht_center_freq_1 }} -{% endif %} - -{% if cap_vht_center_freq_2 -%} -# center freq = 5 GHz + (5 * index) -# So index 159 gives center freq 5.795 GHz -# which is channel 159 in 5G band -vht_oper_centr_freq_seg1_idx={{ cap_vht_center_freq_2 }} -{% endif %} - -{% if disable_broadcast_ssid -%} -# Send empty SSID in beacons and ignore probe request frames that do not -# specify full SSID, i.e., require stations to know SSID. -# default: disabled (0) -# 1 = send empty (length=0) SSID in beacon and ignore probe request for -# broadcast SSID -# 2 = clear SSID (ASCII 0), but keep the original length (this may be required -# with some clients that do not support empty SSID) and ignore probe -# requests for broadcast SSID -ignore_broadcast_ssid=1 -{% endif %} - -# Station MAC address -based authentication -# Please note that this kind of access control requires a driver that uses -# hostapd to take care of management frame processing and as such, this can be -# used with driver=hostap or driver=nl80211, but not with driver=atheros. -# 0 = accept unless in deny list -# 1 = deny unless in accept list -# 2 = use external RADIUS server (accept/deny lists are searched first) -macaddr_acl=0 - -{% if max_stations -%} -# Maximum number of stations allowed in station table. New stations will be -# rejected after the station table is full. IEEE 802.11 has a limit of 2007 -# different association IDs, so this number should not be larger than that. -# (default: 2007) -max_num_sta={{ max_stations }} -{% endif %} - -{% if isolate_stations -%} -# Client isolation can be used to prevent low-level bridging of frames between -# associated stations in the BSS. By default, this bridging is allowed. -ap_isolate=1 -{% endif %} - -{% if reduce_transmit_power -%} -# Add Power Constraint element to Beacon and Probe Response frames -# This config option adds Power Constraint element when applicable and Country -# element is added. Power Constraint element is required by Transmit Power -# Control. This can be used only with ieee80211d=1. -# Valid values are 0..255. -local_pwr_constraint={{ reduce_transmit_power }} -{% endif %} - -{% if expunge_failing_stations -%} -# Disassociate stations based on excessive transmission failures or other -# indications of connection loss. This depends on the driver capabilities and -# may not be available with all drivers. -disassoc_low_ack=1 -{% endif %} - -{% if sec_wep -%} -# IEEE 802.11 specifies two authentication algorithms. hostapd can be -# configured to allow both of these or only one. Open system authentication -# should be used with IEEE 802.1X. -# Bit fields of allowed authentication algorithms: -# bit 0 = Open System Authentication -# bit 1 = Shared Key Authentication (requires WEP) -auth_algs=2 - -# WEP rekeying (disabled if key lengths are not set or are set to 0) -# Key lengths for default/broadcast and individual/unicast keys: -# 5 = 40-bit WEP (also known as 64-bit WEP with 40 secret bits) -# 13 = 104-bit WEP (also known as 128-bit WEP with 104 secret bits) -wep_key_len_broadcast=5 -wep_key_len_unicast=5 - -# Static WEP key configuration -# -# The key number to use when transmitting. -# It must be between 0 and 3, and the corresponding key must be set. -# default: not set -wep_default_key=0 - -# The WEP keys to use. -# A key may be a quoted string or unquoted hexadecimal digits. -# The key length should be 5, 13, or 16 characters, or 10, 26, or 32 -# digits, depending on whether 40-bit (64-bit), 104-bit (128-bit), or -# 128-bit (152-bit) WEP is used. -# Only the default key must be supplied; the others are optional. -{% if sec_wep_key -%} -{% for key in sec_wep_key -%} -wep_key{{ loop.index -1 }}={{ key}} -{% endfor %} -{%- endif %} - -{% elif sec_wpa -%} -##### WPA/IEEE 802.11i configuration ########################################## - -# Enable WPA. Setting this variable configures the AP to require WPA (either -# WPA-PSK or WPA-RADIUS/EAP based on other configuration). For WPA-PSK, either -# wpa_psk or wpa_passphrase must be set and wpa_key_mgmt must include WPA-PSK. -# Instead of wpa_psk / wpa_passphrase, wpa_psk_radius might suffice. -# For WPA-RADIUS/EAP, ieee8021x must be set (but without dynamic WEP keys), -# RADIUS authentication server must be configured, and WPA-EAP must be included -# in wpa_key_mgmt. -# This field is a bit field that can be used to enable WPA (IEEE 802.11i/D3.0) -# and/or WPA2 (full IEEE 802.11i/RSN): -# bit0 = WPA -# bit1 = IEEE 802.11i/RSN (WPA2) (dot11RSNAEnabled) -{% if 'both' in sec_wpa_mode -%} -wpa=3 -{%- elif 'wpa2' in sec_wpa_mode -%} -wpa=2 -{%- elif 'wpa' in sec_wpa_mode -%} -wpa=1 -{%- endif %} - -{% if sec_wpa_cipher -%} -# Set of accepted cipher suites (encryption algorithms) for pairwise keys -# (unicast packets). This is a space separated list of algorithms: -# CCMP = AES in Counter mode with CBC-MAC (CCMP-128) -# TKIP = Temporal Key Integrity Protocol -# CCMP-256 = AES in Counter mode with CBC-MAC with 256-bit key -# GCMP = Galois/counter mode protocol (GCMP-128) -# GCMP-256 = Galois/counter mode protocol with 256-bit key -# Group cipher suite (encryption algorithm for broadcast and multicast frames) -# is automatically selected based on this configuration. If only CCMP is -# allowed as the pairwise cipher, group cipher will also be CCMP. Otherwise, -# TKIP will be used as the group cipher. The optional group_cipher parameter can -# be used to override this automatic selection. -{% if 'wpa2' in sec_wpa_mode -%} -# Pairwise cipher for RSN/WPA2 (default: use wpa_pairwise value) -rsn_pairwise={{ sec_wpa_cipher | join(" ") }} -{% else -%} -# Pairwise cipher for WPA (v1) (default: TKIP) -wpa_pairwise={{ sec_wpa_cipher | join(" ") }} -{%- endif -%} -{% endif %} - -{% if sec_wpa_passphrase -%} -# IEEE 802.11 specifies two authentication algorithms. hostapd can be -# configured to allow both of these or only one. Open system authentication -# should be used with IEEE 802.1X. -# Bit fields of allowed authentication algorithms: -# bit 0 = Open System Authentication -# bit 1 = Shared Key Authentication (requires WEP) -auth_algs=1 - -# WPA pre-shared keys for WPA-PSK. This can be either entered as a 256-bit -# secret in hex format (64 hex digits), wpa_psk, or as an ASCII passphrase -# (8..63 characters) that will be converted to PSK. This conversion uses SSID -# so the PSK changes when ASCII passphrase is used and the SSID is changed. -wpa_passphrase={{ sec_wpa_passphrase }} - -# Set of accepted key management algorithms (WPA-PSK, WPA-EAP, or both). The -# entries are separated with a space. WPA-PSK-SHA256 and WPA-EAP-SHA256 can be -# added to enable SHA256-based stronger algorithms. -# WPA-PSK = WPA-Personal / WPA2-Personal -# WPA-PSK-SHA256 = WPA2-Personal using SHA256 -wpa_key_mgmt=WPA-PSK - -{% elif sec_wpa_radius -%} -##### IEEE 802.1X-2004 related configuration ################################## -# Require IEEE 802.1X authorization -ieee8021x=1 - -# Set of accepted key management algorithms (WPA-PSK, WPA-EAP, or both). The -# entries are separated with a space. WPA-PSK-SHA256 and WPA-EAP-SHA256 can be -# added to enable SHA256-based stronger algorithms. -# WPA-EAP = WPA-Enterprise / WPA2-Enterprise -# WPA-EAP-SHA256 = WPA2-Enterprise using SHA256 -wpa_key_mgmt=WPA-EAP - -{% if sec_wpa_radius_source -%} -# RADIUS client forced local IP address for the access point -# Normally the local IP address is determined automatically based on configured -# IP addresses, but this field can be used to force a specific address to be -# used, e.g., when the device has multiple IP addresses. -radius_client_addr={{ sec_wpa_radius_source }} - -# The own IP address of the access point (used as NAS-IP-Address) -own_ip_addr={{ sec_wpa_radius_source }} -{% else %} -# The own IP address of the access point (used as NAS-IP-Address) -own_ip_addr=127.0.0.1 -{% endif %} - -{% for radius in sec_wpa_radius -%} -{%- if not radius.disabled -%} -# RADIUS authentication server -auth_server_addr={{ radius.server }} -auth_server_port={{ radius.port }} -auth_server_shared_secret={{ radius.key }} -{% if radius.acc_port -%} -# RADIUS accounting server -acct_server_addr={{ radius.server }} -acct_server_port={{ radius.acc_port }} -acct_server_shared_secret={{ radius.key }} -{% endif %} -{% endif %} -{% endfor %} - -{% endif %} - -{% else %} -# Open system -auth_algs=1 -{% endif %} - -# TX queue parameters (EDCF / bursting) -# tx_queue_<queue name>_<param> -# queues: data0, data1, data2, data3 -# (data0 is the highest priority queue) -# parameters: -# aifs: AIFS (default 2) -# cwmin: cwMin (1, 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095, 8191, -# 16383, 32767) -# cwmax: cwMax (same values as cwMin, cwMax >= cwMin) -# burst: maximum length (in milliseconds with precision of up to 0.1 ms) for -# bursting -# -# Default WMM parameters (IEEE 802.11 draft; 11-03-0504-03-000e): -# These parameters are used by the access point when transmitting frames -# to the clients. -# -# Low priority / AC_BK = background -tx_queue_data3_aifs=7 -tx_queue_data3_cwmin=15 -tx_queue_data3_cwmax=1023 -tx_queue_data3_burst=0 -# Note: for IEEE 802.11b mode: cWmin=31 cWmax=1023 burst=0 -# -# Normal priority / AC_BE = best effort -tx_queue_data2_aifs=3 -tx_queue_data2_cwmin=15 -tx_queue_data2_cwmax=63 -tx_queue_data2_burst=0 -# Note: for IEEE 802.11b mode: cWmin=31 cWmax=127 burst=0 -# -# High priority / AC_VI = video -tx_queue_data1_aifs=1 -tx_queue_data1_cwmin=7 -tx_queue_data1_cwmax=15 -tx_queue_data1_burst=3.0 -# Note: for IEEE 802.11b mode: cWmin=15 cWmax=31 burst=6.0 -# -# Highest priority / AC_VO = voice -tx_queue_data0_aifs=1 -tx_queue_data0_cwmin=3 -tx_queue_data0_cwmax=7 -tx_queue_data0_burst=1.5 - -# Default WMM parameters (IEEE 802.11 draft; 11-03-0504-03-000e): -# for 802.11a or 802.11g networks -# These parameters are sent to WMM clients when they associate. -# The parameters will be used by WMM clients for frames transmitted to the -# access point. -# -# note - txop_limit is in units of 32microseconds -# note - acm is admission control mandatory flag. 0 = admission control not -# required, 1 = mandatory -# note - Here cwMin and cmMax are in exponent form. The actual cw value used -# will be (2^n)-1 where n is the value given here. The allowed range for these -# wmm_ac_??_{cwmin,cwmax} is 0..15 with cwmax >= cwmin. -# -wmm_enabled=1 - -# Low priority / AC_BK = background -wmm_ac_bk_cwmin=4 -wmm_ac_bk_cwmax=10 -wmm_ac_bk_aifs=7 -wmm_ac_bk_txop_limit=0 -wmm_ac_bk_acm=0 -# Note: for IEEE 802.11b mode: cWmin=5 cWmax=10 -# -# Normal priority / AC_BE = best effort -wmm_ac_be_aifs=3 -wmm_ac_be_cwmin=4 -wmm_ac_be_cwmax=10 -wmm_ac_be_txop_limit=0 -wmm_ac_be_acm=0 -# Note: for IEEE 802.11b mode: cWmin=5 cWmax=7 -# -# High priority / AC_VI = video -wmm_ac_vi_aifs=2 -wmm_ac_vi_cwmin=3 -wmm_ac_vi_cwmax=4 -wmm_ac_vi_txop_limit=94 -wmm_ac_vi_acm=0 -# Note: for IEEE 802.11b mode: cWmin=4 cWmax=5 txop_limit=188 -# -# Highest priority / AC_VO = voice -wmm_ac_vo_aifs=2 -wmm_ac_vo_cwmin=2 -wmm_ac_vo_cwmax=3 -wmm_ac_vo_txop_limit=47 -wmm_ac_vo_acm=0 - -""" - -# Please be careful if you edit the template. -config_wpa_suppl_tmpl = """ -# WPA supplicant config -network={ - ssid="{{ ssid }}" -{%- if sec_wpa_passphrase %} - psk="{{ sec_wpa_passphrase }}" -{% else %} - key_mgmt=NONE -{% endif %} -} - -""" - default_config_data = { 'address': [], 'address_remove': [], @@ -799,7 +63,7 @@ default_config_data = { 'cap_vht_center_freq_2' : '', 'cap_vht_chan_set_width' : '', 'cap_vht_ldpc' : False, - 'cap_vht_link_adaptation' : False, + 'cap_vht_link_adaptation' : '', 'cap_vht_max_mpdu_exp' : '', 'cap_vht_max_mpdu' : '', 'cap_vht_short_gi' : [], @@ -857,11 +121,8 @@ def get_conf_file(conf_type, intf): # create directory on demand if not os.path.exists(cfg_dir): os.mkdir(cfg_dir) - # fix permissions - corresponds to mode 755 - os.chmod(cfg_dir, S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH) - uid = getpwnam(user).pw_uid - gid = getgrnam(group).gr_gid - os.chown(cfg_dir, uid, gid) + chmod_x(cfg_dir) + chown_file(cfg_dir, user, group) cfg_file = cfg_dir + r'/{}.cfg'.format(intf) return cfg_file @@ -872,11 +133,8 @@ def get_pid(conf_type, intf): # create directory on demand if not os.path.exists(cfg_dir): os.mkdir(cfg_dir) - # fix permissions - corresponds to mode 755 - os.chmod(cfg_dir, S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH) - uid = getpwnam(user).pw_uid - gid = getgrnam(group).gr_gid - os.chown(cfg_dir, uid, gid) + chmod_x(cfg_dir) + chown_file(cfg_dir, user, group) cfg_file = cfg_dir + r'/{}.pid'.format(intf) return cfg_file @@ -888,11 +146,8 @@ def get_wpa_suppl_config_name(intf): # create directory on demand if not os.path.exists(cfg_dir): os.mkdir(cfg_dir) - # fix permissions - corresponds to mode 755 - os.chmod(cfg_dir, S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH) - uid = getpwnam(user).pw_uid - gid = getgrnam(group).gr_gid - os.chown(cfg_dir, uid, gid) + chmod_x(cfg_dir) + chown_file(cfg_dir, user, group) cfg_file = cfg_dir + r'/{}.cfg'.format(intf) return cfg_file @@ -1043,7 +298,7 @@ def get_config(): # VHT link adaptation capabilities if conf.exists('capabilities vht link-adaptation'): wifi['cap_vht'] = True - wifi['cap_vht_link_adaptation'] = True + wifi['cap_vht_link_adaptation'] = conf.return_value('capabilities vht link-adaptation') # Set the maximum length of A-MPDU pre-EOF padding that the station can receive if conf.exists('capabilities vht max-mpdu-exp'): @@ -1329,6 +584,10 @@ def verify(wifi): if wifi['cap_vht_beamform'] and wifi['cap_vht_antenna_cnt'] == 1: raise ConfigError('Cannot use beam forming with just one antenna!') + if wifi['cap_vht_beamform'] == 'single-user-beamformer' and wifi['cap_vht_antenna_cnt'] < 3: + # Nasty Gotcha: see https://w1.fi/cgit/hostap/plain/hostapd/hostapd.conf lines 692-705 + raise ConfigError('Single-user beam former requires at least 3 antennas!') + if wifi['sec_wep'] and (len(wifi['sec_wep_key']) == 0): raise ConfigError('Missing WEP keys') @@ -1364,6 +623,11 @@ 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) + # always stop hostapd service first before reconfiguring it pidfile = get_pid('hostapd', wifi['intf']) if process_running(pidfile): @@ -1418,13 +682,13 @@ def generate(wifi): # render appropriate new config files depending on access-point or station mode if wifi['op_mode'] == 'ap': - tmpl = Template(config_hostapd_tmpl) + 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) elif wifi['op_mode'] == 'station': - tmpl = Template(config_wpa_suppl_tmpl) + 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) diff --git a/src/conf_mode/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py index 7aee5c9bd..da33d54e4 100755 --- a/src/conf_mode/interfaces-wirelessmodem.py +++ b/src/conf_mode/interfaces-wirelessmodem.py @@ -18,84 +18,14 @@ import os from sys import exit from copy import deepcopy -from jinja2 import Template -from subprocess import Popen, PIPE +from jinja2 import FileSystemLoader, Environment from netifaces import interfaces from vyos.config import Config -from vyos.util import chown_file, chmod_x_file +from vyos.defaults import directories as vyos_data_dir +from vyos.util import chown_file, chmod_x, subprocess_cmd from vyos import ConfigError -# Please be careful if you edit the template. -config_wwan_tmpl = """### Autogenerated by interfaces-wirelessmodem.py ### -{% if description %} -# {{ description }} -{% endif %} -ifname {{ intf }} -ipparam "{{ intf }} {{ metric }}" -linkname {{ intf }} -{% if name_server -%} -usepeerdns -{%- endif %} -# physical device -/dev/{{ device }} -lcp-echo-failure 0 -115200 -debug -logfile {{ logfile }} -nodefaultroute -ipcp-max-failure 4 -ipcp-accept-local -ipcp-accept-remote -noauth -crtscts -lock -persist -{% if on_demand -%} -demand -{%- endif %} - -connect '/usr/sbin/chat -v -t6 -f {{ chat_script }}' - -""" - -# Please be careful if you edit the template. -chat_wwan_tmpl = """ -ABORT 'NO DIAL TONE' ABORT 'NO ANSWER' ABORT 'NO CARRIER' ABORT DELAYED -'' AT -OK ATZ -OK 'AT+CGDCONT=1,"IP","{{ apn }}"' -OK ATD*99# -CONNECT '' - -""" - -config_wwan_ip_up_tmpl = """#!/bin/sh -# As WWAN is an "on demand" interface we need to re-configure it when it -# becomes 'up' - -ipparam=$6 - -# device name and metric are received using ipparam -device=`echo "$ipparam"|awk '{ print $1 }'` - -if [ "$device" != "{{ intf }}" ]; then - exit -fi - -# add some info to syslog -DIALER_PID=$(cat /var/run/{{ intf }}.pid) -logger -t pppd[$DIALER_PID] "executing $0" - -echo "{{ description }}" > /sys/class/net/{{ intf }}/ifalias - -{% if vrf -%} -logger -t pppd[$DIALER_PID] "configuring interface {{ intf }} for VRF {{ vrf }}" -ip link set dev {{ intf }} master {{ vrf }} -{% endif %} - -""" - default_config_data = { 'address': [], 'apn': '', @@ -114,10 +44,6 @@ default_config_data = { 'vrf': '' } -def subprocess_cmd(command): - p = Popen(command, stdout=PIPE, shell=True) - p.communicate() - def check_kmod(): modules = ['option', 'usb_wwan', 'usbserial'] for module in modules: @@ -206,12 +132,22 @@ 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'] - config_file_wwan = f'/etc/ppp/peers/{intf}' - config_file_wwan_chat = wwan['chat_script'] - ip_up_script_file = f'/etc/ppp/ip-up.d/9991-vyos-vrf-{intf}' + config_wwan = f'/etc/ppp/peers/{intf}' + config_wwan_chat = wwan['chat_script'] + script_wwan_pre_up = f'/etc/ppp/ip-pre-up.d/1010-vyos-wwan-{intf}' + script_wwan_ip_up = f'/etc/ppp/ip-up.d/1010-vyos-wwan-{intf}' + script_wwan_ip_down = f'/etc/ppp/ip-down.d/1010-vyos-wwan-{intf}' - config_files = [config_file_wwan, config_file_wwan_chat, ip_up_script_file] + config_files = [config_wwan, config_wwan_chat, script_wwan_pre_up, + script_wwan_ip_up, script_wwan_ip_down] # Ensure directories for config files exist - otherwise create them on demand for file in config_files: @@ -231,25 +167,39 @@ def generate(wwan): else: # Create PPP configuration files - tmpl = Template(config_wwan_tmpl) + tmpl = env.get_template('peer.tmpl') config_text = tmpl.render(wwan) - with open(config_file_wwan, 'w') as f: + with open(config_wwan, 'w') as f: f.write(config_text) # Create PPP chat script - tmpl = Template(chat_wwan_tmpl) + tmpl = env.get_template('chat.tmpl') + config_text = tmpl.render(wwan) + with open(config_wwan_chat, 'w') as f: + f.write(config_text) + + # 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) + + # Create script for ip-up.d + tmpl = env.get_template('ip-up.script.tmpl') config_text = tmpl.render(wwan) - with open(wwan['chat_script'], 'w') as f: + with open(script_wwan_ip_up, 'w') as f: f.write(config_text) - # Create ip-pre-up script - tmpl = Template(config_wwan_ip_up_tmpl) + # Create script for ip-down.d + tmpl = env.get_template('ip-down.script.tmpl') config_text = tmpl.render(wwan) - with open(ip_up_script_file, 'w') as f: + with open(script_wwan_ip_down, 'w') as f: f.write(config_text) # make generated script file executable - chmod_x_file(ip_up_script_file) + chmod_x(script_wwan_pre_up) + chmod_x(script_wwan_ip_up) + chmod_x(script_wwan_ip_down) return None diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py index e80c6caf0..90b6b0d57 100755 --- a/src/conf_mode/ipsec-settings.py +++ b/src/conf_mode/ipsec-settings.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,22 +13,18 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import re import os -import jinja2 -import syslog as sl -import time -import vyos.config -import vyos.defaults +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 - ra_conn_name = "remote-access" charon_conf_file = "/etc/strongswan.d/charon.conf" ipsec_secrets_flie = "/etc/ipsec.secrets" @@ -42,55 +38,8 @@ delim_ipsec_l2tp_begin = "### VyOS L2TP VPN Begin ###" delim_ipsec_l2tp_end = "### VyOS L2TP VPN End ###" charon_pidfile = "/var/run/charon.pid" -l2pt_ipsec_conf = ''' -{{delim_ipsec_l2tp_begin}} -include {{ipsec_ra_conn_file}} -{{delim_ipsec_l2tp_end}} -''' - -l2pt_ipsec_secrets_conf = ''' -{{delim_ipsec_l2tp_begin}} -{% if ipsec_l2tp_auth_mode == 'pre-shared-secret' %} -{{outside_addr}} %any : PSK "{{ipsec_l2tp_secret}}" -{% elif ipsec_l2tp_auth_mode == 'x509' %} -: RSA {{server_key_file_copied}} -{% endif%} -{{delim_ipsec_l2tp_end}} -''' - -l2tp_ipsec_ra_conn_conf = ''' -{{delim_ipsec_l2tp_begin}} -conn {{ra_conn_name}} - type=transport - left={{outside_addr}} - leftsubnet=%dynamic[/1701] - rightsubnet=%dynamic - mark_in=%unique - auto=add - ike=aes256-sha1-modp1024,3des-sha1-modp1024,3des-sha1-modp1024! - dpddelay=15 - dpdtimeout=45 - dpdaction=clear - esp=aes256-sha1,3des-sha1! - rekey=no -{% if ipsec_l2tp_auth_mode == 'pre-shared-secret' %} - authby=secret - leftauth=psk - rightauth=psk -{% elif ipsec_l2tp_auth_mode == 'x509' %} - authby=rsasig - leftrsasigkey=%cert - rightrsasigkey=%cert - rightca=%same - leftcert={{server_cert_file_copied}} -{% endif %} - ikelifetime={{ipsec_l2tp_ike_lifetime}} - keylife={{ipsec_l2tp_lifetime}} -{{delim_ipsec_l2tp_end}} -''' - def get_config(): - config = vyos.config.Config() + config = Config() data = {"install_routes": "yes"} if config.exists("vpn ipsec options disable-route-autoinstall"): @@ -146,44 +95,12 @@ def get_config(): return data -### ipsec secret l2tp -def write_ipsec_secrets(c): - tmpl = jinja2.Template(l2pt_ipsec_secrets_conf, trim_blocks=True) - l2pt_ipsec_secrets_txt = tmpl.render(c) - old_umask = os.umask(0o077) - open(ipsec_secrets_flie,'w').write(l2pt_ipsec_secrets_txt) - os.umask(old_umask) - sl.syslog(sl.LOG_NOTICE, ipsec_secrets_flie + ' written') - -### ipsec remote access connection config -def write_ipsec_ra_conn(c): - tmpl = jinja2.Template(l2tp_ipsec_ra_conn_conf, trim_blocks=True) - 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) - sl.syslog(sl.LOG_NOTICE, ipsec_ra_conn_dir + " created") - - open(ipsec_ra_conn_file,'w').write(ipsec_ra_conn_txt) - os.umask(old_umask) - sl.syslog(sl.LOG_NOTICE, ipsec_ra_conn_file + ' written') ### Remove config from file by delimiter def remove_confs(delim_begin, delim_end, conf_file): os.system("sed -i '/"+delim_begin+"/,/"+delim_end+"/d' "+conf_file) -### Append "include /path/to/ra_conn" to ipsec conf file -def append_ipsec_conf(c): - tmpl = jinja2.Template(l2pt_ipsec_conf, trim_blocks=True) - l2pt_ipsec_conf_txt = tmpl.render(c) - old_umask = os.umask(0o077) - open(ipsec_conf_flie,'a').write(l2pt_ipsec_conf_txt) - os.umask(old_umask) - sl.syslog(sl.LOG_NOTICE, ipsec_conf_flie + ' written') - ### Checking certificate storage and notice if certificate not in /config directory def check_cert_file_store(cert_name, file_path, dts_path): if not re.search('^\/config\/.+', file_path): @@ -197,8 +114,6 @@ def check_cert_file_store(cert_name, file_path, dts_path): ret = os.system('cp -f '+file_path+' '+dts_path) if ret: raise ConfigError("L2TP VPN configuration error: Cannot copy "+file_path) - else: - sl.syslog(sl.LOG_NOTICE, file_path + ' copied to '+dts_path) def verify(data): # l2tp ipsec check @@ -231,22 +146,45 @@ 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.defaults.directories["data"], "templates", "ipsec") - fs_loader = jinja2.FileSystemLoader(tmpl_path) - env = jinja2.Environment(loader=fs_loader) - - - charon_conf_tmpl = env.get_template("charon.tmpl") - charon_conf = charon_conf_tmpl.render(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(charon_conf) + f.write(config_text) if data["ipsec_l2tp"]: remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_flie) - write_ipsec_secrets(data) - write_ipsec_ra_conn(data) - append_ipsec_conf(data) + + 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) + 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) + 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) + os.umask(old_umask) + else: if os.path.exists(ipsec_ra_conn_file): remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_ra_conn_file) @@ -254,15 +192,15 @@ def generate(data): remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_flie) def restart_ipsec(): - os.system("ipsec restart >&/dev/null") + os.system('ipsec restart >&/dev/null') # counter for apply swanctl config counter = 10 while counter <= 10: if os.path.exists(charon_pidfile): - os.system("swanctl -q >&/dev/null") + os.system('swanctl -q >&/dev/null') break counter -=1 - time.sleep(1) + sleep(1) if counter == 0: raise ConfigError('VPN configuration error: IPSec is not running.') @@ -278,4 +216,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/lldp.py b/src/conf_mode/lldp.py index b72916ab8..4e3dfc0b6 100755 --- a/src/conf_mode/lldp.py +++ b/src/conf_mode/lldp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2017-2019 VyOS maintainers and contributors +# Copyright (C) 2017-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,47 +14,22 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import re -import sys import os -import jinja2 +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 -# Please be careful if you edit the template. config_file = "/etc/default/lldpd" -lldp_tmpl = """ -### Autogenerated by lldp.py ### -DAEMON_ARGS="-M 4{% if options.snmp %} -x{% endif %}{% if options.cdp %} -c{% endif %}{% if options.edp %} -e{% endif %}{% if options.fdp %} -f{% endif %}{% if options.sonmp %} -s{% endif %}" - -""" - vyos_config_file = "/etc/lldpd.d/01-vyos.conf" -vyos_tmpl = """ -### Autogenerated by lldp.py ### - -configure system platform VyOS -configure system description "VyOS {{ options.description }}" -{% if options.listen_on -%} -configure system interface pattern "{{ ( options.listen_on | select('equalto','all') | map('replace','all','*') | list + options.listen_on | select('equalto','!all') | map('replace','!all','!*') | list + options.listen_on | reject('equalto','all') | reject('equalto','!all') | list ) | unique | join(",") }}" -{%- endif %} -{% if options.mgmt_addr -%} -configure system ip management pattern {{ options.mgmt_addr | join(",") }} -{%- endif %} -{%- for loc in location -%} -{%- if loc.elin %} -configure ports {{ loc.name }} med location elin "{{ loc.elin }}" -{%- endif %} -{%- if loc.coordinate_based %} -configure ports {{ loc.name }} med location coordinate {% if loc.coordinate_based.latitude %}latitude {{ loc.coordinate_based.latitude }}{% endif %} {% if loc.coordinate_based.longitude %}longitude {{ loc.coordinate_based.longitude }}{% endif %} {% if loc.coordinate_based.altitude %}altitude {{ loc.coordinate_based.altitude }} m{% endif %} {% if loc.coordinate_based.datum %}datum {{ loc.coordinate_based.datum }}{% endif %} -{%- endif %} - - -{% endfor %} -""" +base = ['service', 'lldp'] default_config_data = { "options": '', @@ -64,7 +39,7 @@ default_config_data = { def get_options(config): options = {} - config.set_level('service lldp') + config.set_level(base) options['listen_vlan'] = config.exists('listen-vlan') options['mgmt_addr'] = [] @@ -84,30 +59,31 @@ def get_options(config): if snmp: config.set_level('') options["sys_snmp"] = config.exists('service snmp') - config.set_level('service lldp') + config.set_level(base) - config.set_level('service lldp legacy-protocols') + config.set_level(base + ['legacy-protocols']) options['cdp'] = config.exists('cdp') options['edp'] = config.exists('edp') options['fdp'] = config.exists('fdp') options['sonmp'] = config.exists('sonmp') # start with an unknown version information - options['description'] = 'unknown' + version_data = get_version_data() + options['description'] = version_data['version'] options['listen_on'] = [] return options def get_interface_list(config): - config.set_level('service lldp') - intfs_names = config.list_nodes('interface') + config.set_level(base) + intfs_names = config.list_nodes(['interface']) if len(intfs_names) < 0: return 0 interface_list = [] for name in intfs_names: - config.set_level('service lldp interface {0}'.format(name)) - disable = config.exists('disable') + config.set_level(base + ['interface', name]) + disable = config.exists(['disable']) intf = { 'name': name, 'disable': disable @@ -117,10 +93,10 @@ def get_interface_list(config): def get_location_intf(config, name): - path = 'service lldp interface {0}'.format(name) + path = base + ['interface', name] config.set_level(path) - config.set_level('{} location'.format(path)) + config.set_level(path + ['location']) elin = '' coordinate_based = {} @@ -128,18 +104,18 @@ def get_location_intf(config, name): elin = config.return_value('elin') if config.exists('coordinate-based'): - config.set_level('{} location coordinate-based'.format(path)) + config.set_level(path + ['location', 'coordinate-based']) - coordinate_based['latitude'] = config.return_value('latitude') - coordinate_based['longitude'] = config.return_value('longitude') + coordinate_based['latitude'] = config.return_value(['latitude']) + coordinate_based['longitude'] = config.return_value(['longitude']) coordinate_based['altitude'] = '0' - if config.exists('altitude'): - coordinate_based['altitude'] = config.return_value('altitude') + if config.exists(['altitude']): + coordinate_based['altitude'] = config.return_value(['altitude']) coordinate_based['datum'] = 'WGS84' - if config.exists('datum'): - coordinate_based['datum'] = config.return_value('datum') + if config.exists(['datum']): + coordinate_based['datum'] = config.return_value(['datum']) intf = { 'name': name, @@ -151,8 +127,8 @@ def get_location_intf(config, name): def get_location(config): - config.set_level('service lldp') - intfs_names = config.list_nodes('interface') + config.set_level(base) + intfs_names = config.list_nodes(['interface']) if len(intfs_names) < 0: return 0 @@ -170,7 +146,7 @@ def get_location(config): def get_config(): lldp = deepcopy(default_config_data) conf = Config() - if not conf.exists('service lldp'): + if not conf.exists(base): return None else: lldp['options'] = get_options(conf) @@ -232,10 +208,10 @@ def generate(lldp): if lldp is None: return - with open('/opt/vyatta/etc/version', 'r') as f: - tmp = f.read() - lldp['options']['description'] = tmp.split()[1] - + # 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']: @@ -248,13 +224,13 @@ def generate(lldp): lldp['options']['listen_on'].append(tmp) # generate /etc/default/lldpd - tmpl = jinja2.Template(lldp_tmpl) + tmpl = env.get_template('lldpd.tmpl') config_text = tmpl.render(lldp) with open(config_file, 'w') as f: f.write(config_text) # generate /etc/lldpd.d/01-vyos.conf - tmpl = jinja2.Template(vyos_tmpl) + tmpl = env.get_template('vyos.conf.tmpl') config_text = tmpl.render(lldp) with open(vyos_config_file, 'w') as f: f.write(config_text) @@ -278,5 +254,5 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/mdns_repeater.py b/src/conf_mode/mdns_repeater.py index cef735c0d..f738cc6a6 100755 --- a/src/conf_mode/mdns_repeater.py +++ b/src/conf_mode/mdns_repeater.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2017 VyOS maintainers and contributors +# Copyright (C) 2017-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,45 +13,42 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import os -import jinja2 -import netifaces + +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 config_file = r'/etc/default/mdns-repeater' -config_tmpl = """ -### Autogenerated by mdns_repeater.py ### -DAEMON_ARGS="{{ interfaces | join(' ') }}" -""" - default_config_data = { 'disabled': False, 'interfaces': [] } def get_config(): - mdns = default_config_data + mdns = deepcopy(default_config_data) conf = Config() - if not conf.exists('service mdns repeater'): + base = ['service', 'mdns', 'repeater'] + if not conf.exists(base): return None else: - conf.set_level('service mdns repeater') + conf.set_level(base) # Service can be disabled by user - if conf.exists('disable'): + if conf.exists(['disable']): mdns['disabled'] = True return mdns # Interface to repeat mDNS advertisements - if conf.exists('interface'): - mdns['interfaces'] = conf.return_values('interface') + if conf.exists(['interface']): + mdns['interfaces'] = conf.return_values(['interface']) return mdns @@ -69,8 +66,8 @@ def verify(mdns): # For mdns-repeater to work it is essential that the interfaces has # an IPv4 address assigned for interface in mdns['interfaces']: - if netifaces.AF_INET in netifaces.ifaddresses(interface).keys(): - if len(netifaces.ifaddresses(interface)[netifaces.AF_INET]) < 1: + if AF_INET in ifaddresses(interface).keys(): + if len(ifaddresses(interface)[AF_INET]) < 1: raise ConfigError('mDNS repeater requires an IPv6 address configured on interface %s!'.format(interface)) return None @@ -83,7 +80,12 @@ def generate(mdns): print('Warning: mDNS repeater will be deactivated because it is disabled') return None - tmpl = jinja2.Template(config_tmpl) + # 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) @@ -108,4 +110,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py index c3e8d51b3..0f635556b 100755 --- a/src/conf_mode/ntp.py +++ b/src/conf_mode/ntp.py @@ -14,61 +14,19 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import sys import os -import jinja2 -import ipaddress from copy import deepcopy +from ipaddress import ip_network +from jinja2 import FileSystemLoader, Environment +from sys import exit from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError config_file = r'/etc/ntp.conf' -# Please be careful if you edit the template. -config_tmpl = """ -### Autogenerated by ntp.py ### - -# -# Non-configurable defaults -# -driftfile /var/lib/ntp/ntp.drift -# By default, only allow ntpd to query time sources, ignore any incoming requests -restrict default noquery nopeer notrap nomodify -# Local users have unrestricted access, allowing reconfiguration via ntpdc -restrict 127.0.0.1 -restrict -6 ::1 - -# -# Configurable section -# - -{% if servers -%} -{% for s in servers -%} -# Server configuration for: {{ s.name }} -server {{ s.name }} iburst {{ s.options | join(" ") }} -{% endfor -%} -{% endif %} - -{% if allowed_networks -%} -{% for n in allowed_networks -%} -# Client configuration for network: {{ n.network }} -restrict {{ n.address }} mask {{ n.netmask }} nomodify notrap nopeer - -{% endfor -%} -{% endif %} - -{% if listen_address -%} -# NTP should listen on configured addresses only -interface ignore wildcard -{% for a in listen_address -%} -interface listen {{ a }} -{% endfor -%} -{% endif %} - -""" - default_config_data = { 'servers': [], 'allowed_networks': [], @@ -86,7 +44,7 @@ def get_config(): if conf.exists('allow-clients address'): networks = conf.return_values('allow-clients address') for n in networks: - addr = ipaddress.ip_network(n) + addr = ip_network(n) net = { "network" : n, "address" : addr.network_address, @@ -128,7 +86,7 @@ def verify(ntp): for n in ntp['allowed_networks']: try: - addr = ipaddress.ip_network( n['network'] ) + addr = ip_network( n['network'] ) break except ValueError: raise ConfigError("{0} does not appear to be a valid IPv4 or IPv6 network, check host bits!".format(n['network'])) @@ -140,7 +98,12 @@ def generate(ntp): if ntp is None: return None - tmpl = jinja2.Template(config_tmpl) + # 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) @@ -165,4 +128,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py index 58f5b5a0e..9940c80c5 100755 --- a/src/conf_mode/protocols_bfd.py +++ b/src/conf_mode/protocols_bfd.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,39 +13,20 @@ # # 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 jinja2 -import copy import os -import vyos.validate -from vyos import ConfigError +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 config_file = r'/tmp/bfd.frr' -# Please be careful if you edit the template. -config_tmpl = """ -! -bfd -{% for peer in old_peers -%} - no peer {{ peer.remote }}{% if peer.multihop %} multihop{% endif %}{% if peer.src_addr %} local-address {{ peer.src_addr }}{% endif %}{% if peer.src_if %} interface {{ peer.src_if }}{% endif %} -{% endfor -%} -! -{% for peer in new_peers -%} - peer {{ peer.remote }}{% if peer.multihop %} multihop{% endif %}{% if peer.src_addr %} local-address {{ peer.src_addr }}{% endif %}{% if peer.src_if %} interface {{ peer.src_if }}{% endif %} - detect-multiplier {{ peer.multiplier }} - receive-interval {{ peer.rx_interval }} - transmit-interval {{ peer.tx_interval }} - {% if peer.echo_mode %}echo-mode{% endif %} - {% if peer.echo_interval != '' %}echo-interval {{ peer.echo_interval }}{% endif %} - {% if not peer.shutdown %}no {% endif %}shutdown -{% endfor -%} -! -""" - default_config_data = { 'new_peers': [], 'old_peers' : [] @@ -132,7 +113,7 @@ def get_bfd_peer_config(peer, conf_mode="proposed"): return bfd_peer def get_config(): - bfd = copy.deepcopy(default_config_data) + bfd = deepcopy(default_config_data) conf = Config() if not (conf.exists('protocols bfd') or conf.exists_effective('protocols bfd')): return None @@ -164,12 +145,12 @@ def verify(bfd): for peer in bfd['new_peers']: # IPv6 link local peers require an explicit local address/interface - if vyos.validate.is_ipv6_link_local(peer['remote']): + if is_ipv6_link_local(peer['remote']): if not (peer['src_if'] and peer['src_addr']): raise ConfigError('BFD IPv6 link-local peers require explicit local address and interface setting') # IPv6 peers require an explicit local address - if vyos.validate.is_ipv6(peer['remote']): + if is_ipv6(peer['remote']): if not peer['src_addr']: raise ConfigError('BFD IPv6 peers require explicit local address setting') @@ -208,18 +189,23 @@ 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) + return None def apply(bfd): if bfd is None: return None - tmpl = jinja2.Template(config_tmpl) - config_text = tmpl.render(bfd) - with open(config_file, 'w') as f: - f.write(config_text) - - os.system("sudo vtysh -d bfdd -f " + config_file) + os.system(f'vtysh -d bfdd -f {config_file}') if os.path.exists(config_file): os.remove(config_file) @@ -233,4 +219,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/protocols_igmp.py b/src/conf_mode/protocols_igmp.py index 8d25fe9d5..0148b5dac 100755 --- a/src/conf_mode/protocols_igmp.py +++ b/src/conf_mode/protocols_igmp.py @@ -13,46 +13,19 @@ # # 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 jinja2 -import copy import os -import vyos.validate + +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 config_file = r'/tmp/igmp.frr' -# Please be careful if you edit the template. -config_tmpl = """ -! -{% for iface in old_ifaces -%} -interface {{ iface }} -no ip igmp -! -{% endfor -%} -{% for iface in ifaces -%} -interface {{ iface }} -{% if ifaces[iface].version -%} -ip igmp version {{ ifaces[iface].version }} -{% else -%} -{# IGMP default version 3 #} -ip igmp -{% endif -%} -{% if ifaces[iface].query_interval -%} -ip igmp query-interval {{ ifaces[iface].query_interval }} -{% endif -%} -{% if ifaces[iface].query_max_resp_time -%} -ip igmp query-max-response-time {{ ifaces[iface].query_max_resp_time }} -{% endif -%} -! -{% endfor -%} -! -""" - def get_config(): conf = Config() igmp_conf = { @@ -74,18 +47,24 @@ def get_config(): iface : { 'version' : conf.return_effective_value('interface {0} version'.format(iface)), 'query_interval' : conf.return_effective_value('interface {0} query-interval'.format(iface)), - 'query_max_resp_time' : conf.return_effective_value('interface {0} query-max-response-time'.format(iface)) + 'query_max_resp_time' : conf.return_effective_value('interface {0} query-max-response-time'.format(iface)), + 'gr_join' : {} } }) + for gr_join in conf.list_effective_nodes('interface {0} join'.format(iface)): + igmp_conf['old_ifaces'][iface]['gr_join'][gr_join] = conf.return_effective_values('interface {0} join {1} source'.format(iface, gr_join)) for iface in conf.list_nodes('interface'): igmp_conf['ifaces'].update({ iface : { 'version' : conf.return_value('interface {0} version'.format(iface)), 'query_interval' : conf.return_value('interface {0} query-interval'.format(iface)), - 'query_max_resp_time' : conf.return_value('interface {0} query-max-response-time'.format(iface)) + 'query_max_resp_time' : conf.return_value('interface {0} query-max-response-time'.format(iface)), + 'gr_join' : {} } }) + for gr_join in conf.list_nodes('interface {0} join'.format(iface)): + igmp_conf['ifaces'][iface]['gr_join'][gr_join] = conf.return_values('interface {0} join {1} source'.format(iface, gr_join)) return igmp_conf @@ -97,12 +76,22 @@ def verify(igmp): # Check interfaces if not igmp['ifaces']: raise ConfigError(f"IGMP require defined interfaces!") + # Check, is this multicast group + for intfc in igmp['ifaces']: + for gr_addr in igmp['ifaces'][intfc]['gr_join']: + if IPv4Address(gr_addr) < IPv4Address('224.0.0.0'): + raise ConfigError(gr_addr + " not a multicast group") def generate(igmp): if igmp is None: return None - tmpl = jinja2.Template(config_tmpl) + # 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) @@ -127,4 +116,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py index 5f2497660..514fe5efb 100755 --- a/src/conf_mode/protocols_mpls.py +++ b/src/conf_mode/protocols_mpls.py @@ -13,81 +13,17 @@ # # 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 jinja2 -import copy import os -import vyos.validate -from vyos import ConfigError +from jinja2 import FileSystemLoader, Environment + from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir +from vyos import ConfigError config_file = r'/tmp/ldpd.frr' -# Please be careful if you edit the template. -config_tmpl = """ -! -{% if mpls_ldp -%} -mpls ldp -{% if old_router_id -%} -no router-id {{ old_router_id }} -{% endif -%} -{% if router_id -%} -router-id {{ router_id }} -{% endif -%} -{% for neighbor_id in old_ldp.neighbors -%} -no neighbor {{neighbor_id}} password {{old_ldp.neighbors[neighbor_id].password}} -{% endfor -%} -{% for neighbor_id in ldp.neighbors -%} -neighbor {{neighbor_id}} password {{ldp.neighbors[neighbor_id].password}} -{% endfor -%} -address-family ipv4 -label local allocate host-routes -{% if old_ldp.d_transp_ipv4 -%} -no discovery transport-address {{ old_ldp.d_transp_ipv4 }} -{% endif -%} -{% if ldp.d_transp_ipv4 -%} -discovery transport-address {{ ldp.d_transp_ipv4 }} -{% endif -%} -{% for interface in old_ldp.interfaces -%} -no interface {{interface}} -{% endfor -%} -{% for interface in ldp.interfaces -%} -interface {{interface}} -{% endfor -%} -! -! -exit-address-family -! -{% if ldp.d_transp_ipv6 -%} -address-family ipv6 -label local allocate host-routes -{% if old_ldp.d_transp_ipv6 -%} -no discovery transport-address {{ old_ldp.d_transp_ipv6 }} -{% endif -%} -{% if ldp.d_transp_ipv6 -%} -discovery transport-address {{ ldp.d_transp_ipv6 }} -{% endif -%} -{% for interface in old_ldp.interfaces -%} -no interface {{interface}} -{% endfor -%} -{% for interface in ldp.interfaces -%} -interface {{interface}} -{% endfor -%} -! -exit-address-family -{% else -%} -no address-family ipv6 -{% endif -%} -! -{% else -%} -no mpls ldp -{% endif -%} -! -""" - def sysctl(name, value): os.system('sysctl -wq {}={}'.format(name, value)) @@ -159,12 +95,10 @@ def get_config(): } }) - # print(mpls_conf) - # sys.exit(1) return mpls_conf def operate_mpls_on_intfc(interfaces, action): - rp_filter = 0 + rp_filter = 0 if action == 1: rp_filter = 2 for iface in interfaces: @@ -193,7 +127,12 @@ def generate(mpls): if mpls is None: return None - tmpl = jinja2.Template(config_tmpl) + # 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) @@ -234,4 +173,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/protocols_pim.py b/src/conf_mode/protocols_pim.py index dc65a59a6..7b360d62c 100755 --- a/src/conf_mode/protocols_pim.py +++ b/src/conf_mode/protocols_pim.py @@ -13,55 +13,19 @@ # # 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 jinja2 -import copy import os -import vyos.validate + 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 import ConfigError config_file = r'/tmp/pimd.frr' -# Please be careful if you edit the template. -config_tmpl = """ -! -{% for rp_addr in old_pim.rp -%} -{% for group in old_pim.rp[rp_addr] -%} -no ip pim rp {{ rp_addr }} {{ group }} -{% endfor -%} -{% endfor -%} -{% if old_pim.rp_keep_alive -%} -no ip pim rp keep-alive-timer {{ old_pim.rp_keep_alive }} -{% endif -%} -{% for iface in old_pim.ifaces -%} -interface {{ iface }} -no ip pim -! -{% endfor -%} -{% for iface in pim.ifaces -%} -interface {{ iface }} -{% if pim.ifaces[iface].hello -%} -ip pim hello {{ pim.ifaces[iface].hello }} -{% endif -%} -ip pim -! -{% endfor -%} -{% for rp_addr in pim.rp -%} -{% for group in pim.rp[rp_addr] -%} -ip pim rp {{ rp_addr }} {{ group }} -{% endfor -%} -{% endfor -%} -{% if pim.rp_keep_alive -%} -ip pim rp keep-alive-timer {{ pim.rp_keep_alive }} -{% endif -%} -! -""" - def get_config(): conf = Config() pim_conf = { @@ -87,14 +51,16 @@ def get_config(): for iface in conf.list_effective_nodes('interface'): pim_conf['old_pim']['ifaces'].update({ iface : { - 'hello' : conf.return_effective_value('interface {0} hello'.format(iface)) + 'hello' : conf.return_effective_value('interface {0} hello'.format(iface)), + 'dr_prio' : conf.return_effective_value('interface {0} dr-priority'.format(iface)) } }) for iface in conf.list_nodes('interface'): pim_conf['pim']['ifaces'].update({ iface : { - 'hello' : conf.return_value('interface {0} hello'.format(iface)) + 'hello' : conf.return_value('interface {0} hello'.format(iface)), + 'dr_prio' : conf.return_value('interface {0} dr-priority'.format(iface)), } }) @@ -147,7 +113,12 @@ def generate(pim): if pim is None: return None - tmpl = jinja2.Template(config_tmpl) + # 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) @@ -159,7 +130,7 @@ def apply(pim): return None if os.path.exists(config_file): - os.system("sudo vtysh -d pimd -f " + config_file) + os.system("vtysh -d pimd -f " + config_file) os.remove(config_file) return None @@ -172,4 +143,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/salt-minion.py b/src/conf_mode/salt-minion.py index 303ddae48..bc1767454 100755 --- a/src/conf_mode/salt-minion.py +++ b/src/conf_mode/salt-minion.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,102 +13,35 @@ # # 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 pwd -import socket -import urllib3 -import jinja2 +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 config_file = r'/etc/salt/minion' -# Please be careful if you edit the template. -config_tmpl = """ -### Autogenerated by salt-minion.py ### - -##### Primary configuration settings ##### -########################################## - -# The hash_type is the hash to use when discovering the hash of a file on -# the master server. The default is sha256, but md5, sha1, sha224, sha384 and -# sha512 are also supported. -# -# WARNING: While md5 and sha1 are also supported, do not use them due to the -# high chance of possible collisions and thus security breach. -# -# Prior to changing this value, the master should be stopped and all Salt -# caches should be cleared. -hash_type: {{ hash_type }} - -##### Logging settings ##### -########################################## -# The location of the minion log file -# The minion log can be sent to a regular file, local path name, or network -# location. Remote logging works best when configured to use rsyslogd(8) (e.g.: -# ``file:///dev/log``), with rsyslogd(8) configured for network logging. The URI -# format is: <file|udp|tcp>://<host|socketpath>:<port-if-required>/<log-facility> -#log_file: /var/log/salt/minion -#log_file: file:///dev/log -#log_file: udp://loghost:10514 -# -log_file: {{ log_file }} - -# The level of messages to send to the console. -# One of 'garbage', 'trace', 'debug', info', 'warning', 'error', 'critical'. -# -# The following log levels are considered INSECURE and may log sensitive data: -# ['garbage', 'trace', 'debug'] -# -# Default: 'warning' -log_level: {{ log_level }} - -# Set the location of the salt master server, if the master server cannot be -# resolved, then the minion will fail to start. -master: -{% for host in master -%} -- {{ host }} -{% endfor %} - -# The user to run salt -user: {{ user }} - -# The directory to store the pki information in -pki_dir: /config/salt/pki/minion - -# Explicitly declare the id for this minion to use, if left commented the id -# will be the hostname as returned by the python call: socket.getfqdn() -# Since salt uses detached ids it is possible to run multiple minions on the -# same machine but with different ids, this can be useful for salt compute -# clusters. -id: {{ salt_id }} - - -# The number of minutes between mine updates. -mine_interval: {{ mine_interval }} - -verify_master_pubkey_sign: {{ verify_master_pubkey_sign }} -""" - default_config_data = { 'hash_type': 'sha256', 'log_file': '/var/log/salt/minion', 'log_level': 'warning', 'master' : 'salt', 'user': 'minion', - 'salt_id': socket.gethostname(), + 'salt_id': gethostname(), 'mine_interval': '60', 'verify_master_pubkey_sign': 'false' } def get_config(): - salt = default_config_data + salt = deepcopy(default_config_data) conf = Config() if not conf.exists('service salt-minion'): return None @@ -145,25 +78,31 @@ def get_config(): return salt def generate(salt): - paths = ['/etc/salt/','/var/run/salt','/opt/vyatta/etc/config/salt/'] + paths = ['/etc/salt/','/var/run/salt','/opt/vyatta/etc/config/salt/'] directory = '/opt/vyatta/etc/config/salt/pki/minion' - uid = pwd.getpwnam(salt['user']).pw_uid - http = urllib3.PoolManager() + uid = getpwnam(salt['user']).pw_uid + http = PoolManager() 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 = jinja2.Template(config_tmpl) + tmpl = env.get_template('minion.tmpl') config_text = tmpl.render(salt) with open(config_file, 'w') as f: f.write(config_text) - path = "/etc/salt/" + + path = "/etc/salt/" for path in paths: - for root, dirs, files in os.walk(path): - for usgr in dirs: + for root, dirs, files in os.walk(path): + for usgr in dirs: os.chown(os.path.join(root, usgr), uid, 100) for usgr in files: os.chown(os.path.join(root, usgr), uid, 100) @@ -171,14 +110,14 @@ def generate(salt): if not os.path.exists('/opt/vyatta/etc/config/salt/pki/minion/master_sign.pub'): if not salt['master-key'] is None: r = http.request('GET', salt['master-key'], preload_content=False) - + with open('/opt/vyatta/etc/config/salt/pki/minion/master_sign.pub', 'wb') as out: while True: data = r.read(1024) if not data: break out.write(data) - + r.release_conn() return None @@ -200,4 +139,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/service-ipoe.py b/src/conf_mode/service-ipoe.py index bd7a898d0..dd9616a62 100755 --- a/src/conf_mode/service-ipoe.py +++ b/src/conf_mode/service-ipoe.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,19 +13,18 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import os import re -import time -import socket import subprocess -import jinja2 -import syslog as sl + +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 ipoe_cnf_dir = r'/etc/accel-ppp/ipoe' @@ -37,157 +36,8 @@ cmd_port = r'2002' chap_secrets = ipoe_cnf_dir + '/chap-secrets' ## accel-pppd -d -c /etc/accel-ppp/pppoe/pppoe.config -p /var/run/accel_pppoe.pid -ipoe_config = ''' -### generated by ipoe.py ### -[modules] -log_syslog -ipoe -shaper -ipv6pool -ipv6_nd -ipv6_dhcp -{% if auth['mech'] == 'radius' %} -radius -{% endif -%} -ippool -{% if auth['mech'] == 'local' %} -chap-secrets -{% endif %} - -[core] -thread-count={{thread_cnt}} - -[log] -syslog=accel-ipoe,daemon -copy=1 -level=5 - -[ipoe] -verbose=1 -{% for intfc in interfaces %} -{% if interfaces[intfc]['vlan_mon'] %} -interface=re:{{intfc}}\.\d+,\ -{% else %} -interface={{intfc}},\ -{% endif %} -shared={{interfaces[intfc]['shared']}},\ -mode={{interfaces[intfc]['mode']}},\ -ifcfg={{interfaces[intfc]['ifcfg']}},\ -range={{interfaces[intfc]['range']}},\ -start={{interfaces[intfc]['sess_start']}},\ -ipv6=1 -{% endfor %} -{% if auth['mech'] == 'noauth' %} -noauth=1 -{% endif %} -{% if auth['mech'] == 'local' %} -username=ifname -password=csid -{% endif %} - -{%- for intfc in interfaces %} -{% if (interfaces[intfc]['shared'] == '0') and (interfaces[intfc]['vlan_mon']) %} -vlan-mon={{intfc}},{{interfaces[intfc]['vlan_mon']|join(',')}} -{% endif %} -{% endfor %} - -{% if (dns['server1']) or (dns['server2']) %} -[dns] -{% if dns['server1'] %} -dns1={{dns['server1']}} -{% endif -%} -{% if dns['server2'] %} -dns2={{dns['server2']}} -{% endif -%} -{% endif -%} - -{% if (dnsv6['server1']) or (dnsv6['server2']) or (dnsv6['server3']) %} -[dnsv6] -dns={{dnsv6['server1']}} -dns={{dnsv6['server2']}} -dns={{dnsv6['server3']}} -{% endif %} - -[ipv6-nd] -verbose=1 - -[ipv6-dhcp] -verbose=1 - -{% if ipv6['prfx'] %} -[ipv6-pool] -{% for prfx in ipv6['prfx'] %} -{{prfx}} -{% endfor %} -{% for pd in ipv6['pd'] %} -delegate={{pd}} -{% endfor %} -{% endif %} - -{% if auth['mech'] == 'local' %} -[chap-secrets] -chap-secrets=/etc/accel-ppp/ipoe/chap-secrets -{% endif %} - -{% if auth['mech'] == 'radius' %} -[radius] -verbose=1 -{% for srv in auth['radius'] %} -server={{srv}},{{auth['radius'][srv]['secret']}},\ -req-limit={{auth['radius'][srv]['req-limit']}},\ -fail-time={{auth['radius'][srv]['fail-time']}} -{% endfor %} -{% if auth['radsettings']['dae-server']['ip-address'] %} -dae-server={{auth['radsettings']['dae-server']['ip-address']}}:\ -{{auth['radsettings']['dae-server']['port']}},\ -{{auth['radsettings']['dae-server']['secret']}} -{% endif -%} -{% if auth['radsettings']['acct-timeout'] %} -acct-timeout={{auth['radsettings']['acct-timeout']}} -{% endif -%} -{% if auth['radsettings']['max-try'] %} -max-try={{auth['radsettings']['max-try']}} -{% endif -%} -{% if auth['radsettings']['timeout'] %} -timeout={{auth['radsettings']['timeout']}} -{% endif -%} -{% if auth['radsettings']['nas-ip-address'] %} -nas-ip-address={{auth['radsettings']['nas-ip-address']}} -{% endif -%} -{% if auth['radsettings']['nas-identifier'] %} -nas-identifier={{auth['radsettings']['nas-identifier']}} -{% endif -%} -{% endif %} - -[cli] -tcp=127.0.0.1:2002 -''' - -# chap secrets -chap_secrets_conf = ''' -# username server password acceptable local IP addresses shaper -{% for aifc in auth['auth_if'] %} -{% for mac in auth['auth_if'][aifc] %} -{% if (auth['auth_if'][aifc][mac]['up']) and (auth['auth_if'][aifc][mac]['down']) %} -{% if auth['auth_if'][aifc][mac]['vlan'] %} -{{aifc}}.{{auth['auth_if'][aifc][mac]['vlan']}}\t*\t{{mac.lower()}}\t*\t{{auth['auth_if'][aifc][mac]['down']}}/{{auth['auth_if'][aifc][mac]['up']}} -{% else %} -{{aifc}}\t*\t{{mac.lower()}}\t*\t{{auth['auth_if'][aifc][mac]['down']}}/{{auth['auth_if'][aifc][mac]['up']}} -{% endif %} -{% else %} -{% if auth['auth_if'][aifc][mac]['vlan'] %} -{{aifc}}.{{auth['auth_if'][aifc][mac]['vlan']}}\t*\t{{mac.lower()}}\t* -{% else %} -{{aifc}}\t*\t{{mac.lower()}}\t* -{% endif %} -{% endif %} -{% endfor %} -{% endfor %} -''' - if not os.path.exists(ipoe_cnf_dir): os.makedirs(ipoe_cnf_dir) - sl.syslog(sl.LOG_NOTICE, ipoe_cnf_dir + " created") def _get_cpu(): @@ -201,13 +51,13 @@ def _get_cpu(): def _chk_con(): cnt = 0 - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s = socket(AF_INET, SOCK_STREAM) while True: try: s.connect(("127.0.0.1", int(cmd_port))) break except ConnectionRefusedError: - time.sleep(0.5) + sleep(0.5) cnt += 1 if cnt == 100: raise("failed to start pppoe server") @@ -224,18 +74,6 @@ def _accel_cmd(cmd=''): except: return 1 -# chap_secrets file if auth mode local - - -def _gen_chap_secrets(c): - - tmpl = jinja2.Template(chap_secrets_conf, trim_blocks=True) - chap_secrets_txt = tmpl.render(c) - old_umask = os.umask(0o077) - open(chap_secrets, 'w').write(chap_secrets_txt) - os.umask(old_umask) - sl.syslog(sl.LOG_NOTICE, chap_secrets + ' written') - ##### Inline functions end #### @@ -388,14 +226,25 @@ 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': - _gen_chap_secrets(c) - - tmpl = jinja2.Template(ipoe_config, trim_blocks=True) + 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) + os.umask(old_umask) + + tmpl = env.get_template('ipoe.config.tmpl') config_text = tmpl.render(c) - open(ipoe_cnf, 'w').write(config_text) + with open(ipoe_cnf, 'w') as f: + f.write(config_text) return c @@ -465,7 +314,6 @@ def apply(c): raise ConfigError('accel-pppd failed to start') else: _accel_cmd('restart') - sl.syslog(sl.LOG_NOTICE, "reloading config via daemon restart") if __name__ == '__main__': @@ -476,4 +324,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/service-pppoe.py b/src/conf_mode/service-pppoe.py index 22250d18b..afcc5ba99 100755 --- a/src/conf_mode/service-pppoe.py +++ b/src/conf_mode/service-pppoe.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,19 +13,18 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import os import re import subprocess -import jinja2 -import socket -import time -import syslog as sl + +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 pidfile = r'/var/run/accel_pppoe.pid' @@ -38,282 +37,26 @@ pppoe_conf = pppoe_cnf_dir + '/pppoe.config' # config path creation if not os.path.exists(pppoe_cnf_dir): os.makedirs(pppoe_cnf_dir) - sl.syslog(sl.LOG_NOTICE, pppoe_cnf_dir + " created") - -pppoe_config = ''' -### generated by accel_pppoe.py ### -[modules] -log_syslog -pppoe -{% if authentication['mode'] == 'radius' %} -radius -{% endif %} -ippool -{% if ppp_options['ipv6'] != 'deny' %} -ipv6pool -ipv6_nd -ipv6_dhcp -{% endif %} -chap-secrets -auth_pap -auth_chap_md5 -auth_mschap_v1 -auth_mschap_v2 -#pppd_compat -shaper -{% if snmp == 'enable' or snmp == 'enable-ma' %} -net-snmp -{% endif %} -{% if limits %} -connlimit -{% endif %} - -[core] -thread-count={{thread_cnt}} - -[log] -syslog=accel-pppoe,daemon -copy=1 -level=5 - -{% if snmp == 'enable-ma' %} -[snmp] -master=1 -{% endif -%} - -[client-ip-range] -disable - -{% if ppp_gw %} -[ip-pool] -gw-ip-address={{ppp_gw}} -{% if client_ip_pool %} -{{client_ip_pool}} -{% endif -%} - -{% if client_ip_subnets %} -{% for sn in client_ip_subnets %} -{{sn}} -{% endfor %} -{% endif %} -{% endif -%} - -{% if client_ipv6_pool %} -[ipv6-pool] -{% for prfx in client_ipv6_pool['prefix']: %} -{{prfx}} -{% endfor %} -{% for prfx in client_ipv6_pool['delegate-prefix']: %} -delegate={{prfx}} -{% endfor %} -{% endif %} - -{% if dns %} -[dns] -{% if dns[0] %} -dns1={{dns[0]}} -{% endif -%} -{% if dns[1] %} -dns2={{dns[1]}} -{% endif -%} -{% endif %} - -{% if dnsv6 %} -[ipv6-dns] -{% for srv in dnsv6: %} -{{srv}} -{% endfor %} -{% endif %} - -{% if wins %} -[wins] -{% if wins[0] %} -wins1={{wins[0]}} -{% endif %} -{% if wins[1] %} -wins2={{wins[1]}} -{% endif -%} -{% endif -%} - -{% if authentication['mode'] == 'local' %} -[chap-secrets] -chap-secrets=/etc/accel-ppp/pppoe/chap-secrets -{% endif -%} - -{% if authentication['mode'] == 'radius' %} -[radius] -{% for rsrv in authentication['radiussrv']: %} -server={{rsrv}},{{authentication['radiussrv'][rsrv]['secret']}},\ -req-limit={{authentication['radiussrv'][rsrv]['req-limit']}},\ -fail-time={{authentication['radiussrv'][rsrv]['fail-time']}} -{% endfor %} -{% if authentication['radiusopt']['timeout'] %} -timeout={{authentication['radiusopt']['timeout']}} -{% endif %} -{% if authentication['radiusopt']['acct-timeout'] %} -acct-timeout={{authentication['radiusopt']['acct-timeout']}} -{% endif %} -{% if authentication['radiusopt']['max-try'] %} -max-try={{authentication['radiusopt']['max-try']}} -{% endif %} -{% if authentication['radiusopt']['nas-id'] %} -nas-identifier={{authentication['radiusopt']['nas-id']}} -{% endif %} -{% if authentication['radiusopt']['nas-ip'] %} -nas-ip-address={{authentication['radiusopt']['nas-ip']}} -{% endif -%} -{% if authentication['radiusopt']['dae-srv'] %} -dae-server={{authentication['radiusopt']['dae-srv']['ip-addr']}}:\ -{{authentication['radiusopt']['dae-srv']['port']}},\ -{{authentication['radiusopt']['dae-srv']['secret']}} -{% endif -%} -gw-ip-address={{ppp_gw}} -verbose=1 - -{% if authentication['radiusopt']['shaper'] %} -[shaper] -verbose=1 -attr={{authentication['radiusopt']['shaper']['attr']}} -{% if authentication['radiusopt']['shaper']['vendor'] %} -vendor={{authentication['radiusopt']['shaper']['vendor']}} -{% endif -%} -{% endif -%} -{% endif %} - -[ppp] -verbose=1 -check-ip=1 -{% if not sesscrtl == 'disable' %} -single-session={{sesscrtl}} -{% endif -%} -{% if ppp_options['ccp'] %} -ccp=1 -{% endif %} -{% if ppp_options['min-mtu'] %} -min-mtu={{ppp_options['min-mtu']}} -{% else %} -min-mtu={{mtu}} -{% endif %} -{% if ppp_options['mru'] %} -mru={{ppp_options['mru']}} -{% endif %} -{% if ppp_options['mppe'] %} -mppe={{ppp_options['mppe']}} -{% else %} -mppe=prefer -{% endif %} -{% if ppp_options['lcp-echo-interval'] %} -lcp-echo-interval={{ppp_options['lcp-echo-interval']}} -{% else %} -lcp-echo-interval=30 -{% endif %} -{% if ppp_options['lcp-echo-timeout'] %} -lcp-echo-timeout={{ppp_options['lcp-echo-timeout']}} -{% endif %} -{% if ppp_options['lcp-echo-failure'] %} -lcp-echo-failure={{ppp_options['lcp-echo-failure']}} -{% else %} -lcp-echo-failure=3 -{% endif %} -{% if ppp_options['ipv4'] %} -ipv4={{ppp_options['ipv4']}} -{% endif %} -{% if client_ipv6_pool %} -ipv6=allow -{% endif %} - -{% if ppp_options['ipv6'] %} -ipv6={{ppp_options['ipv6']}} -{% if ppp_options['ipv6-intf-id'] %} -ipv6-intf-id={{ppp_options['ipv6-intf-id']}} -{% endif %} -{% if ppp_options['ipv6-peer-intf-id'] %} -ipv6-peer-intf-id={{ppp_options['ipv6-peer-intf-id']}} -{% endif %} -{% if ppp_options['ipv6-accept-peer-intf-id'] %} -ipv6-accept-peer-intf-id={{ppp_options['ipv6-accept-peer-intf-id']}} -{% endif %} -{% endif %} -mtu={{mtu}} - -[pppoe] -verbose=1 -{% if concentrator %} -ac-name={{concentrator}} -{% endif %} -{% if interface %} -{% for int in interface %} -interface={{int}} -{% if interface[int]['vlans'] %} -vlan-mon={{int}},{{interface[int]['vlans']|join(',')}} -interface=re:{{int}}\.\d+ -{% endif %} -{% endfor -%} -{% endif -%} - -{% if svc_name %} -service-name={{svc_name|join(',')}} -{% endif -%} - -{% if pado_delay %} -pado-delay={{pado_delay}} -{% endif %} - -{% if limits %} -[connlimit] -limit={{limits['conn-limit']}} -burst={{limits['burst']}} -timeout={{limits['timeout']}} -{% endif %} - -[cli] -tcp=127.0.0.1:2001 -''' - -# pppoe chap secrets -chap_secrets_conf = ''' -# username server password acceptable local IP addresses shaper -{% for user in authentication['local-users'] %} -{% if authentication['local-users'][user]['state'] == 'enabled' %} -{% if (authentication['local-users'][user]['upload']) and (authentication['local-users'][user]['download']) %} -{{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}}\t\ -{{authentication['local-users'][user]['download']}}/{{authentication['local-users'][user]['upload']}} -{% else %} -{{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}} -{% endif %} -{% endif %} -{% endfor %} -''' + # # 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) + s = socket(AF_INET, SOCK_STREAM) while True: try: s.connect(("127.0.0.1", 2001)) break except ConnectionRefusedError: - time.sleep(0.5) + sleep(0.5) cnt += 1 if cnt == 100: raise("failed to start pppoe server") -def _write_chap_secrets(c): - tmpl = jinja2.Template(chap_secrets_conf, trim_blocks=True) - chap_secrets_txt = tmpl.render(c) - old_umask = os.umask(0o077) - open(chap_secrets, 'w').write(chap_secrets_txt) - os.umask(old_umask) - sl.syslog(sl.LOG_NOTICE, chap_secrets + ' written') - - def _accel_cmd(cmd=''): if not cmd: return None @@ -640,6 +383,11 @@ 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: @@ -653,12 +401,18 @@ def generate(c): else: c['thread_cnt'] = int(os.cpu_count() / 2) - tmpl = jinja2.Template(pppoe_config, trim_blocks=True) + tmpl = env.get_template('pppoe.config.tmpl') config_text = tmpl.render(c) - open(pppoe_conf, 'w').write(config_text) + with open(pppoe_conf, 'w') as f: + f.write(config_text) if c['authentication']['local-users']: - _write_chap_secrets(c) + 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) + os.umask(old_umask) return c @@ -680,7 +434,6 @@ def apply(c): raise ConfigError('accel-pppd failed to start') else: _accel_cmd('restart') - sl.syslog(sl.LOG_NOTICE, "reloading config via daemon restart") if __name__ == '__main__': @@ -691,4 +444,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/service-router-advert.py b/src/conf_mode/service-router-advert.py index 1e0d28397..38c5cb2dc 100755 --- a/src/conf_mode/service-router-advert.py +++ b/src/conf_mode/service-router-advert.py @@ -15,55 +15,17 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import sys -import jinja2 +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 config_file = r'/etc/radvd.conf' -config_tmpl = """ -### Autogenerated by service-router-advert.py ### - -{% for i in interfaces -%} -interface {{ i.name }} { - IgnoreIfMissing on; - AdvDefaultPreference {{ i.default_preference }}; - AdvManagedFlag {{ i.managed_flag }}; - MaxRtrAdvInterval {{ i.interval_max }}; -{% if i.interval_min %} - MinRtrAdvInterval {{ i.interval_min }}; -{% endif %} - AdvReachableTime {{ i.reachable_time }}; - AdvIntervalOpt {{ i.send_advert }}; - AdvSendAdvert {{ i.send_advert }}; -{% if i.default_lifetime %} - AdvDefaultLifetime {{ i.default_lifetime }}; -{% endif %} -{% if i.link_mtu %} - AdvLinkMTU {{ i.link_mtu }}; -{% endif %} - AdvOtherConfigFlag {{ i.other_config_flag }}; - AdvRetransTimer {{ i.retrans_timer }}; - AdvCurHopLimit {{ i.hop_limit }}; -{% for p in i.prefixes %} - prefix {{ p.prefix }} { - AdvAutonomous {{ p.autonomous_flag }}; - AdvValidLifetime {{ p.valid_lifetime }}; - AdvOnLink {{ p.on_link }}; - AdvPreferredLifetime {{ p.preferred_lifetime }}; - }; -{% endfor %} -{% if i.name_server %} - RDNSS {{ i.name_server | join(" ") }} { - }; -{% endif %} -}; -{% endfor -%} -""" - default_config_data = { 'interfaces': [] } @@ -175,7 +137,12 @@ def generate(rtradv): if not rtradv['interfaces']: return None - tmpl = jinja2.Template(config_tmpl, trim_blocks=True) + # 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) @@ -189,13 +156,13 @@ def generate(rtradv): def apply(rtradv): if not rtradv['interfaces']: # bail out early - looks like removal from running config - os.system('sudo systemctl stop radvd.service') + os.system('systemctl stop radvd.service') if os.path.exists(config_file): os.unlink(config_file) return None - os.system('sudo systemctl restart radvd.service') + os.system('systemctl restart radvd.service') return None if __name__ == '__main__': @@ -206,4 +173,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index 34832aac1..ed8c1d7e1 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -15,16 +15,17 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import jinja2 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.config import Config from vyos import ConfigError config_file_client = r'/etc/snmp/snmp.conf' @@ -42,168 +43,6 @@ OIDs = { 'none': '.1.3.6.1.6.3.10.1.2.1' } -# SNMP template (/etc/snmp/snmp.conf) - be careful if you edit the template. -client_config_tmpl = """ -### Autogenerated by snmp.py ### -{% if trap_source -%} -clientaddr {{ trap_source }} -{% endif %} - -""" - -# SNMP template (/usr/share/snmp/snmpd.conf) - be careful if you edit the template. -access_config_tmpl = """ -### Autogenerated by snmp.py ### -{%- for u in v3_users %} -{{ u.mode }}user {{ u.name }} -{%- endfor %} - -rwuser {{ vyos_user }} - -""" - -# SNMP template (/var/lib/snmp/snmpd.conf) - be careful if you edit the template. -user_config_tmpl = """ -### Autogenerated by snmp.py ### -# user -{%- for u in v3_users %} -{%- if u.authOID == 'none' %} -createUser {{ u.name }} -{%- elif u.authPassword %} -createUser {{ u.name }} {{ u.authProtocol | upper }} "{{ u.authPassword }}" {{ u.privProtocol | upper }} {{ u.privPassword }} -{%- else %} -usmUser 1 3 {{ v3_engineid }} "{{ u.name }}" "{{ u.name }}" NULL {{ u.authOID }} {{ u.authMasterKey }} {{ u.privOID }} {{ u.privMasterKey }} 0x -{%- endif %} -{%- endfor %} - -createUser {{ vyos_user }} MD5 "{{ vyos_user_pass }}" DES -{%- if v3_engineid %} -oldEngineID {{ v3_engineid }} -{%- endif %} -""" - -# SNMP template (/etc/snmp/snmpd.conf) - be careful if you edit the template. -daemon_config_tmpl = """ -### Autogenerated by snmp.py ### - -# non configurable defaults -sysObjectID 1.3.6.1.4.1.44641 -sysServices 14 -master agentx -agentXPerms 0777 0777 -pass .1.3.6.1.2.1.31.1.1.1.18 /opt/vyatta/sbin/if-mib-alias -smuxpeer .1.3.6.1.2.1.83 -smuxpeer .1.3.6.1.2.1.157 -smuxsocket localhost - -# linkUp/Down configure the Event MIB tables to monitor -# the ifTable for network interfaces being taken up or down -# for making internal queries to retrieve any necessary information -iquerySecName {{ vyos_user }} - -# Modified from the default linkUpDownNotification -# to include more OIDs and poll more frequently -notificationEvent linkUpTrap linkUp ifIndex ifDescr ifType ifAdminStatus ifOperStatus -notificationEvent linkDownTrap linkDown ifIndex ifDescr ifType ifAdminStatus ifOperStatus -monitor -r 10 -e linkUpTrap "Generate linkUp" ifOperStatus != 2 -monitor -r 10 -e linkDownTrap "Generate linkDown" ifOperStatus == 2 - -######################## -# configurable section # -######################## - -# Default system description is VyOS version -sysDescr VyOS {{ version }} - -{% if description %} -# Description -SysDescr {{ description }} -{%- endif %} - -# Listen -agentaddress unix:/run/snmpd.socket{% if listen_on %}{% for li in listen_on %},{{ li }}{% endfor %}{% else %},udp:161{% if ipv6_enabled %},udp6:161{% endif %}{% endif %} - -# SNMP communities -{%- for c in communities %} - -{%- if c.network_v4 %} -{%- for network in c.network_v4 %} -{{ c.authorization }}community {{ c.name }} {{ network }} -{%- endfor %} -{%- elif not c.has_source %} -{{ c.authorization }}community {{ c.name }} -{%- endif %} - -{%- if c.network_v6 %} -{%- for network in c.network_v6 %} -{{ c.authorization }}community6 {{ c.name }} {{ network }} -{%- endfor %} -{%- elif not c.has_source %} -{{ c.authorization }}community6 {{ c.name }} -{%- endif %} - -{%- endfor %} - -{% if contact %} -# system contact information -SysContact {{ contact }} -{%- endif %} - -{% if location %} -# system location information -SysLocation {{ location }} -{%- endif %} - -{% if smux_peers -%} -# additional smux peers -{%- for sp in smux_peers %} -smuxpeer {{ sp }} -{%- endfor %} -{%- endif %} - -{% if trap_targets -%} -# if there is a problem - tell someone! -{%- for t in trap_targets %} -trap2sink {{ t.target }}{% if t.port -%}:{{ t.port }}{% endif %} {{ t.community }} -{%- endfor %} -{%- endif %} - -{%- if v3_enabled %} -# -# SNMPv3 stuff goes here -# -# views -{%- for v in v3_views %} -{%- for oid in v.oids %} -view {{ v.name }} included .{{ oid.oid }} -{%- endfor %} -{%- endfor %} - -# access -# context sec.model sec.level match read write notif -{%- for g in v3_groups %} -access {{ g.name }} "" usm {{ g.seclevel }} exact {{ g.view }} {% if g.mode == 'ro' %}none{% else %}{{ g.view }}{% endif %} none -{%- endfor %} - -# trap-target -{%- for t in v3_traps %} -trapsess -v 3 {{ '-Ci' if t.type == 'inform' }} -e {{ v3_engineid }} -u {{ t.secName }} -l {{ t.secLevel }} -a {{ t.authProtocol }} {% if t.authPassword %}-A {{ t.authPassword }}{% elif t.authMasterKey %}-3m {{ t.authMasterKey }}{% endif %} -x {{ t.privProtocol }} {% if t.privPassword %}-X {{ t.privPassword }}{% elif t.privMasterKey %}-3M {{ t.privMasterKey }}{% endif %} {{ t.ipProto }}:{{ t.ipAddr }}:{{ t.ipPort }} -{%- endfor %} - -# group -{%- for u in v3_users %} -group {{ u.group }} usm {{ u.name }} -{% endfor %} -{%- endif %} - -{% if script_ext %} -# extension scripts -{%- for ext in script_ext|sort(attribute='name') %} -extend {{ ext.name }} {{ ext.script }} -{%- endfor %} -{% endif %} -""" - default_config_data = { 'listen_on': [], 'listen_address': [], @@ -669,34 +508,39 @@ 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 os.system("systemctl stop snmpd.service") - rmfile(config_file_client) - rmfile(config_file_daemon) - rmfile(config_file_access) - rmfile(config_file_user) + config_files = [config_file_client, config_file_daemon, config_file_access, + config_file_user] + for file in config_files: + rmfile(file) 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 = jinja2.Template(client_config_tmpl) + 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) # Write server config file - tmpl = jinja2.Template(daemon_config_tmpl) + 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) # Write access rights config file - tmpl = jinja2.Template(access_config_tmpl) + 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) # Write access rights config file - tmpl = jinja2.Template(user_config_tmpl) + 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) diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py index 9fe22bfee..014045796 100755 --- a/src/conf_mode/ssh.py +++ b/src/conf_mode/ssh.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,149 +13,17 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import os - -import jinja2 +from 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 config_file = r'/etc/ssh/sshd_config' -# Please be careful if you edit the template. -config_tmpl = """ - -### Autogenerated by ssh.py ### - -# Non-configurable defaults -Protocol 2 -HostKey /etc/ssh/ssh_host_rsa_key -HostKey /etc/ssh/ssh_host_dsa_key -HostKey /etc/ssh/ssh_host_ecdsa_key -HostKey /etc/ssh/ssh_host_ed25519_key -SyslogFacility AUTH -LoginGraceTime 120 -StrictModes yes -PubkeyAuthentication yes -IgnoreRhosts yes -HostbasedAuthentication no -PermitEmptyPasswords no -ChallengeResponseAuthentication no -X11Forwarding yes -X11DisplayOffset 10 -PrintMotd no -PrintLastLog yes -TCPKeepAlive yes -Banner /etc/issue.net -Subsystem sftp /usr/lib/openssh/sftp-server -UsePAM yes -HostKey /etc/ssh/ssh_host_rsa_key - -# Specifies whether sshd should look up the remote host name, -# and to check that the resolved host name for the remote IP -# address maps back to the very same IP address. -UseDNS {{ host_validation }} - -# Specifies the port number that sshd listens on. The default is 22. -# Multiple options of this type are permitted. -{% if mport|length != 0 %} -{% for p in mport %} -Port {{ p }} -{% endfor %} -{% else %} -Port {{ port }} -{% endif %} - -# Gives the verbosity level that is used when logging messages from sshd -LogLevel {{ log_level }} - -# Specifies whether root can log in using ssh -PermitRootLogin no - -# Specifies whether password authentication is allowed -PasswordAuthentication {{ password_authentication }} - -{% if listen_on %} -# Specifies the local addresses sshd should listen on -{% for a in listen_on %} -ListenAddress {{ a }} -{% endfor %} -{{ "\n" }} -{% endif %} - -{%- if ciphers %} -# Specifies the ciphers allowed. Multiple ciphers must be comma-separated. -# -# NOTE: As of now, there is no 'multi' node for 'ciphers', thus we have only one :/ -Ciphers {{ ciphers | join(",") }} -{{ "\n" }} -{% endif %} - -{%- if mac %} -# Specifies the available MAC (message authentication code) algorithms. The MAC -# algorithm is used for data integrity protection. Multiple algorithms must be -# comma-separated. -# -# NOTE: As of now, there is no 'multi' node for 'mac', thus we have only one :/ -MACs {{ mac | join(",") }} -{{ "\n" }} -{% endif %} - -{%- if key_exchange %} -# Specifies the available KEX (Key Exchange) algorithms. Multiple algorithms must -# be comma-separated. -# -# NOTE: As of now, there is no 'multi' node for 'key-exchange', thus we have only one :/ -KexAlgorithms {{ key_exchange | join(",") }} -{{ "\n" }} -{% endif %} - -{%- if allow_users %} -# This keyword can be followed by a list of user name patterns, separated by spaces. -# If specified, login is allowed only for user names that match one of the patterns. -# Only user names are valid, a numerical user ID is not recognized. -AllowUsers {{ allow_users | join(" ") }} -{{ "\n" }} -{% endif %} - -{%- if allow_groups %} -# This keyword can be followed by a list of group name patterns, separated by spaces. -# If specified, login is allowed only for users whose primary group or supplementary -# group list matches one of the patterns. Only group names are valid, a numerical group -# ID is not recognized. -AllowGroups {{ allow_groups | join(" ") }} -{{ "\n" }} -{% endif %} - -{%- if deny_users %} -# This keyword can be followed by a list of user name patterns, separated by spaces. -# Login is disallowed for user names that match one of the patterns. Only user names -# are valid, a numerical user ID is not recognized. -DenyUsers {{ deny_users | join(" ") }} -{{ "\n" }} -{% endif %} - -{%- if deny_groups %} -# This keyword can be followed by a list of group name patterns, separated by spaces. -# Login is disallowed for users whose primary group or supplementary group list matches -# one of the patterns. Only group names are valid, a numerical group ID is not recognized. -DenyGroups {{ deny_groups | join(" ") }} -{{ "\n" }} -{% endif %} - -{%- if client_keepalive %} -# Sets a timeout interval in seconds after which if no data has been received from the client, -# sshd will send a message through the encrypted channel to request a response from the client. -# The default is 0, indicating that these messages will not be sent to the client. -# This option applies to protocol version 2 only. -ClientAliveInterval {{ client_keepalive }} -{% endif %} -""" - default_config_data = { 'port' : '22', 'log_level': 'INFO', @@ -250,7 +118,12 @@ def generate(ssh): if ssh is None: return None - tmpl = jinja2.Template(config_tmpl, trim_blocks=True) + # 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) @@ -275,4 +148,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py index 959e86e5b..7acb0a9a2 100755 --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -14,36 +14,21 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import sys import os -import jinja2 +from 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 from subprocess import Popen, PIPE, STDOUT -from psutil import users +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 radius_config_file = "/etc/pam_radius_auth.conf" -radius_config_tmpl = """ -# Automatically generated by VyOS -# RADIUS configuration file -{%- if radius_server %} -# server[:port] shared_secret timeout (s) source_ip -{% for s in radius_server %} -{%- if not s.disabled -%} -{{ s.address }}:{{ s.port }} {{ s.key }} {{ s.timeout }} {% if radius_source_address -%}{{ radius_source_address }}{% endif %} -{% endif %} -{%- endfor %} - -priv-lvl 15 -mapped_priv_user radius_priv_user -{% endif %} - -""" default_config_data = { 'deleted': False, @@ -229,7 +214,12 @@ def generate(login): 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: - tmpl = jinja2.Template(radius_config_tmpl) + # 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) + + 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) @@ -364,4 +354,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py index 2d47cc061..8c0a6629c 100755 --- a/src/conf_mode/system-syslog.py +++ b/src/conf_mode/system-syslog.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,85 +13,18 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import os import re -import subprocess -import jinja2 + +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 subprocess_cmd from vyos import ConfigError -# config templates - -# /etc/rsyslog.d/vyos-rsyslog.conf ### -configs = ''' -## generated by syslog.py ## -## file based logging -{% if files['global']['marker'] -%} -$ModLoad immark -{% if files['global']['marker-interval'] %} -$MarkMessagePeriod {{files['global']['marker-interval']}} -{% endif %} -{% endif -%} -{% if files['global']['preserver_fqdn'] -%} -$PreserveFQDN on -{% endif -%} -{% for file in files %} -$outchannel {{file}},{{files[file]['log-file']}},{{files[file]['max-size']}},{{files[file]['action-on-max-size']}} -{{files[file]['selectors']}} :omfile:${{file}} -{% endfor %} -{% if console %} -## console logging -{% for con in console %} -{{console[con]['selectors']}} /dev/console -{% endfor %} -{% endif %} -{% if hosts %} -## remote logging -{% for host in hosts %} -{% if hosts[host]['proto'] == 'tcp' %} -{% if hosts[host]['port'] %} -{{hosts[host]['selectors']}} @@{{host}}:{{hosts[host]['port']}} -{% else %} -{{hosts[host]['selectors']}} @@{{host}} -{% endif %} -{% else %} -{% if hosts[host]['port'] %} -{{hosts[host]['selectors']}} @{{host}}:{{hosts[host]['port']}} -{% else %} -{{hosts[host]['selectors']}} @{{host}} -{% endif %} -{% endif %} -{% endfor %} -{% endif %} -{% if user %} -{% for u in user %} -{{user[u]['selectors']}} :omusrmsg:{{u}} -{% endfor %} -{% endif %} -''' - -logrotate_configs = ''' -{% for file in files %} -{{files[file]['log-file']}} { - missingok - notifempty - create - rotate {{files[file]['max-files']}} - size={{files[file]['max-size']//1024}}k - postrotate - invoke-rc.d rsyslog rotate > /dev/null - endscript -} -{% endfor %} -''' -# config templates end - - def get_config(): c = Config() if not c.exists('system syslog'): @@ -259,14 +192,19 @@ def generate(c): if c == None: return None - tmpl = jinja2.Template(configs, trim_blocks=True) + # 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) # eventually write for each file its own logrotate file, since size is # defined it shouldn't matter - tmpl = jinja2.Template(logrotate_configs, trim_blocks=True) + 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) @@ -315,10 +253,10 @@ def verify(c): def apply(c): if not c: - subprocess.call(['sudo', 'systemctl', 'stop', 'syslog']) - return 0 + subprocess_cmd('systemctl stop syslog') + return None - subprocess.call(['sudo', 'systemctl', 'restart', 'syslog']) + subprocess_cmd('systemctl restart syslog') if __name__ == '__main__': try: @@ -328,4 +266,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/system-wifi-regdom.py b/src/conf_mode/system-wifi-regdom.py index 01dc92a20..943c42274 100755 --- a/src/conf_mode/system-wifi-regdom.py +++ b/src/conf_mode/system-wifi-regdom.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 @@ -15,52 +15,34 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import jinja2 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 config_80211_file='/etc/modprobe.d/cfg80211.conf' config_crda_file='/etc/default/crda' -# Please be careful if you edit the template. -config_80211_tmpl = """ -{%- if regdom -%} -options cfg80211 ieee80211_regdom={{ regdom }} -{% endif %} -""" - -# Please be careful if you edit the template. -config_crda_tmpl = """ -{%- if regdom -%} -REGDOMAIN={{ regdom }} -{% endif %} -""" - default_config_data = { 'regdom' : '', 'deleted' : False } - def get_config(): regdom = deepcopy(default_config_data) conf = Config() - - # set new configuration level - conf.set_level('system') + base = ['system', 'wifi-regulatory-domain'] # Check if interface has been removed - if not conf.exists('wifi-regulatory-domain'): + if not conf.exists(base): regdom['deleted'] = True return regdom - - # retrieve configured regulatory domain - if conf.exists('wifi-regulatory-domain'): - regdom['regdom'] = conf.return_value('wifi-regulatory-domain') + else: + regdom['regdom'] = conf.return_value(base) return regdom @@ -85,12 +67,17 @@ def generate(regdom): return None - tmpl = jinja2.Template(config_80211_tmpl) + # 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 = jinja2.Template(config_crda_tmpl) + tmpl = env.get_template('crda.tmpl') config_text = tmpl.render(regdom) with open(config_crda_file, 'w') as f: f.write(config_text) diff --git a/src/conf_mode/tftp_server.py b/src/conf_mode/tftp_server.py index ff7cad0c9..fe2da8455 100755 --- a/src/conf_mode/tftp_server.py +++ b/src/conf_mode/tftp_server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,32 +13,23 @@ # # 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 stat import pwd -import copy -import glob -import jinja2 -import vyos.validate +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 config_file = r'/etc/default/tftpd' -# Please be careful if you edit the template. -config_tmpl = """ -### Autogenerated by tftp_server.py ### -DAEMON_ARGS="--listen --user tftp --address {% for a in listen-%}{{ a }}{% endfor %}{% if allow_upload %} --create --umask 000{% endif %} --secure {{ directory }}" - - -""" - default_config_data = { 'directory': '', 'allow_upload': False, @@ -47,23 +38,25 @@ default_config_data = { } def get_config(): - tftpd = copy.deepcopy(default_config_data) + tftpd = deepcopy(default_config_data) conf = Config() - if not conf.exists('service tftp-server'): + base = ['service', 'tftp-server'] + if not conf.exists(base): return None else: - conf.set_level('service tftp-server') + conf.set_level(base) - if conf.exists('directory'): - tftpd['directory'] = conf.return_value('directory') + if conf.exists(['directory']): + tftpd['directory'] = conf.return_value(['directory']) - if conf.exists('allow-upload'): + if conf.exists(['allow-upload']): tftpd['allow_upload'] = True - if conf.exists('port'): - tftpd['port'] = conf.return_value('port') + if conf.exists(['port']): + tftpd['port'] = conf.return_value(['port']) - tftpd['listen'] = conf.return_values('listen-address') + if conf.exists(['listen-address']): + tftpd['listen'] = conf.return_values(['listen-address']) return tftpd @@ -80,7 +73,7 @@ def verify(tftpd): raise ConfigError('TFTP server listen address must be configured!') for addr in tftpd['listen']: - if not vyos.validate.is_addr_assigned(addr): + if not is_addr_assigned(addr): print('WARNING: TFTP server listen address {0} not assigned to any interface!'.format(addr)) return None @@ -88,22 +81,27 @@ def verify(tftpd): def generate(tftpd): # cleanup any available configuration file # files will be recreated on demand - for i in glob.glob(config_file + '*'): + for i in glob(config_file + '*'): os.unlink(i) # bail out early - looks like removal from running config 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 = copy.deepcopy(tftpd) - if vyos.validate.is_ipv4(listen): + config = deepcopy(tftpd) + if is_ipv4(listen): config['listen'] = [listen + ":" + tftpd['port'] + " -4"] else: config['listen'] = ["[" + listen + "]" + tftpd['port'] + " -6"] - tmpl = jinja2.Template(config_tmpl) + tmpl = env.get_template('default.tmpl') config_text = tmpl.render(config) file = config_file + str(idx) with open(file, 'w') as f: @@ -115,7 +113,7 @@ def generate(tftpd): def apply(tftpd): # stop all services first - then we will decide - os.system('sudo systemctl stop tftpd@{0..20}') + os.system('systemctl stop tftpd@{0..20}') # bail out early - e.g. service deletion if tftpd is None: @@ -140,7 +138,7 @@ def apply(tftpd): idx = 0 for listen in tftpd['listen']: - os.system('sudo systemctl restart tftpd@{0}.service'.format(idx)) + os.system('systemctl restart tftpd@{0}.service'.format(idx)) idx = idx + 1 return None @@ -153,4 +151,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/vpn-pptp.py b/src/conf_mode/vpn-pptp.py index 355adf715..b1204a505 100755 --- a/src/conf_mode/vpn-pptp.py +++ b/src/conf_mode/vpn-pptp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,19 +13,18 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import os import re import subprocess -import jinja2 -import socket -import time -import syslog as sl + +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 pidfile = r'/var/run/accel_pptp.pid' @@ -36,117 +35,16 @@ pptp_conf = pptp_cnf_dir + '/pptp.config' # config path creation if not os.path.exists(pptp_cnf_dir): os.makedirs(pptp_cnf_dir) - sl.syslog(sl.LOG_NOTICE, pptp_cnf_dir + " created") - -pptp_config = ''' -### generated by accel_pptp.py ### -[modules] -log_syslog -pptp -ippool -chap-secrets -{% if authentication['auth_proto'] %} -{{ authentication['auth_proto'] }} -{% else %} -auth_mschap_v2 -{% endif %} -{% if authentication['mode'] == 'radius' %} -radius -{% endif -%} - -[core] -thread-count={{thread_cnt}} - -[log] -syslog=accel-pptp,daemon -copy=1 -level=5 - -{% if dns %} -[dns] -{% if dns[0] %} -dns1={{dns[0]}} -{% endif %} -{% if dns[1] %} -dns2={{dns[1]}} -{% endif %} -{% endif %} - -{% if wins %} -[wins] -{% if wins[0] %} -wins1={{wins[0]}} -{% endif %} -{% if wins[1] %} -wins2={{wins[1]}} -{% endif %} -{% endif %} - -[pptp] -ifname=pptp%d -{% if outside_addr %} -bind={{outside_addr}} -{% endif %} -verbose=1 -ppp-max-mtu={{mtu}} -mppe={{authentication['mppe']}} -echo-interval=10 -echo-failure=3 - - -[client-ip-range] -0.0.0.0/0 - -[ip-pool] -tunnel={{client_ip_pool}} -gw-ip-address={{gw_ip}} - -{% if authentication['mode'] == 'local' %} -[chap-secrets] -chap-secrets=/etc/accel-ppp/pptp/chap-secrets -{% endif %} - -[ppp] -verbose=5 -check-ip=1 -single-session=replace - -{% if authentication['mode'] == 'radius' %} -[radius] -{% for rsrv in authentication['radiussrv']: %} -server={{rsrv}},{{authentication['radiussrv'][rsrv]['secret']}},\ -req-limit={{authentication['radiussrv'][rsrv]['req-limit']}},\ -fail-time={{authentication['radiussrv'][rsrv]['fail-time']}} -{% endfor %} -timeout=30 -acct-timeout=30 -max-try=3 -{%endif %} - -[cli] -tcp=127.0.0.1:2003 -''' - -# pptp chap secrets -chap_secrets_conf = ''' -# username server password acceptable local IP addresses -{% for user in authentication['local-users'] %} -{% if authentication['local-users'][user]['state'] == 'enabled' %} -{{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}} -{% endif %} -{% endfor %} -''' - def _chk_con(): cnt = 0 - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s = socket(AF_INET, SOCK_STREAM) while True: try: s.connect(("127.0.0.1", 2003)) break except ConnectionRefusedError: - time.sleep(0.5) + sleep(0.5) cnt += 1 if cnt == 100: raise("failed to start pptp server") @@ -154,16 +52,6 @@ def _chk_con(): # chap_secrets file if auth mode local - -def _write_chap_secrets(c): - tmpl = jinja2.Template(chap_secrets_conf, trim_blocks=True) - chap_secrets_txt = tmpl.render(c) - old_umask = os.umask(0o077) - open(chap_secrets, 'w').write(chap_secrets_txt) - os.umask(old_umask) - sl.syslog(sl.LOG_NOTICE, chap_secrets + ' written') - - def _accel_cmd(cmd=''): if not cmd: return None @@ -326,6 +214,11 @@ 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: @@ -338,12 +231,18 @@ def generate(c): else: c['thread_cnt'] = int(os.cpu_count()/2) - tmpl = jinja2.Template(pptp_config, trim_blocks=True) + tmpl = env.get_template('pptp.config.tmpl') config_text = tmpl.render(c) - open(pptp_conf, 'w').write(config_text) + with open(pptp_conf, 'w') as f: + f.write(config_text) if c['authentication']['local-users']: - _write_chap_secrets(c) + 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) + os.umask(old_umask) return c @@ -366,8 +265,6 @@ def apply(c): else: # if gw ip changes, only restart doesn't work _accel_cmd('restart') - sl.syslog(sl.LOG_NOTICE, "reloading config via daemon restart") - if __name__ == '__main__': try: @@ -377,4 +274,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index 070437443..66b1822df 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -15,17 +15,18 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import jinja2 from time import sleep from sys import exit -from subprocess import Popen, PIPE, check_output +from subprocess import check_output 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.util import process_running +from vyos.defaults import directories as vyos_data_dir +from vyos.util import process_running, subprocess_cmd from vyos import ConfigError pidfile = r'/var/run/accel_sstp.pid' @@ -37,140 +38,6 @@ sstp_conf = sstp_cnf_dir + '/sstp.config' if not os.path.exists(sstp_cnf_dir): os.makedirs(sstp_cnf_dir) -sstp_config = """### generated by vpn_sstp.py ### -[modules] -log_syslog -sstp -shaper -{% if auth_mode == 'local' %} -chap-secrets -{% elif auth_mode == 'radius' %} -radius -{% endif -%} -ippool - -{% for proto in auth_proto %} -{{proto}} -{% endfor %} - -[core] -thread-count={{thread_cnt}} - -[common] -single-session=replace - -[log] -syslog=accel-sstp,daemon -copy=1 -level=5 - -[client-ip-range] -disable - -[sstp] -verbose=1 -accept=ssl -ssl-ca-file={{ ssl_ca }} -ssl-pemfile={{ ssl_cert }} -ssl-keyfile={{ ssl_key }} - -{% if client_ip_pool %} -[ip-pool] -gw-ip-address={{ client_gateway }} -{% for subnet in client_ip_pool %} -{{ subnet }} -{% endfor %} -{% endif %} - -{% if dnsv4 %} -[dns] -{% for dns in dnsv4 -%} -dns{{ loop.index }}={{ dns }} -{% endfor -%} -{% endif %} - -{% if auth_mode == 'local' %} -[chap-secrets] -chap-secrets=/etc/accel-ppp/sstp/chap-secrets -{% elif auth_mode == 'radius' %} -[radius] -verbose=1 -{% for r in radius_server %} -server={{ r.server }},{{ r.key }},auth-port={{ r.port }},req-limit=0,fail-time={{ r.fail_time }} -{% endfor -%} - -acct-timeout={{ radius_acct_tmo }} -timeout={{ radius_timeout }} -max-try={{ radius_max_try }} - -{% if radius_nas_id %} -nas-identifier={{ radius_nas_id }} -{% endif -%} -{% if radius_nas_ip %} -nas-ip-address={{ radius_nas_ip }} -{% endif -%} -{% if radius_source_address %} -bind={{ radius_source_address }} -{% endif -%} - - -{% if radius_dynamic_author %} -dae-server={{ radius_dynamic_author.server }}:{{ radius_dynamic_author.port }},{{ radius_dynamic_author.key }} -{% endif -%} -{% endif %} - -[ppp] -verbose=1 -check-ip=1 -{% if mtu %} -mtu={{ mtu }} -{% endif -%} - -{% if ppp_mppe %} -mppe={{ ppp_mppe }} -{% endif -%} -{% if ppp_echo_interval %} -lcp-echo-interval={{ ppp_echo_interval }} -{% endif -%} -{% if ppp_echo_failure %} -lcp-echo-failure={{ ppp_echo_failure }} -{% endif -%} -{% if ppp_echo_timeout %} -lcp-echo-timeout={{ ppp_echo_timeout }} -{% endif %} - -{% if radius_shaper_attr %} -[shaper] -verbose=1 -attr={{ radius_shaper_attr }} -{% if radius_shaper_vendor %} -vendor={{ radius_shaper_vendor }} -{% endif -%} -{% endif %} - -[cli] -tcp=127.0.0.1:2005 - -""" - -# sstp chap secrets -chap_secrets_conf = """ -# username server password acceptable local IP addresses shaper -{% for user in local_users %} -{% if user.state == 'enabled' %} -{% if user.upload and user.download %} -{{ "%-12s" | format(user.name) }} * {{ "%-16s" | format(user.password) }} {{ "%-16s" | format(user.ip) }} {{ user.download }} / {{ user.upload }} -{% else %} -{{ "%-12s" | format(user.name) }} * {{ "%-16s" | format(user.password) }} {{ "%-16s" | format(user.ip) }} -{% endif %} -{% endif %} -{% endfor %} -""" - -def subprocess_cmd(command): - p = Popen(command, stdout=PIPE, shell=True) - p.communicate() - def chk_con(): cnt = 0 s = socket(AF_INET, SOCK_STREAM) @@ -469,14 +336,19 @@ def generate(sstp): if sstp is None: 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) + # accel-cmd reload doesn't work so any change results in a restart of the daemon - tmpl = jinja2.Template(sstp_config, trim_blocks=True) + tmpl = env.get_template('sstp.config.tmpl') config_text = tmpl.render(sstp) with open(sstp_conf, 'w') as f: f.write(config_text) if sstp['local_users']: - tmpl = jinja2.Template(chap_secrets_conf, trim_blocks=True) + tmpl = env.get_template('chap-secrets.tmpl') config_text = tmpl.render(sstp) with open(chap_secrets, 'w') as f: f.write(config_text) diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index a74b79317..8cf4b72ae 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -15,35 +15,24 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import jinja2 from sys import exit from copy import deepcopy +from jinja2 import FileSystemLoader, Environment from json import loads from subprocess import check_output, CalledProcessError 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 from vyos import ConfigError config_file = r'/etc/iproute2/rt_tables.d/vyos-vrf.conf' -# Please be careful if you edit the template. -config_tmpl = """ -### Autogenerated by vrf.py ### -# -# Routing table ID to name mapping reference - -# id vrf name comment -{% for vrf in vrf_add -%} -{{ "%-10s" | format(vrf.table) }} {{ "%-16s" | format(vrf.name) }} # {{ vrf.description }} -{% endfor -%} - -""" - default_config_data = { - 'bind_to_all': 0, + 'bind_to_all': '0', 'deleted': False, 'vrf_add': [], 'vrf_existing': [], @@ -103,7 +92,7 @@ def get_config(): # Should services be allowed to bind to all VRFs? if conf.exists(['bind-to-all']): - vrf_config['bind_to_all'] = 1 + vrf_config['bind_to_all'] = '1' # Determine vrf interfaces (currently effective) - to determine which # vrf interface is no longer present and needs to be removed @@ -193,7 +182,12 @@ def verify(vrf_config): return None def generate(vrf_config): - tmpl = jinja2.Template(config_tmpl) + # 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) @@ -210,12 +204,15 @@ def apply(vrf_config): # set the default VRF global behaviour bind_all = vrf_config['bind_to_all'] - _cmd(f'sysctl -wq net.ipv4.tcp_l3mdev_accept={bind_all}') - _cmd(f'sysctl -wq net.ipv4.udp_l3mdev_accept={bind_all}') + if read_file('/proc/sys/net/ipv4/tcp_l3mdev_accept') != bind_all: + _cmd(f'sysctl -wq net.ipv4.tcp_l3mdev_accept={bind_all}') + _cmd(f'sysctl -wq net.ipv4.udp_l3mdev_accept={bind_all}') for vrf in vrf_config['vrf_remove']: name = vrf['name'] if os.path.isdir(f'/sys/class/net/{name}'): + _cmd(f'sudo ip -4 route del vrf {name} unreachable default metric 4278198272') + _cmd(f'sudo ip -6 route del vrf {name} unreachable default metric 4278198272') _cmd(f'ip link delete dev {name}') for vrf in vrf_config['vrf_add']: diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py index b17f1ce82..8683faca7 100755 --- a/src/conf_mode/vrrp.py +++ b/src/conf_mode/vrrp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,135 +13,26 @@ # # 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 sys import subprocess -import ipaddress -import json -import jinja2 + +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 pathlib import Path daemon_file = "/etc/default/keepalived" config_file = "/etc/keepalived/keepalived.conf" config_dict_path = "/run/keepalived_config.dict" -config_tmpl = """ -# Autogenerated by VyOS -# Do not edit this file, all your changes will be lost -# on next commit or reboot - -global_defs { - dynamic_interfaces - script_user root - notify_fifo /run/keepalived_notify_fifo - notify_fifo_script /usr/libexec/vyos/system/keepalived-fifo.py -} - -{% for group in groups -%} - -{% if group.health_check_script -%} -vrrp_script healthcheck_{{ group.name }} { - script "{{ group.health_check_script }}" - interval {{ group.health_check_interval }} - fall {{ group.health_check_count }} - rise 1 - -} -{% endif %} - -vrrp_instance {{ group.name }} { - {% if group.description -%} - # {{ group.description }} - {% endif -%} - - state BACKUP - interface {{ group.interface }} - virtual_router_id {{ group.vrid }} - priority {{ group.priority }} - advert_int {{ group.advertise_interval }} - - {% if group.preempt -%} - preempt_delay {{ group.preempt_delay }} - {% else -%} - nopreempt - {% endif -%} - - {% if group.peer_address -%} - unicast_peer { {{ group.peer_address }} } - {% endif -%} - - {% if group.hello_source -%} - {%- if group.peer_address -%} - unicast_src_ip {{ group.hello_source }} - {%- else -%} - mcast_src_ip {{ group.hello_source }} - {%- endif %} - {% endif -%} - - {% if group.use_vmac and group.peer_address -%} - use_vmac {{group.interface}}v{{group.vrid}} - vmac_xmit_base - {% elif group.use_vmac -%} - use_vmac {{group.interface}}v{{group.vrid}} - {% endif -%} - - {% if group.auth_password -%} - authentication { - auth_pass "{{ group.auth_password }}" - auth_type {{ group.auth_type }} - } - {% endif -%} - - virtual_ipaddress { - {% for addr in group.virtual_addresses -%} - {{ addr }} - {% endfor -%} - } - - {% if group.health_check_script -%} - track_script { - healthcheck_{{ group.name }} - } - {% endif -%} -} - -{% endfor -%} - -{% for sync_group in sync_groups -%} -vrrp_sync_group {{ sync_group.name }} { - group { - {% for member in sync_group.members -%} - {{ member }} - {% endfor -%} - } - - {% if sync_group.conntrack_sync -%} - notify_master "/opt/vyatta/sbin/vyatta-vrrp-conntracksync.sh master {{ sync_group.name }}" - notify_backup "/opt/vyatta/sbin/vyatta-vrrp-conntracksync.sh backup {{ sync_group.name }}" - notify_fault "/opt/vyatta/sbin/vyatta-vrrp-conntracksync.sh fault {{ sync_group.name }}" - {% endif -%} -} - -{% endfor -%} - -""" - -daemon_tmpl = """ -# Autogenerated by VyOS -# Options to pass to keepalived - -# DAEMON_ARGS are appended to the keepalived command-line -DAEMON_ARGS="--snmp" -""" - - def get_config(): vrrp_groups = [] sync_groups = [] @@ -209,7 +100,7 @@ def get_config(): vrrp_groups.append(group) - config.set_level("") + config.set_level("") # Get the sync group used for conntrack-sync conntrack_sync_group = None @@ -227,11 +118,17 @@ def get_config(): if conntrack_sync_group == sync_group_name: sync_group["conntrack_sync"] = True + # add transition script configuration + sync_group["master_script"] = config.return_value("transition-script master") + sync_group["backup_script"] = config.return_value("transition-script backup") + sync_group["fault_script"] = config.return_value("transition-script fault") + sync_group["stop_script"] = config.return_value("transition-script stop") + sync_groups.append(sync_group) # create a file with dict with proposed configuration with open("{}.temp".format(config_dict_path), 'w') as dict_file: - dict_file.write(json.dumps({'vrrp_groups': vrrp_groups, 'sync_groups': sync_groups})) + dict_file.write(dumps({'vrrp_groups': vrrp_groups, 'sync_groups': sync_groups})) return (vrrp_groups, sync_groups) @@ -256,31 +153,31 @@ def verify(data): # XXX: filter on map object is destructive, so we force it to list. # Additionally, filter objects always evaluate to True, empty or not, # so we force them to lists as well. - vaddrs = list(map(lambda i: ipaddress.ip_interface(i), group["virtual_addresses"])) - vaddrs4 = list(filter(lambda x: isinstance(x, ipaddress.IPv4Interface), vaddrs)) - vaddrs6 = list(filter(lambda x: isinstance(x, ipaddress.IPv6Interface), vaddrs)) + vaddrs = list(map(lambda i: ip_interface(i), group["virtual_addresses"])) + vaddrs4 = list(filter(lambda x: isinstance(x, IPv4Interface), vaddrs)) + vaddrs6 = list(filter(lambda x: isinstance(x, IPv6Interface), vaddrs)) if vaddrs4 and vaddrs6: raise ConfigError("VRRP group {0} mixes IPv4 and IPv6 virtual addresses, this is not allowed. Create separate groups for IPv4 and IPv6".format(group["name"])) if vaddrs4: if group["hello_source"]: - hsa = ipaddress.ip_address(group["hello_source"]) - if isinstance(hsa, ipaddress.IPv6Address): + hsa = ip_address(group["hello_source"]) + if isinstance(hsa, IPv6Address): raise ConfigError("VRRP group {0} uses IPv4 but its hello-source-address is IPv6".format(group["name"])) if group["peer_address"]: - pa = ipaddress.ip_address(group["peer_address"]) - if isinstance(pa, ipaddress.IPv6Address): + pa = ip_address(group["peer_address"]) + if isinstance(pa, IPv6Address): raise ConfigError("VRRP group {0} uses IPv4 but its peer-address is IPv6".format(group["name"])) if vaddrs6: if group["hello_source"]: - hsa = ipaddress.ip_address(group["hello_source"]) - if isinstance(hsa, ipaddress.IPv4Address): + hsa = ip_address(group["hello_source"]) + if isinstance(hsa, IPv4Address): raise ConfigError("VRRP group {0} uses IPv6 but its hello-source-address is IPv4".format(group["name"])) if group["peer_address"]: - pa = ipaddress.ip_address(group["peer_address"]) - if isinstance(pa, ipaddress.IPv4Address): + pa = ip_address(group["peer_address"]) + if isinstance(pa, IPv4Address): raise ConfigError("VRRP group {0} uses IPv6 but its peer-address is IPv4".format(group["name"])) # Disallow same VRID on multiple interfaces @@ -304,6 +201,11 @@ 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 @@ -315,13 +217,15 @@ def generate(data): # Filter out disabled groups vrrp_groups = list(filter(lambda x: x["disable"] is not True, vrrp_groups)) - tmpl = jinja2.Template(config_tmpl) + 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(daemon_tmpl) + f.write(config_text) return None @@ -362,4 +266,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print("VRRP error: {0}".format(str(e))) - sys.exit(1) + exit(1) diff --git a/src/etc/ppp/ip-down.d/0020-wirelessmodem b/src/etc/ppp/ip-down.d/0020-wirelessmodem deleted file mode 100755 index c93c7cabe..000000000 --- a/src/etc/ppp/ip-down.d/0020-wirelessmodem +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh - -tty=$2 -ipparam=$6 - -# Only applicable for Wireless Modems (WWAN) -if [ -z "$(echo $tty | egrep "tty(USB|ACM)")" ]; then - exit 0 -fi - -# device name and metric are received using ipparam -device=`echo "$ipparam"|awk '{ print $1 }'` -metric=`echo "$ipparam"|awk '{ print $2 }'` - -vtysh -c "conf t" -c "no ip route 0.0.0.0/0 ${device} ${metric}" - -DIALER_PID=$(cat /var/run/${device}.pid) -logger -t pppd[$DIALER_PID] "removed default route via $device metric $metric" diff --git a/src/etc/ppp/ip-up.d/0020-wirelessmodem b/src/etc/ppp/ip-up.d/0020-wirelessmodem deleted file mode 100755 index 95549387b..000000000 --- a/src/etc/ppp/ip-up.d/0020-wirelessmodem +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh - -tty=$2 -ipparam=$6 - -# Only applicable for Wireless Modems (WWAN) -if [ -z "$(echo $tty | egrep "tty(USB|ACM)")" ]; then - exit 0 -fi - -# device name and metric are received using ipparam -device=`echo "$ipparam"|awk '{ print $1 }'` -metric=`echo "$ipparam"|awk '{ print $2 }'` - -vtysh -c "conf t" -c "ip route 0.0.0.0/0 ${device} ${metric}" - -DIALER_PID=$(cat /var/run/${device}.pid) -logger -t pppd[$DIALER_PID] "added default route via $device metric $metric" diff --git a/src/migration-scripts/interfaces/6-to-7 b/src/migration-scripts/interfaces/6-to-7 index b4f59c363..220c7e601 100755 --- a/src/migration-scripts/interfaces/6-to-7 +++ b/src/migration-scripts/interfaces/6-to-7 @@ -35,7 +35,7 @@ if __name__ == '__main__': # Nothing to do sys.exit(0) - # list all individual interface types like dummy, ethernet and so on + # list all individual wwan/wireless modem interfaces for i in config.list_nodes(base): iface = base + [i] diff --git a/src/migration-scripts/interfaces/7-to-8 b/src/migration-scripts/interfaces/7-to-8 new file mode 100755 index 000000000..78bd2781b --- /dev/null +++ b/src/migration-scripts/interfaces/7-to-8 @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# Remove network provider name from CLI and rather use provider APN from CLI + +from sys import exit, argv +from vyos.configtree import ConfigTree + +if __name__ == '__main__': + if (len(argv) < 1): + print("Must specify file name!") + exit(1) + + file_name = argv[1] + with open(file_name, 'r') as f: + config_file = f.read() + + config = ConfigTree(config_file) + base = ['interfaces', 'wireguard'] + + if not config.exists(base): + # Nothing to do + exit(0) + + # list all individual wireguard interface isntance + for i in config.list_nodes(base): + iface = base + [i] + for peer in config.list_nodes(iface + ['peer']): + base_peer = iface + ['peer', peer] + if config.exists(base_peer + ['endpoint']): + endpoint = config.return_value(base_peer + ['endpoint']) + address = endpoint.split(':')[0] + port = endpoint.split(':')[1] + # delete old node + config.delete(base_peer + ['endpoint']) + # setup new nodes + config.set(base_peer + ['address'], value=address) + config.set(base_peer + ['port'], value=port) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/op_mode/show_openvpn.py b/src/op_mode/show_openvpn.py index 06b90296f..32918ddce 100755 --- a/src/op_mode/show_openvpn.py +++ b/src/op_mode/show_openvpn.py @@ -15,6 +15,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # +import os import jinja2 import argparse @@ -63,6 +64,9 @@ def get_status(mode, interface): 'clients': [], } + if not os.path.exists(status_file): + return data + with open(status_file, 'r') as f: lines = f.readlines() for line_no, line in enumerate(lines): diff --git a/src/op_mode/wireguard.py b/src/op_mode/wireguard.py index 512c80dda..c684f8a47 100755 --- a/src/op_mode/wireguard.py +++ b/src/op_mode/wireguard.py @@ -150,7 +150,7 @@ if __name__ == '__main__': if args.listkdir: list_key_dirs() if args.showinterface: - intf = WireGuardIf(args.showinterface, debug=False) + intf = WireGuardIf(args.showinterface, create=False, debug=False) intf.op_show_interface() if args.delkdir: if args.location: |