summaryrefslogtreecommitdiff
path: root/src/conf_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-xsrc/conf_mode/accel_l2tp.py840
-rwxr-xr-xsrc/conf_mode/accel_sstp.py469
-rwxr-xr-xsrc/conf_mode/arp.py7
-rwxr-xr-xsrc/conf_mode/bcast_relay.py64
-rwxr-xr-xsrc/conf_mode/dhcp_relay.py71
-rwxr-xr-xsrc/conf_mode/dhcp_server.py290
-rwxr-xr-xsrc/conf_mode/dhcpv6_relay.py37
-rwxr-xr-xsrc/conf_mode/dhcpv6_server.py128
-rwxr-xr-xsrc/conf_mode/dns_forwarding.py138
-rwxr-xr-xsrc/conf_mode/dynamic_dns.py84
-rwxr-xr-xsrc/conf_mode/firewall_options.py28
-rwxr-xr-xsrc/conf_mode/flow_accounting_conf.py155
-rwxr-xr-xsrc/conf_mode/host_name.py21
-rwxr-xr-xsrc/conf_mode/http-api.py13
-rwxr-xr-xsrc/conf_mode/https.py144
-rwxr-xr-xsrc/conf_mode/igmp_proxy.py98
-rwxr-xr-xsrc/conf_mode/intel_qat.py24
-rwxr-xr-xsrc/conf_mode/interfaces-bonding.py100
-rwxr-xr-xsrc/conf_mode/interfaces-bridge.py63
-rwxr-xr-xsrc/conf_mode/interfaces-dummy.py38
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py81
-rwxr-xr-xsrc/conf_mode/interfaces-geneve.py25
-rwxr-xr-xsrc/conf_mode/interfaces-l2tpv3.py100
-rwxr-xr-xsrc/conf_mode/interfaces-loopback.py8
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py417
-rwxr-xr-xsrc/conf_mode/interfaces-pppoe.py211
-rwxr-xr-xsrc/conf_mode/interfaces-pseudo-ethernet.py128
-rwxr-xr-xsrc/conf_mode/interfaces-tunnel.py646
-rwxr-xr-xsrc/conf_mode/interfaces-vxlan.py81
-rwxr-xr-xsrc/conf_mode/interfaces-wireguard.py433
-rwxr-xr-xsrc/conf_mode/interfaces-wireless.py1038
-rwxr-xr-xsrc/conf_mode/interfaces-wirelessmodem.py237
-rwxr-xr-xsrc/conf_mode/ipsec-settings.py157
-rwxr-xr-xsrc/conf_mode/le_cert.py29
-rwxr-xr-xsrc/conf_mode/lldp.py102
-rwxr-xr-xsrc/conf_mode/mdns_repeater.py50
-rwxr-xr-xsrc/conf_mode/ntp.py78
-rwxr-xr-xsrc/conf_mode/protocols_bfd.py62
-rwxr-xr-xsrc/conf_mode/protocols_igmp.py121
-rwxr-xr-xsrc/conf_mode/protocols_mpls.py178
-rwxr-xr-xsrc/conf_mode/protocols_pim.py148
-rwxr-xr-xsrc/conf_mode/salt-minion.py144
-rwxr-xr-xsrc/conf_mode/service-ipoe.py218
-rwxr-xr-xsrc/conf_mode/service-pppoe.py313
-rwxr-xr-xsrc/conf_mode/service-router-advert.py178
-rwxr-xr-xsrc/conf_mode/snmp.py225
-rwxr-xr-xsrc/conf_mode/ssh.py155
-rwxr-xr-xsrc/conf_mode/system-ip.py4
-rwxr-xr-xsrc/conf_mode/system-ipv6.py4
-rwxr-xr-xsrc/conf_mode/system-login-banner.py28
-rwxr-xr-xsrc/conf_mode/system-login.py102
-rwxr-xr-xsrc/conf_mode/system-options.py5
-rwxr-xr-xsrc/conf_mode/system-syslog.py96
-rwxr-xr-xsrc/conf_mode/system-timezone.py6
-rwxr-xr-xsrc/conf_mode/system-wifi-regdom.py41
-rwxr-xr-xsrc/conf_mode/tftp_server.py66
-rwxr-xr-xsrc/conf_mode/vpn-pptp.py168
-rwxr-xr-xsrc/conf_mode/vpn_sstp.py402
-rwxr-xr-xsrc/conf_mode/vrf.py276
-rwxr-xr-xsrc/conf_mode/vrrp.py203
-rwxr-xr-xsrc/conf_mode/vyos_cert.py30
61 files changed, 4688 insertions, 5118 deletions
diff --git a/src/conf_mode/accel_l2tp.py b/src/conf_mode/accel_l2tp.py
index a7af9cc68..4ca5a858a 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,20 +13,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 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
+from vyos.util import run
+
pidfile = r'/var/run/accel_l2tp.pid'
l2tp_cnf_dir = r'/etc/accel-ppp/l2tp'
@@ -34,526 +35,363 @@ 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')
-
-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
+ cnt = 0
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ while True:
+ try:
+ s.connect(("127.0.0.1", 2004))
+ break
+ except ConnectionRefusedError:
+ time.sleep(0.5)
+ cnt += 1
+ if cnt == 100:
+ raise("failed to start l2tp server")
+ break
+
+
+def _accel_cmd(command):
+ return run(f'/usr/bin/accel-cmd -p 2004 {command}')
###
# inline helper functions end
###
+
def get_config():
- c = Config()
- if not c.exists('vpn l2tp remote-access '):
- return None
-
- c.set_level('vpn l2tp remote-access')
- config_data = {
- 'authentication' : {
- 'mode' : 'local',
- 'local-users' : {
+ 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 = run(f'/usr/sbin/accel-pppd -c {l2tp_conf} -p {pidfile} -d')
+ chk_con()
+ if ret != 0 and os.path.exists(pidfile):
+ os.remove(pidfile)
+ raise ConfigError('accel-pppd failed to start')
+ else:
+ # if gw ip changes, only restart doesn't work
+ _accel_cmd('restart')
+
if __name__ == '__main__':
- try:
- c = get_config()
- verify(c)
- generate(c)
- apply(c)
- except ConfigError as e:
- print(e)
- sys.exit(1)
+ 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/accel_sstp.py b/src/conf_mode/accel_sstp.py
deleted file mode 100755
index 1317a32db..000000000
--- a/src/conf_mode/accel_sstp.py
+++ /dev/null
@@ -1,469 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-#
-#
-
-import sys
-import os
-import re
-import subprocess
-import jinja2
-import socket
-import time
-import syslog as sl
-
-from vyos.config import Config
-from vyos import ConfigError
-
-pidfile = r'/var/run/accel_sstp.pid'
-sstp_cnf_dir = r'/etc/accel-ppp/sstp'
-chap_secrets = sstp_cnf_dir + '/chap-secrets'
-sstp_conf = sstp_cnf_dir + '/sstp.config'
-ssl_cert_dir = r'/config/user-data/sstp'
-
-### config path creation
-if not os.path.exists(sstp_cnf_dir):
- os.makedirs(sstp_cnf_dir)
- sl.syslog(sl.LOG_NOTICE, sstp_cnf_dir + " created")
-
-if not os.path.exists(ssl_cert_dir):
- os.makedirs(ssl_cert_dir)
- sl.syslog(sl.LOG_NOTICE, ssl_cert_dir + " created")
-
-sstp_config = '''
-### generated by accel_sstp.py ###
-[modules]
-log_syslog
-sstp
-ippool
-shaper
-{% if authentication['mode'] == 'local' %}
-chap-secrets
-{% endif -%}
-{% for proto in authentication['auth_proto'] %}
-{{proto}}
-{% endfor %}
-{% if authentication['mode'] == 'radius' %}
-radius
-{% endif %}
-
-[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
-{% if certs %}
-ssl-ca-file=/config/user-data/sstp/{{certs['ca']}}
-ssl-pemfile=/config/user-data/sstp/{{certs['server-cert']}}
-ssl-keyfile=/config/user-data/sstp/{{certs['server-key']}}
-{% endif %}
-
-{%if ip_pool %}
-[ip-pool]
-gw-ip-address={{gw}}
-{% for sn in ip_pool %}
-{{sn}}
-{% endfor %}
-{% endif %}
-
-{% if dnsv4 %}
-[dns]
-{% if dnsv4['primary'] %}
-dns1={{dnsv4['primary']}}
-{% endif -%}
-{% if dnsv4['secondary'] %}
-dns2={{dnsv4['secondary']}}
-{% endif -%}
-{% endif %}
-
-{% if authentication['mode'] == 'local' %}
-[chap-secrets]
-chap-secrets=/etc/accel-ppp/sstp/chap-secrets
-{% endif %}
-
-{%- if authentication['mode'] == 'radius' %}
-[radius]
-verbose=1
-{% for rsrv in authentication['radius-srv']: %}
-server={{rsrv}},{{authentication['radius-srv'][rsrv]['secret']}},\
-req-limit={{authentication['radius-srv'][rsrv]['req-limit']}},\
-fail-time={{authentication['radius-srv'][rsrv]['fail-time']}}
-{% endfor -%}
-{% if authentication['radiusopt']['acct-timeout'] %}
-acct-timeout={{authentication['radiusopt']['acct-timeout']}}
-{% endif -%}
-{% if authentication['radiusopt']['timeout'] %}
-timeout={{authentication['radiusopt']['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 -%}
-{% endif %}
-
-[ppp]
-verbose=1
-check-ip=1
-{% if mtu %}
-mtu={{mtu}}
-{% endif -%}
-{% if ppp['mppe'] %}
-mppe={{ppp['mppe']}}
-{% endif -%}
-{% if ppp['lcp-echo-interval'] %}
-lcp-echo-interval={{ppp['lcp-echo-interval']}}
-{% endif -%}
-{% if ppp['lcp-echo-failure'] %}
-lcp-echo-failure={{ppp['lcp-echo-failure']}}
-{% endif -%}
-{% if ppp['lcp-echo-timeout'] %}
-lcp-echo-timeout={{ppp['lcp-echo-timeout']}}
-{% 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:2005
-'''
-
-### sstp 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 %}
-'''
-###
-# inline helper functions
-###
-# depending on hw and threads, daemon needs a little to start
-# if it takes longer than 100 * 0.5 secs, exception is being raised
-# not sure if that's the best way to check it, but it worked so far quite well
-###
-def chk_con():
- cnt = 0
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- while True:
- try:
- s.connect(("127.0.0.1", 2005))
- s.close()
- break
- except ConnectionRefusedError:
- time.sleep(0.5)
- cnt +=1
- if cnt == 100:
- raise("failed to start sstp 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')
-
-def accel_cmd(cmd=''):
- if not cmd:
- return None
- try:
- ret = subprocess.check_output(['/usr/bin/accel-cmd','-p','2005',cmd]).decode().strip()
- return ret
- except:
- return 1
-
-#### check ig local-ip is in client pool subnet
-
-
-###
-# inline helper functions end
-###
-
-def get_config():
- c = Config()
- if not c.exists('service sstp-server'):
- return None
-
- c.set_level('service sstp-server')
-
- config_data = {
- 'authentication' : {
- 'local-users' : {
- },
- 'mode' : 'local',
- 'auth_proto' : [],
- 'radius-srv' : {},
- 'radiusopt' : {},
- 'dae-srv' : {}
- },
- 'certs' : {
- 'ca' : None,
- 'server-key' : None,
- 'server-cert' : None
- },
- 'ip_pool' : [],
- 'gw' : None,
- 'dnsv4' : {},
- 'mtu' : None,
- 'ppp' : {},
- }
-
- ### local auth
- if c.exists('authentication mode local'):
- if c.exists('authentication local-users'):
- for usr in c.list_nodes('authentication local-users username'):
- config_data['authentication']['local-users'].update(
- {
- usr : {
- 'passwd' : None,
- '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')
-
- if c.exists('authentication protocols'):
- auth_mods = {'pap' : 'pap','chap' : 'auth_chap_md5', 'mschap' : 'auth_mschap_v1', 'mschap-v2' : 'auth_mschap_v2'}
- for proto in c.return_values('authentication protocols'):
- config_data['authentication']['auth_proto'].append(auth_mods[proto])
- else:
- config_data['authentication']['auth_proto'] = ['auth_mschap_v2']
-
- #### RADIUS auth and settings
- if c.exists('authentication mode radius'):
- config_data['authentication']['mode'] = c.return_value('authentication mode')
- if c.exists('authentication radius-server'):
- for rsrv in c.list_nodes('authentication radius-server'):
- config_data['authentication']['radius-srv'][rsrv] = {}
- if c.exists('authentication radius-server ' + rsrv + ' secret'):
- config_data['authentication']['radius-srv'][rsrv]['secret'] = c.return_value('authentication radius-server ' + rsrv + ' secret')
- else:
- config_data['authentication']['radius-srv'][rsrv]['secret'] = None
- if c.exists('authentication radius-server ' + rsrv + ' fail-time'):
- config_data['authentication']['radius-srv'][rsrv]['fail-time'] = c.return_value('authentication radius-server ' + rsrv + ' fail-time')
- else:
- config_data['authentication']['radius-srv'][rsrv]['fail-time'] = 0
- if c.exists('authentication radius-server ' + rsrv + ' req-limit'):
- config_data['authentication']['radius-srv'][rsrv]['req-limit'] = c.return_value('authentication radius-server ' + rsrv + ' req-limit')
- else:
- config_data['authentication']['radius-srv'][rsrv]['req-limit'] = 0
-
- #### advanced radius-setting
- if c.exists('authentication radius-settings'):
- if c.exists('authentication radius-settings acct-timeout'):
- config_data['authentication']['radiusopt']['acct-timeout'] = c.return_value('authentication radius-settings acct-timeout')
- if c.exists('authentication radius-settings max-try'):
- config_data['authentication']['radiusopt']['max-try'] = c.return_value('authentication radius-settings max-try')
- if c.exists('authentication radius-settings timeout'):
- config_data['authentication']['radiusopt']['timeout'] = c.return_value('authentication radius-settings timeout')
- if c.exists('authentication radius-settings nas-identifier'):
- config_data['authentication']['radiusopt']['nas-id'] = c.return_value('authentication radius-settings nas-identifier')
- if c.exists('authentication radius-settings nas-ip-address'):
- config_data['authentication']['radiusopt']['nas-ip'] = c.return_value('authentication radius-settings nas-ip-address')
- if c.exists('authentication radius-settings dae-server'):
- config_data['authentication']['radiusopt'].update(
- {
- 'dae-srv' : {
- 'ip-addr' : c.return_value('authentication radius-settings dae-server ip-address'),
- 'port' : c.return_value('authentication radius-settings dae-server port'),
- 'secret' : str(c.return_value('authentication radius-settings dae-server secret'))
- }
- }
- )
- if c.exists('authentication radius-settings rate-limit enable'):
- if not c.exists('authentication radius-settings rate-limit attribute'):
- config_data['authentication']['radiusopt']['shaper'] = { 'attr' : 'Filter-Id' }
- else:
- config_data['authentication']['radiusopt']['shaper'] = {
- 'attr' : c.return_value('authentication radius-settings rate-limit attribute')
- }
- if c.exists('authentication radius-settings rate-limit vendor'):
- config_data['authentication']['radiusopt']['shaper']['vendor'] = c.return_value('authentication radius-settings rate-limit vendor')
-
- if c.exists('sstp-settings ssl-certs ca'):
- config_data['certs']['ca'] = c.return_value('sstp-settings ssl-certs ca')
- if c.exists('sstp-settings ssl-certs server-cert'):
- config_data['certs']['server-cert'] = c.return_value('sstp-settings ssl-certs server-cert')
- if c.exists('sstp-settings ssl-certs server-key'):
- config_data['certs']['server-key'] = c.return_value('sstp-settings ssl-certs server-key')
-
- if c.exists('network-settings client-ip-settings subnet'):
- config_data['ip_pool'] = c.return_values('network-settings client-ip-settings subnet')
- if c.exists('network-settings client-ip-settings gateway-address'):
- config_data['gw'] = c.return_value('network-settings client-ip-settings gateway-address')
- if c.exists('network-settings dns-server primary-dns'):
- config_data['dnsv4']['primary'] = c.return_value('network-settings dns-server primary-dns')
- if c.exists('network-settings dns-server secondary-dns'):
- config_data['dnsv4']['secondary'] = c.return_value('network-settings dns-server secondary-dns')
- if c.exists('network-settings mtu'):
- config_data['mtu'] = c.return_value('network-settings mtu')
-
- #### ppp
- if c.exists('ppp-settings mppe'):
- config_data['ppp']['mppe'] = c.return_value('ppp-settings mppe')
- if c.exists('ppp-settings lcp-echo-failure'):
- config_data['ppp']['lcp-echo-failure'] = c.return_value('ppp-settings lcp-echo-failure')
- if c.exists('ppp-settings lcp-echo-interval'):
- config_data['ppp']['lcp-echo-interval'] = c.return_value('ppp-settings lcp-echo-interval')
- if c.exists('ppp-settings lcp-echo-timeout'):
- config_data['ppp']['lcp-echo-timeout'] = c.return_value('ppp-settings lcp-echo-timeout')
-
- return config_data
-
-def verify(c):
- if c == None:
- return None
- ### vertify auth settings
- if c['authentication']['mode'] == 'local':
- if not c['authentication']['local-users']:
- raise ConfigError('sstp-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 up/download is set, check that both have a value
- if c['authentication']['local-users'][usr]['upload']:
- if not c['authentication']['local-users'][usr]['download']:
- raise ConfigError('user ' + usr + ' requires download speed value')
- if c['authentication']['local-users'][usr]['download']:
- if not c['authentication']['local-users'][usr]['upload']:
- raise ConfigError('user ' + usr + ' requires upload speed value')
-
- if not c['certs']['ca'] or not c['certs']['server-key'] or not c['certs']['server-cert']:
- raise ConfigError('service sstp-server sstp-settings ssl-certs needs the ssl certificates set up')
- else:
- ssl_path = ssl_cert_dir + '/'
- if not os.path.exists(ssl_path + c['certs']['ca']):
- raise ConfigError('CA {0} doesn\'t exist'.format(ssl_path + c['certs']['ca']))
- if not os.path.exists(ssl_path + c['certs']['server-cert']):
- raise ConfigError('SSL Cert {0} doesn\'t exist'.format(ssl_path + c['certs']['server-cert']))
- if not os.path.exists(ssl_path + c['certs']['server-cert']):
- raise ConfigError('SSL Key {0} doesn\'t exist'.format(ssl_path + c['certs']['server-key']))
-
- if c['authentication']['mode'] == 'radius':
- if len(c['authentication']['radius-srv']) == 0:
- raise ConfigError('service sstp-server authentication radius-server needs a value')
- for rsrv in c['authentication']['radius-srv']:
- if c['authentication']['radius-srv'][rsrv]['secret'] == None:
- raise ConfigError('service sstp-server authentication radius-server {0} secret requires a value'.format(rsrv))
-
- if c['authentication']['mode'] == 'local':
- if not c['ip_pool']:
- print ("WARNING: service sstp-server network-settings client-ip-settings subnet requires a value")
- if not c['gw']:
- print ("WARNING: service sstp-server network-settings client-ip-settings gateway-address requires a value")
-
-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)
-
- tmpl = jinja2.Template(sstp_config, trim_blocks=True)
- config_text = tmpl.render(c)
- open(sstp_conf,'w').write(config_text)
-
- if c['authentication']['local-users']:
- write_chap_secrets(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',sstp_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:
- accel_cmd('restart')
- sl.syslog(sl.LOG_NOTICE, "reloading config via daemon restart")
-
-if __name__ == '__main__':
- try:
- c = get_config()
- verify(c)
- generate(c)
- apply(c)
- except ConfigError as e:
- print(e)
- sys.exit(1)
diff --git a/src/conf_mode/arp.py b/src/conf_mode/arp.py
index aeca08432..fde7dc521 100755
--- a/src/conf_mode/arp.py
+++ b/src/conf_mode/arp.py
@@ -20,9 +20,9 @@ import sys
import os
import re
import syslog as sl
-import subprocess
from vyos.config import Config
+from vyos.util import call
from vyos import ConfigError
arp_cmd = '/usr/sbin/arp'
@@ -82,11 +82,12 @@ def generate(c):
def apply(c):
for ip_addr in c['remove']:
sl.syslog(sl.LOG_NOTICE, "arp -d " + ip_addr)
- subprocess.call([arp_cmd + ' -d ' + ip_addr + ' >/dev/null 2>&1'], shell=True)
+ call(f'{arp_cmd} -d {ip_addr} >/dev/null 2>&1')
for ip_addr in c['update']:
sl.syslog(sl.LOG_NOTICE, "arp -s " + ip_addr + " " + c['update'][ip_addr])
- subprocess.call([arp_cmd + ' -s ' + ip_addr + ' ' + c['update'][ip_addr] ], shell=True)
+ updated = c['update'][ip_addr]
+ call(f'{arp_cmd} -s {ip_addr} {updated}')
if __name__ == '__main__':
diff --git a/src/conf_mode/bcast_relay.py b/src/conf_mode/bcast_relay.py
index 8889e701c..8d4c4a89a 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,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 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
+from vyos.util import call
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 +51,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 +62,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 +112,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 +146,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)
@@ -157,7 +155,7 @@ def generate(relay):
def apply(relay):
# first stop all running services
- os.system('sudo systemctl stop udp-broadcast-relay@{1..99}')
+ call('sudo systemctl stop udp-broadcast-relay@{1..99}')
if (relay is None) or relay['disabled']:
return None
@@ -167,7 +165,7 @@ def apply(relay):
# Don't start individual instance when it's disabled
if r['disabled']:
continue
- os.system('sudo systemctl start udp-broadcast-relay@{0}'.format(r['id']))
+ call('sudo systemctl start udp-broadcast-relay@{0}'.format(r['id']))
return None
@@ -179,4 +177,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..c92d6a4e1 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,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 jinja2 import FileSystemLoader, Environment
+from sys import exit
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
from vyos import ConfigError
+from vyos.util import call
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 +37,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 +61,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 +74,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 +99,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)
@@ -128,10 +113,10 @@ def generate(relay):
def apply(relay):
if relay is not None:
- os.system('sudo systemctl restart isc-dhcp-relay.service')
+ call('sudo systemctl restart isc-dhcp-relay.service')
else:
# DHCP relay support is removed in the commit
- os.system('sudo systemctl stop isc-dhcp-relay.service')
+ call('sudo systemctl stop isc-dhcp-relay.service')
os.unlink(config_file)
return None
@@ -144,4 +129,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 bf86e484b..553247b88 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,232 +14,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 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
+from vyos.util import call
+
config_file = r'/etc/dhcp/dhcpd.conf'
lease_file = r'/config/dhcpd.leases'
pid_file = r'/var/run/dhcpd.pid'
daemon_config_file = r'/etc/default/isc-dhcpv4-server'
-# 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 }};
- 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,
@@ -315,6 +109,26 @@ def dhcp_slice_range(exclude_list, range_list):
return output
+def dhcp_static_route(static_subnet, static_router):
+ # https://ercpe.de/blog/pushing-static-routes-with-isc-dhcp-server
+ # Option format is:
+ # <netmask>, <network-byte1>, <network-byte2>, <network-byte3>, <router-byte1>, <router-byte2>, <router-byte3>
+ # where bytes with the value 0 are omitted.
+ net = ip_network(static_subnet)
+ # add netmask
+ string = str(net.prefixlen) + ','
+ # add network bytes
+ if net.prefixlen:
+ width = net.prefixlen // 8
+ if net.prefixlen % 8:
+ width += 1
+ string += ','.join(map(str,tuple(net.network_address.packed)[:width])) + ','
+
+ # add router bytes
+ string += ','.join(static_router.split('.'))
+
+ return string
+
def get_config():
dhcp = default_config_data
conf = Config()
@@ -395,6 +209,7 @@ def get_config():
'bootfile_server': '',
'client_prefix_length': '',
'default_router': '',
+ 'rfc3442_default_router': '',
'dns_server': [],
'domain_name': '',
'domain_search': [],
@@ -438,11 +253,12 @@ 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'):
subnet['default_router'] = conf.return_value('default-router')
+ subnet['rfc3442_default_router'] = dhcp_static_route("0.0.0.0/0", subnet['default_router'])
# Specifies a list of Domain Name System (STD 13, RFC 1035) name servers available to
# the client. Servers should be listed in order of preference.
@@ -586,27 +402,7 @@ def get_config():
subnet['static_router'] = conf.return_value('static-route router')
if subnet['static_router'] and subnet['static_subnet']:
- # https://ercpe.de/blog/pushing-static-routes-with-isc-dhcp-server
- # Option format is:
- # <netmask>, <network-byte1>, <network-byte2>, <network-byte3>, <router-byte1>, <router-byte2>, <router-byte3>
- # where bytes with the value 0 are omitted.
- net = ip_network(subnet['static_subnet'])
- # add netmask
- string = str(net.prefixlen) + ','
- # add network bytes
- bytes = str(net.network_address).split('.')
- for b in bytes:
- if b != '0':
- string += b + ','
-
- # add router bytes
- bytes = subnet['static_router'].split('.')
- for b in bytes:
- string += b
- if b is not bytes[-1]:
- string += ','
-
- subnet['static_route'] = string
+ subnet['static_route'] = dhcp_static_route(subnet['static_subnet'], subnet['static_router'])
# HACKS AND TRICKS
#
@@ -776,7 +572,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
@@ -808,9 +604,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("&quot;",'"')
@@ -818,7 +618,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)
@@ -828,7 +628,7 @@ def generate(dhcp):
def apply(dhcp):
if (dhcp is None) or dhcp['disabled']:
# DHCP server is removed in the commit
- os.system('sudo systemctl stop isc-dhcpv4-server.service')
+ call('sudo systemctl stop isc-dhcpv4-server.service')
if os.path.exists(config_file):
os.unlink(config_file)
if os.path.exists(daemon_config_file):
@@ -838,7 +638,7 @@ def apply(dhcp):
if not os.path.exists(lease_file):
os.mknod(lease_file)
- os.system('sudo systemctl restart isc-dhcpv4-server.service')
+ call('sudo systemctl restart isc-dhcpv4-server.service')
return None
@@ -850,4 +650,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..9355d9794 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,26 +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 vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
from vyos import ConfigError
+from vyos.util import call
-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(' ') }}"
-"""
+config_file = r'/etc/default/isc-dhcpv6-relay'
default_config_data = {
'listen_addr': [],
@@ -41,7 +35,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 +86,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)
@@ -101,10 +100,10 @@ def generate(relay):
def apply(relay):
if relay is not None:
- os.system('sudo systemctl restart isc-dhcpv6-relay.service')
+ call('sudo systemctl restart isc-dhcpv6-relay.service')
else:
# DHCPv6 relay support is removed in the commit
- os.system('sudo systemctl stop isc-dhcpv6-relay.service')
+ call('sudo systemctl stop isc-dhcpv6-relay.service')
os.unlink(config_file)
return None
@@ -117,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/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py
index 44a927789..950ca1ce2 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,119 +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 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
+from vyos.util import call
+
config_file = r'/etc/dhcp/dhcpdv6.conf'
lease_file = r'/config/dhcpdv6.leases'
pid_file = r'/var/run/dhcpdv6.pid'
daemon_config_file = r'/etc/default/isc-dhcpv6-server'
-# 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 +41,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 +316,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 +344,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)
@@ -452,7 +364,7 @@ def generate(dhcpv6):
def apply(dhcpv6):
if (dhcpv6 is None) or dhcpv6['disabled']:
# DHCP server is removed in the commit
- os.system('sudo systemctl stop isc-dhcpv6-server.service')
+ call('sudo systemctl stop isc-dhcpv6-server.service')
if os.path.exists(config_file):
os.unlink(config_file)
if os.path.exists(daemon_config_file):
@@ -462,7 +374,7 @@ def apply(dhcpv6):
if not os.path.exists(lease_file):
os.mknod(lease_file)
- os.system('sudo systemctl restart isc-dhcpv6-server.service')
+ call('sudo systemctl restart isc-dhcpv6-server.service')
return None
@@ -474,4 +386,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..4071c05c9 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,22 +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 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
-
+from vyos.util import call
parser = argparse.ArgumentParser()
parser.add_argument("--dhclient", action="store_true",
@@ -36,53 +34,6 @@ parser.add_argument("--dhclient", action="store_true",
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 +47,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 +153,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)
@@ -211,11 +167,11 @@ def generate(dns):
def apply(dns):
if dns is None:
# DNS forwarding is removed in the commit
- os.system("systemctl stop pdns-recursor")
+ call("systemctl stop pdns-recursor")
if os.path.isfile(config_file):
os.unlink(config_file)
else:
- os.system("systemctl restart pdns-recursor")
+ call("systemctl restart pdns-recursor")
if __name__ == '__main__':
args = parser.parse_args()
@@ -223,7 +179,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 +188,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 4663987b4..b54d76b06 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,22 @@
# 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
+from vyos.util import call
+
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 +54,7 @@ default_config_data = {
}
def get_config():
- dyndns = default_config_data
+ dyndns = deepcopy(default_config_data)
conf = Config()
base_level = ['service', 'dns', 'dynamic']
@@ -198,8 +152,8 @@ def get_config():
node['service'].append(srv)
- # set config level back to top level
- conf.set_level(base_level)
+ # 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']):
@@ -208,6 +162,9 @@ def get_config():
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
@@ -269,6 +226,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)
@@ -277,7 +239,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)
@@ -295,11 +257,11 @@ def apply(dyndns):
os.unlink('/etc/ddclient.conf')
if dyndns['deleted']:
- os.system('/etc/init.d/ddclient stop')
+ call('/etc/init.d/ddclient stop')
if os.path.exists(dyndns['pid_file']):
os.unlink(dyndns['pid_file'])
else:
- os.system('/etc/init.d/ddclient restart')
+ call('/etc/init.d/ddclient restart')
return None
@@ -311,4 +273,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/firewall_options.py b/src/conf_mode/firewall_options.py
index 2be80cdbf..0b800f48f 100755
--- a/src/conf_mode/firewall_options.py
+++ b/src/conf_mode/firewall_options.py
@@ -21,6 +21,8 @@ import copy
from vyos.config import Config
from vyos import ConfigError
+from vyos.util import call
+
default_config_data = {
'intf_opts': [],
@@ -85,19 +87,19 @@ def apply(tcp):
target = 'VYOS_FW_OPTIONS'
# always cleanup iptables
- os.system('iptables --table mangle --delete FORWARD --jump {} >&/dev/null'.format(target))
- os.system('iptables --table mangle --flush {} >&/dev/null'.format(target))
- os.system('iptables --table mangle --delete-chain {} >&/dev/null'.format(target))
+ call('iptables --table mangle --delete FORWARD --jump {} >&/dev/null'.format(target))
+ call('iptables --table mangle --flush {} >&/dev/null'.format(target))
+ call('iptables --table mangle --delete-chain {} >&/dev/null'.format(target))
# always cleanup ip6tables
- os.system('ip6tables --table mangle --delete FORWARD --jump {} >&/dev/null'.format(target))
- os.system('ip6tables --table mangle --flush {} >&/dev/null'.format(target))
- os.system('ip6tables --table mangle --delete-chain {} >&/dev/null'.format(target))
+ call('ip6tables --table mangle --delete FORWARD --jump {} >&/dev/null'.format(target))
+ call('ip6tables --table mangle --flush {} >&/dev/null'.format(target))
+ call('ip6tables --table mangle --delete-chain {} >&/dev/null'.format(target))
# Setup new iptables rules
if tcp['new_chain4']:
- os.system('iptables --table mangle --new-chain {} >&/dev/null'.format(target))
- os.system('iptables --table mangle --append FORWARD --jump {} >&/dev/null'.format(target))
+ call('iptables --table mangle --new-chain {} >&/dev/null'.format(target))
+ call('iptables --table mangle --append FORWARD --jump {} >&/dev/null'.format(target))
for opts in tcp['intf_opts']:
intf = opts['intf']
@@ -109,13 +111,13 @@ def apply(tcp):
# adjust TCP MSS per interface
if mss:
- os.system('iptables --table mangle --append {} --out-interface {} --protocol tcp ' \
+ call('iptables --table mangle --append {} --out-interface {} --protocol tcp '
'--tcp-flags SYN,RST SYN --jump TCPMSS --set-mss {} >&/dev/null'.format(target, intf, mss))
# Setup new ip6tables rules
if tcp['new_chain6']:
- os.system('ip6tables --table mangle --new-chain {} >&/dev/null'.format(target))
- os.system('ip6tables --table mangle --append FORWARD --jump {} >&/dev/null'.format(target))
+ call('ip6tables --table mangle --new-chain {} >&/dev/null'.format(target))
+ call('ip6tables --table mangle --append FORWARD --jump {} >&/dev/null'.format(target))
for opts in tcp['intf_opts']:
intf = opts['intf']
@@ -127,8 +129,8 @@ def apply(tcp):
# adjust TCP MSS per interface
if mss:
- os.system('ip6tables --table mangle --append {} --out-interface {} --protocol tcp ' \
- '--tcp-flags SYN,RST SYN --jump TCPMSS --set-mss {} >&/dev/null'.format(target, intf, mss))
+ call('ip6tables --table mangle --append {} --out-interface {} --protocol tcp '
+ '--tcp-flags SYN,RST SYN --jump TCPMSS --set-mss {} >&/dev/null'.format(target, intf, mss))
return None
diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py
index 0bc50482c..1008f3fae 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,18 +13,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 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
-import vyos.interfaces
-from vyos.ifconfig import Interface
-from jinja2 import Template
+from vyos.util import cmd
+
# default values
default_sflow_server_port = 6343
@@ -37,78 +40,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):
@@ -129,7 +60,7 @@ def _sflow_default_agentip(config):
return config.return_value('protocols ospfv3 parameters router-id')
# if router-id was not found, use first available ip of any interface
- for iface in vyos.interfaces.list_interfaces():
+ for iface in Interface.listing():
for address in Interface(iface).get_addr():
# return an IP, if this is not loopback
regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$')
@@ -151,11 +82,7 @@ def _iptables_get_nflog():
for iptables_variant in ['iptables', 'ip6tables']:
# run iptables, save output and split it by lines
iptables_command = "sudo {0} -t {1} -S {2}".format(iptables_variant, iptables_nflog_table, iptables_nflog_chain)
- process = subprocess.Popen(iptables_command, stdout=subprocess.PIPE, shell=True, universal_newlines=True)
- 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)
+ cmd(iptables_command, universal_newlines=True, message='Failed to get flows list')
iptables_out = stdout.splitlines()
# parse each line and add information to list
@@ -180,14 +107,21 @@ 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:
- if rule['interface'] not in configured_ifaces:
- iptable_commands.append("sudo {0} -t {1} -D {2}".format(rule['iptables_variant'], rule['table'], rule['rule_definition']))
+ iptables = rule['iptables_variant']
+ interface = rule['interface']
+ if interface not in configured_ifaces:
+ table = rule['table']
+ rule = rule['rule_definition']
+ iptable_commands.append(f'sudo {iptables} -t {table} -D {rule}')
else:
- active_nflog_ifaces.append({ 'iface': rule['interface'], 'iptables_variant': rule['iptables_variant'] })
+ active_nflog_ifaces.append({
+ 'iface': interface,
+ 'iptables_variant': iptables,
+ })
# do not create new rules for already configured interfaces
for iface in active_nflog_ifaces:
@@ -196,14 +130,14 @@ def _iptables_config(configured_ifaces):
# create missed rules
for iface_extended in configured_ifaces_extended:
- rule_definition = "{0} -i {1} -m comment --comment FLOW_ACCOUNTING_RULE -j NFLOG --nflog-group 2 --nflog-size {2} --nflog-threshold 100".format(iptables_nflog_chain, iface_extended['iface'], default_captured_packet_size)
- iptable_commands.append("sudo {0} -t {1} -I {2}".format(iface_extended['iptables_variant'], iptables_nflog_table, rule_definition))
+ iface = iface_extended['iface']
+ iptables = iface_extended['iptables_variant']
+ rule_definition = f'{iptables_nflog_chain} -i {iface} -m comment --comment FLOW_ACCOUNTING_RULE -j NFLOG --nflog-group 2 --nflog-size {default_captured_packet_size} --nflog-threshold 100'
+ iptable_commands.append(f'sudo {iptables} -t {iptables_nflog_table} -I {rule_definition}')
# change iptables
for command in iptable_commands:
- return_code = subprocess.call(command.split(' '))
- if not return_code == 0:
- raise ConfigError("Failed to run command: {}\nExit code {}".format(command, return_code))
+ cmd(command, raising=ConfigError)
def get_config():
@@ -300,7 +234,7 @@ def verify(config):
# check that all configured interfaces exists in the system
for iface in config['interfaces']:
- if not iface in vyos.interfaces.list_interfaces():
+ if not iface in Interface.listing():
# chnged from error to warning to allow adding dynamic interfaces and interface templates
# raise ConfigError("The {} interface is not presented in the system".format(iface))
print("Warning: the {} interface is not presented in the system".format(iface))
@@ -315,20 +249,20 @@ 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
agent_id_presented = None
- for iface in vyos.interfaces.list_interfaces():
+ for iface in Interface.listing():
for address in Interface(iface).get_addr():
# check an IP, if this is not loopback
regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$')
@@ -348,7 +282,7 @@ def verify(config):
# check if configured netflow source-ip exist in the system
if config['netflow']['source-ip']:
source_ip_presented = None
- for iface in vyos.interfaces.list_interfaces():
+ for iface in Interface.listing():
for address in Interface(iface).get_addr():
# check an IP
regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$')
@@ -379,7 +313,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
@@ -400,13 +334,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):
@@ -419,9 +356,7 @@ def apply(config):
command = '/usr/bin/sudo /bin/systemctl restart uacctd'
# run command to start or stop flow-accounting
- return_code = subprocess.call(command.split(' '))
- if not return_code == 0:
- raise ConfigError("Failed to start/stop flow-accounting: command {} returned exit code {}".format(command, return_code))
+ cmd(command, raising=ConfigError, message='Failed to start/stop flow-accounting')
# configure iptables rules for defined interfaces
if config['interfaces']:
@@ -437,4 +372,4 @@ if __name__ == '__main__':
apply(config)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py
index 47cf232e9..7c2f79abc 100755
--- a/src/conf_mode/host_name.py
+++ b/src/conf_mode/host_name.py
@@ -25,7 +25,6 @@ import re
import sys
import copy
import glob
-import subprocess
import argparse
import jinja2
@@ -34,6 +33,9 @@ import vyos.hostsd_client
from vyos.config import Config
from vyos import ConfigError
+from vyos.util import cmd
+from vyos.util import call
+from vyos.util import run
default_config_data = {
@@ -156,21 +158,22 @@ def apply(config):
# rsyslog runs into a race condition at boot time with systemd
# restart rsyslog only if the hostname changed.
- hostname_old = subprocess.check_output(['hostnamectl', '--static']).decode().strip()
-
- os.system("hostnamectl set-hostname --static {0}".format(hostname_new))
+ hostname_old = cmd('hostnamectl --static')
+ call(f'hostnamectl set-hostname --static {hostname_new}')
# Restart services that use the hostname
if hostname_new != hostname_old:
- os.system("systemctl restart rsyslog.service")
+ call("systemctl restart rsyslog.service")
# If SNMP is running, restart it too
- if os.system("pgrep snmpd > /dev/null") == 0:
- os.system("systemctl restart snmpd.service")
+ ret = run("pgrep snmpd")
+ if ret == 0:
+ call("systemctl restart snmpd.service")
# restart pdns if it is used
- if os.system("/usr/bin/rec_control ping >/dev/null 2>&1") == 0:
- os.system("/etc/init.d/pdns-recursor restart >/dev/null")
+ ret = run('/usr/bin/rec_control ping')
+ if ret == 0:
+ call('/etc/init.d/pdns-recursor restart >/dev/null')
return None
diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py
index 0c2e029e9..26f4aea7f 100755
--- a/src/conf_mode/http-api.py
+++ b/src/conf_mode/http-api.py
@@ -18,13 +18,14 @@
import sys
import os
-import subprocess
import json
from copy import deepcopy
import vyos.defaults
from vyos.config import Config
from vyos import ConfigError
+from vyos.util import cmd
+from vyos.util import call
config_file = '/etc/vyos/http-api.conf'
@@ -91,16 +92,12 @@ def generate(http_api):
def apply(http_api):
if http_api is not None:
- os.system('sudo systemctl restart vyos-http-api.service')
+ call('sudo systemctl restart vyos-http-api.service')
else:
- os.system('sudo systemctl stop vyos-http-api.service')
+ call('sudo systemctl stop vyos-http-api.service')
for dep in dependencies:
- cmd = '{0}/{1}'.format(vyos_conf_scripts_dir, dep)
- try:
- subprocess.check_call(cmd, shell=True)
- except subprocess.CalledProcessError as err:
- raise ConfigError("{}.".format(err))
+ cmd(f'{vyos_conf_scripts_dir}/{dep}', raising=ConfigError)
if __name__ == '__main__':
try:
diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py
index 84d1a7691..da7193c9b 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,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 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
+from vyos.util import call
-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) {
-{% 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 %}
-"""
+config_file = '/etc/nginx/sites-available/default'
default_server_block = {
+ 'id' : '',
'address' : '*',
'port' : '443',
'name' : ['_'],
@@ -111,22 +49,23 @@ def get_config():
else:
conf.set_level('service https')
- if conf.exists('listen-address'):
- for addr in conf.list_nodes('listen-address'):
- server_block = {'address' : addr}
- server_block['port'] = '443'
- server_block['name'] = ['_']
- if conf.exists('listen-address {0} listen-port'.format(addr)):
- port = conf.return_value('listen-address {0} listen-port'.format(addr))
+ if not conf.exists('virtual-host'):
+ server_block_list.append(default_server_block)
+ else:
+ for vhost in conf.list_nodes('virtual-host'):
+ server_block = deepcopy(default_server_block)
+ server_block['id'] = vhost
+ if conf.exists(f'virtual-host {vhost} listen-address'):
+ addr = conf.return_value(f'virtual-host {vhost} listen-address')
+ server_block['address'] = addr
+ if conf.exists(f'virtual-host {vhost} listen-port'):
+ port = conf.return_value(f'virtual-host {vhost} listen-port')
server_block['port'] = port
- if conf.exists('listen-address {0} server-name'.format(addr)):
- names = conf.return_values('listen-address {0} server-name'.format(addr))
+ if conf.exists(f'virtual-host {vhost} server-name'):
+ names = conf.return_values(f'virtual-host {vhost} server-name')
server_block['name'] = names[:]
server_block_list.append(server_block)
- if not server_block_list:
- server_block_list.append(default_server_block)
-
vyos_cert_data = {}
if conf.exists('certificates system-generated-certificate'):
vyos_cert_data = vyos.defaults.vyos_cert_data
@@ -149,17 +88,33 @@ def get_config():
# certbot organizes certificates by first domain
sb['certbot_dir'] = certbot_domains[0]
+ api_somewhere = False
api_data = {}
if conf.exists('api'):
+ api_somewhere = True
api_data = vyos.defaults.api_data
if conf.exists('api port'):
port = conf.return_value('api port')
api_data['port'] = port
- if api_data:
- for block in server_block_list:
- block['api'] = api_data
+ if conf.exists('api-restrict virtual-host'):
+ vhosts = conf.return_values('api-restrict virtual-host')
+ api_data['vhost'] = vhosts[:]
- https = {'server_block_list' : server_block_list, 'certbot': certbot}
+ if api_data:
+ # we do not want to include 'vhost' key as part of
+ # vyos.defaults.api_data, so check for key existence
+ vhost_list = api_data.get('vhost')
+ if vhost_list is None:
+ for block in server_block_list:
+ block['api'] = api_data
+ else:
+ for block in server_block_list:
+ if block['id'] in vhost_list:
+ block['api'] = api_data
+
+ https = {'server_block_list' : server_block_list,
+ 'api_somewhere': api_somewhere,
+ 'certbot': certbot}
return https
def verify(https):
@@ -170,7 +125,7 @@ def verify(https):
for sb in https['server_block_list']:
if sb['certbot']:
return None
- raise ConfigError("At least one 'listen-address x.x.x.x server-name' "
+ raise ConfigError("At least one 'virtual-host <id> server-name' "
"matching the 'certbot domain-name' is required.")
return None
@@ -178,10 +133,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)
@@ -190,9 +150,9 @@ def generate(https):
def apply(https):
if https is not None:
- os.system('sudo systemctl restart nginx.service')
+ call('sudo systemctl restart nginx.service')
else:
- os.system('sudo systemctl stop nginx.service')
+ call('sudo systemctl stop nginx.service')
if __name__ == '__main__':
try:
@@ -202,4 +162,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..77e2bb150 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,59 +13,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 sys import exit
+from copy import deepcopy
+from jinja2 import FileSystemLoader, Environment
from netifaces import interfaces
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
from vyos import ConfigError
+from vyos.util import call
-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 %}
-"""
+config_file = r'/etc/igmpproxy.conf'
default_config_data = {
'disable': False,
@@ -74,23 +36,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 +62,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 +116,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)
@@ -163,11 +131,11 @@ def generate(igmp_proxy):
def apply(igmp_proxy):
if igmp_proxy is None or igmp_proxy['disable']:
# IGMP Proxy support is removed in the commit
- os.system('sudo systemctl stop igmpproxy.service')
+ call('sudo systemctl stop igmpproxy.service')
if os.path.exists(config_file):
os.unlink(config_file)
else:
- os.system('sudo systemctl restart igmpproxy.service')
+ call('systemctl restart igmpproxy.service')
return None
@@ -179,4 +147,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/intel_qat.py b/src/conf_mode/intel_qat.py
index a1abd5e81..cc7d4a915 100755
--- a/src/conf_mode/intel_qat.py
+++ b/src/conf_mode/intel_qat.py
@@ -19,10 +19,10 @@
import sys
import os
import re
-import subprocess
from vyos.config import Config
from vyos import ConfigError
+from vyos.util import popen, run
# Define for recovering
gl_ipsec_conf = None
@@ -49,13 +49,10 @@ def get_config():
# Control configured VPN service which can use QAT
def vpn_control(action):
+ # XXX: Should these commands report failure
if action == 'restore' and gl_ipsec_conf:
- ret = subprocess.Popen(['sudo', 'ipsec', 'start'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- (output, err) = ret.communicate()
- return
-
- ret = subprocess.Popen(['sudo', 'ipsec', action], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- (output, err) = ret.communicate()
+ return run('sudo ipsec start')
+ return run(f'sudo ipsec {action}')
def verify(c):
# Check if QAT service installed
@@ -66,10 +63,9 @@ def verify(c):
return
# Check if QAT device exist
- ret = subprocess.Popen(['sudo', 'lspci', '-nn'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- (output, err) = ret.communicate()
+ output, err = popen('sudo lspci -nn', decode='utf-8')
if not err:
- data = re.findall('(8086:19e2)|(8086:37c8)|(8086:0435)|(8086:6f54)', output.decode("utf-8"))
+ data = re.findall('(8086:19e2)|(8086:37c8)|(8086:0435)|(8086:6f54)', output)
#If QAT devices found
if not data:
print("\t No QAT acceleration device found")
@@ -82,17 +78,13 @@ def apply(c):
# Disable QAT service
if c['qat_conf'] == None:
- ret = subprocess.Popen(['sudo', '/etc/init.d/vyos-qat-utilities', 'stop'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- (output, err) = ret.communicate()
+ run('sudo /etc/init.d/vyos-qat-utilities stop')
if c['ipsec_conf']:
vpn_control('start')
-
return
# Run qat init.d script
- ret = subprocess.Popen(['sudo', '/etc/init.d/vyos-qat-utilities', 'start'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- (output, err) = ret.communicate()
-
+ run('sudo /etc/init.d/vyos-qat-utilities start')
if c['ipsec_conf']:
# Recovery VPN service
vpn_control('start')
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
index 6cdfb764c..32aa2826b 100755
--- a/src/conf_mode/interfaces-bonding.py
+++ b/src/conf_mode/interfaces-bonding.py
@@ -21,9 +21,11 @@ from sys import exit
from netifaces import interfaces
from vyos.ifconfig import BondIf
-from vyos.ifconfig_vlan import apply_vlan_config
+from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
from vyos.configdict import list_diff, vlan_to_dict
from vyos.config import Config
+from vyos.util import is_bridge_member
+from vyos.util import call
from vyos import ConfigError
default_config_data = {
@@ -48,6 +50,10 @@ default_config_data = {
'ip_enable_arp_ignore': 0,
'ip_proxy_arp': 0,
'ip_proxy_arp_pvlan': 0,
+ 'ipv6_autoconf': 0,
+ 'ipv6_eui64_prefix': '',
+ 'ipv6_forwarding': 1,
+ 'ipv6_dup_addr_detect': 1,
'intf': '',
'mac': '',
'mode': '802.3ad',
@@ -58,7 +64,8 @@ default_config_data = {
'vif_s': [],
'vif_s_remove': [],
'vif': [],
- 'vif_remove': []
+ 'vif_remove': [],
+ 'vrf': ''
}
@@ -85,7 +92,7 @@ def get_config():
if not os.path.isfile('/sys/class/net/bonding_masters'):
import syslog
syslog.syslog(syslog.LOG_NOTICE, "loading bonding kernel module")
- if os.system('modprobe bonding max_bonds=0 miimon=250') != 0:
+ if call('modprobe bonding max_bonds=0 miimon=250') != 0:
syslog.syslog(syslog.LOG_NOTICE, "failed loading bonding kernel module")
raise ConfigError("failed loading bonding kernel module")
@@ -93,10 +100,10 @@ def get_config():
conf = Config()
# determine tagNode instance
- try:
- bond['intf'] = os.environ['VYOS_TAGNODE_VALUE']
- except KeyError as E:
- print("Interface not specified")
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ bond['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# check if bond has been removed
cfg_base = 'interfaces bonding ' + bond['intf']
@@ -188,6 +195,22 @@ def get_config():
if conf.exists('ip proxy-arp-pvlan'):
bond['ip_proxy_arp_pvlan'] = 1
+ # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
+ if conf.exists('ipv6 address autoconf'):
+ bond['ipv6_autoconf'] = 1
+
+ # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ if conf.exists('ipv6 address eui64'):
+ bond['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+
+ # Disable IPv6 forwarding on this interface
+ if conf.exists('ipv6 disable-forwarding'):
+ bond['ipv6_forwarding'] = 0
+
+ # IPv6 Duplicate Address Detection (DAD) tries
+ if conf.exists('ipv6 dup-addr-detect-transmits'):
+ bond['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+
# Media Access Control (MAC) address
if conf.exists('mac'):
bond['mac'] = conf.return_value('mac')
@@ -221,8 +244,10 @@ def get_config():
if conf.exists('primary'):
bond['primary'] = conf.return_value('primary')
- # re-set configuration level to parse new nodes
- conf.set_level(cfg_base)
+ # retrieve VRF instance
+ if conf.exists('vrf'):
+ bond['vrf'] = conf.return_value('vrf')
+
# get vif-s interfaces (currently effective) - to determine which vif-s
# interface is no longer present and needs to be removed
eff_intf = conf.list_effective_nodes('vif-s')
@@ -253,38 +278,30 @@ def get_config():
def verify(bond):
+ if bond['deleted']:
+ interface = bond['intf']
+ is_member, bridge = is_bridge_member(interface)
+ if is_member:
+ # can not use a f'' formatted-string here as bridge would not get
+ # expanded in the print statement
+ raise ConfigError('Can not delete interface "{0}" as it ' \
+ 'is a member of bridge "{1}"!'.format(interface, bridge))
+ return None
+
if len (bond['arp_mon_tgt']) > 16:
raise ConfigError('The maximum number of targets that can be specified is 16')
if bond['primary']:
if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:
raise ConfigError('Mode dependency failed, primary not supported ' \
- 'in this mode.'.format())
-
- if bond['primary'] not in bond['member']:
- raise ConfigError('Interface "{}" is not part of the bond' \
- .format(bond['primary']))
-
-
- # DHCPv6 parameters-only and temporary address are mutually exclusive
- for vif_s in bond['vif_s']:
- if vif_s['dhcpv6_prm_only'] and vif_s['dhcpv6_temporary']:
- raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
+ 'in mode "{}"!'.format(bond['mode']))
- for vif_c in vif_s['vif_c']:
- if vif_c['dhcpv6_prm_only'] and vif_c['dhcpv6_temporary']:
- raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
-
- for vif in bond['vif']:
- if vif['dhcpv6_prm_only'] and vif['dhcpv6_temporary']:
- raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
-
-
- for vif_s in bond['vif_s']:
- for vif in bond['vif']:
- if vif['id'] == vif_s['id']:
- raise ConfigError('Can not use identical ID on vif and vif-s interface')
+ vrf_name = bond['vrf']
+ if vrf_name and vrf_name not in interfaces():
+ raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ # use common function to verify VLAN configuration
+ verify_vlan_config(bond)
conf = Config()
for intf in bond['member']:
@@ -427,6 +444,14 @@ def apply(bond):
b.set_proxy_arp(bond['ip_proxy_arp'])
# Enable private VLAN proxy ARP on this interface
b.set_proxy_arp_pvlan(bond['ip_proxy_arp_pvlan'])
+ # IPv6 address autoconfiguration
+ b.set_ipv6_autoconf(bond['ipv6_autoconf'])
+ # IPv6 EUI-based address
+ b.set_ipv6_eui64_address(bond['ipv6_eui64_prefix'])
+ # IPv6 forwarding
+ b.set_ipv6_forwarding(bond['ipv6_forwarding'])
+ # IPv6 Duplicate Address Detection (DAD) tries
+ b.set_ipv6_dad_messages(bond['ipv6_dup_addr_detect'])
# Change interface MAC address
if bond['mac']:
@@ -442,7 +467,7 @@ def apply(bond):
# Some parameters can not be changed when the bond is up.
if bond['shutdown_required']:
# Disable bond prior changing of certain properties
- b.set_state('down')
+ b.set_admin_state('down')
# The bonding mode can not be changed when there are interfaces enslaved
# to this bond, thus we will free all interfaces from the bond first!
@@ -460,9 +485,9 @@ def apply(bond):
# parameters we will only re-enable the interface if it is not
# administratively disabled
if not bond['disable']:
- b.set_state('up')
+ b.set_admin_state('up')
else:
- b.set_state('down')
+ b.set_admin_state('down')
# Configure interface address(es)
# - not longer required addresses get removed first
@@ -472,6 +497,9 @@ def apply(bond):
for addr in bond['address']:
b.add_addr(addr)
+ # assign/remove VRF
+ b.set_vrf(bond['vrf'])
+
# remove no longer required service VLAN interfaces (vif-s)
for vif_s in bond['vif_s_remove']:
b.del_vlan(vif_s)
diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py
index a3213f309..79247ee51 100755
--- a/src/conf_mode/interfaces-bridge.py
+++ b/src/conf_mode/interfaces-bridge.py
@@ -20,7 +20,8 @@ from copy import deepcopy
from sys import exit
from netifaces import interfaces
-from vyos.ifconfig import BridgeIf, STPIf
+from vyos.ifconfig import BridgeIf
+from vyos.ifconfig.stp import STP
from vyos.configdict import list_diff
from vyos.config import Config
from vyos import ConfigError
@@ -45,6 +46,10 @@ default_config_data = {
'ip_enable_arp_accept': 0,
'ip_enable_arp_announce': 0,
'ip_enable_arp_ignore': 0,
+ 'ipv6_autoconf': 0,
+ 'ipv6_eui64_prefix': '',
+ 'ipv6_forwarding': 1,
+ 'ipv6_dup_addr_detect': 1,
'igmp_querier': 0,
'intf': '',
'mac' : '',
@@ -52,7 +57,8 @@ default_config_data = {
'member': [],
'member_remove': [],
'priority': 32768,
- 'stp': 0
+ 'stp': 0,
+ 'vrf': ''
}
def get_config():
@@ -60,10 +66,10 @@ def get_config():
conf = Config()
# determine tagNode instance
- try:
- bridge['intf'] = os.environ['VYOS_TAGNODE_VALUE']
- except KeyError as E:
- print("Interface not specified")
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ bridge['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# Check if bridge has been removed
if not conf.exists('interfaces bridge ' + bridge['intf']):
@@ -150,6 +156,22 @@ def get_config():
if conf.exists('ip enable-arp-ignore'):
bridge['ip_enable_arp_ignore'] = 1
+ # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
+ if conf.exists('ipv6 address autoconf'):
+ bridge['ipv6_autoconf'] = 1
+
+ # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ if conf.exists('ipv6 address eui64'):
+ bridge['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+
+ # Disable IPv6 forwarding on this interface
+ if conf.exists('ipv6 disable-forwarding'):
+ bridge['ipv6_forwarding'] = 0
+
+ # IPv6 Duplicate Address Detection (DAD) tries
+ if conf.exists('ipv6 dup-addr-detect-transmits'):
+ bridge['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+
# Media Access Control (MAC) address
if conf.exists('mac'):
bridge['mac'] = conf.return_value('mac')
@@ -191,12 +213,20 @@ def get_config():
if conf.exists('stp'):
bridge['stp'] = 1
+ # retrieve VRF instance
+ if conf.exists('vrf'):
+ bridge['vrf'] = conf.return_value('vrf')
+
return bridge
def verify(bridge):
if bridge['dhcpv6_prm_only'] and bridge['dhcpv6_temporary']:
raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
+ vrf_name = bridge['vrf']
+ if vrf_name and vrf_name not in interfaces():
+ raise ConfigError(f'VRF "{vrf_name}" does not exist')
+
conf = Config()
for br in conf.list_nodes('interfaces bridge'):
# it makes no sense to verify ourself in this case
@@ -213,6 +243,9 @@ def verify(bridge):
if intf['name'] not in interfaces():
raise ConfigError('Can not add non existing interface "{}" to bridge "{}"'.format(intf['name'], bridge['intf']))
+ if intf['name'] == 'lo':
+ raise ConfigError('Loopback interface "lo" can not be added to a bridge')
+
# bridge members are not allowed to be bond members, too
for intf in bridge['member']:
for bond in conf.list_nodes('interfaces bonding'):
@@ -233,7 +266,7 @@ def apply(bridge):
br.remove()
else:
# enable interface
- br.set_state('up')
+ br.set_admin_state('up')
# set ageing time
br.set_ageing_time(bridge['aging'])
# set bridge forward delay
@@ -248,6 +281,14 @@ def apply(bridge):
br.set_arp_announce(bridge['ip_enable_arp_announce'])
# configure ARP ignore
br.set_arp_ignore(bridge['ip_enable_arp_ignore'])
+ # IPv6 address autoconfiguration
+ br.set_ipv6_autoconf(bridge['ipv6_autoconf'])
+ # IPv6 EUI-based address
+ br.set_ipv6_eui64_address(bridge['ipv6_eui64_prefix'])
+ # IPv6 forwarding
+ br.set_ipv6_forwarding(bridge['ipv6_forwarding'])
+ # IPv6 Duplicate Address Detection (DAD) tries
+ br.set_ipv6_dad_messages(bridge['ipv6_dup_addr_detect'])
# set max message age
br.set_max_age(bridge['max_age'])
# set bridge priority
@@ -286,6 +327,9 @@ def apply(bridge):
# store DHCPv6 config dictionary - used later on when addresses are aquired
br.set_dhcpv6_options(opt)
+ # assign/remove VRF
+ br.set_vrf(bridge['vrf'])
+
# Change interface MAC address
if bridge['mac']:
br.set_mac(bridge['mac'])
@@ -300,7 +344,7 @@ def apply(bridge):
# up/down interface
if bridge['disable']:
- br.set_state('down')
+ br.set_admin_state('down')
# Configure interface address(es)
# - not longer required addresses get removed first
@@ -310,9 +354,10 @@ def apply(bridge):
for addr in bridge['address']:
br.add_addr(addr)
+ STPBridgeIf = STP.enable(BridgeIf)
# configure additional bridge member options
for member in bridge['member']:
- i = STPIf(member['name'])
+ i = STPBridgeIf(member['name'])
# configure ARP cache timeout
i.set_arp_cache_tmo(bridge['arp_cache_tmo'])
# ignore link state changes
diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py
index eb0145f65..a256103af 100755
--- a/src/conf_mode/interfaces-dummy.py
+++ b/src/conf_mode/interfaces-dummy.py
@@ -18,10 +18,12 @@ import os
from copy import deepcopy
from sys import exit
+from netifaces import interfaces
from vyos.ifconfig import DummyIf
from vyos.configdict import list_diff
from vyos.config import Config
+from vyos.util import is_bridge_member
from vyos import ConfigError
default_config_data = {
@@ -30,7 +32,8 @@ default_config_data = {
'deleted': False,
'description': '',
'disable': False,
- 'intf': ''
+ 'intf': '',
+ 'vrf': ''
}
def get_config():
@@ -38,10 +41,10 @@ def get_config():
conf = Config()
# determine tagNode instance
- try:
- dummy['intf'] = os.environ['VYOS_TAGNODE_VALUE']
- except KeyError as E:
- print("Interface not specified")
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ dummy['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# Check if interface has been removed
if not conf.exists('interfaces dummy ' + dummy['intf']):
@@ -69,9 +72,27 @@ def get_config():
act_addr = conf.return_values('address')
dummy['address_remove'] = list_diff(eff_addr, act_addr)
+ # retrieve VRF instance
+ if conf.exists('vrf'):
+ dummy['vrf'] = conf.return_value('vrf')
+
return dummy
def verify(dummy):
+ if dummy['deleted']:
+ interface = dummy['intf']
+ is_member, bridge = is_bridge_member(interface)
+ if is_member:
+ # can not use a f'' formatted-string here as bridge would not get
+ # expanded in the print statement
+ raise ConfigError('Can not delete interface "{0}" as it ' \
+ 'is a member of bridge "{1}"!'.format(interface, bridge))
+ return None
+
+ vrf_name = dummy['vrf']
+ if vrf_name and vrf_name not in interfaces():
+ raise ConfigError(f'VRF "{vrf_name}" does not exist')
+
return None
def generate(dummy):
@@ -95,11 +116,14 @@ def apply(dummy):
for addr in dummy['address']:
d.add_addr(addr)
+ # assign/remove VRF
+ d.set_vrf(dummy['vrf'])
+
# disable interface on demand
if dummy['disable']:
- d.set_state('down')
+ d.set_admin_state('down')
else:
- d.set_state('up')
+ d.set_admin_state('up')
return None
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index e4f6e5ff2..15e9b4185 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -16,11 +16,12 @@
import os
-from copy import deepcopy
from sys import exit
+from copy import deepcopy
+from netifaces import interfaces
from vyos.ifconfig import EthernetIf
-from vyos.ifconfig_vlan import apply_vlan_config
+from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
from vyos.configdict import list_diff, vlan_to_dict
from vyos.config import Config
from vyos import ConfigError
@@ -47,6 +48,10 @@ default_config_data = {
'ip_enable_arp_ignore': 0,
'ip_proxy_arp': 0,
'ip_proxy_arp_pvlan': 0,
+ 'ipv6_autoconf': 0,
+ 'ipv6_eui64_prefix': '',
+ 'ipv6_forwarding': 1,
+ 'ipv6_dup_addr_detect': 1,
'intf': '',
'mac': '',
'mtu': 1500,
@@ -59,7 +64,8 @@ default_config_data = {
'vif_s': [],
'vif_s_remove': [],
'vif': [],
- 'vif_remove': []
+ 'vif_remove': [],
+ 'vrf': ''
}
def get_config():
@@ -67,10 +73,10 @@ def get_config():
conf = Config()
# determine tagNode instance
- try:
- eth['intf'] = os.environ['VYOS_TAGNODE_VALUE']
- except KeyError as E:
- print("Interface not specified")
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ eth['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# check if ethernet interface has been removed
cfg_base = ['interfaces', 'ethernet', eth['intf']]
@@ -165,6 +171,22 @@ def get_config():
if conf.exists('ip proxy-arp-pvlan'):
eth['ip_proxy_arp_pvlan'] = 1
+ # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
+ if conf.exists('ipv6 address autoconf'):
+ eth['ipv6_autoconf'] = 1
+
+ # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ if conf.exists('ipv6 address eui64'):
+ eth['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+
+ # Disable IPv6 forwarding on this interface
+ if conf.exists('ipv6 disable-forwarding'):
+ eth['ipv6_forwarding'] = 0
+
+ # IPv6 Duplicate Address Detection (DAD) tries
+ if conf.exists('ipv6 dup-addr-detect-transmits'):
+ eth['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+
# Media Access Control (MAC) address
if conf.exists('mac'):
eth['mac'] = conf.return_value('mac')
@@ -197,6 +219,10 @@ def get_config():
if conf.exists('speed'):
eth['speed'] = conf.return_value('speed')
+ # retrieve VRF instance
+ if conf.exists('vrf'):
+ eth['vrf'] = conf.return_value('vrf')
+
# re-set configuration level to parse new nodes
conf.set_level(cfg_base)
# get vif-s interfaces (currently effective) - to determine which vif-s
@@ -232,6 +258,9 @@ def verify(eth):
if eth['deleted']:
return None
+ if eth['intf'] not in interfaces():
+ raise ConfigError(f"Interface ethernet {eth['intf']} does not exist")
+
if eth['speed'] == 'auto':
if eth['duplex'] != 'auto':
raise ConfigError('If speed is hardcoded, duplex must be hardcoded, too')
@@ -243,6 +272,10 @@ def verify(eth):
if eth['dhcpv6_prm_only'] and eth['dhcpv6_temporary']:
raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
+ vrf_name = eth['vrf']
+ if vrf_name and vrf_name not in interfaces():
+ raise ConfigError(f'VRF "{vrf_name}" does not exist')
+
conf = Config()
# some options can not be changed when interface is enslaved to a bond
for bond in conf.list_nodes('interfaces bonding'):
@@ -250,21 +283,10 @@ def verify(eth):
bond_member = conf.return_values('interfaces bonding ' + bond + ' member interface')
if eth['intf'] in bond_member:
if eth['address']:
- raise ConfigError('Can not assign address to interface {} which is a member of {}').format(eth['intf'], bond)
-
- # DHCPv6 parameters-only and temporary address are mutually exclusive
- for vif_s in eth['vif_s']:
- if vif_s['dhcpv6_prm_only'] and vif_s['dhcpv6_temporary']:
- raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
-
- for vif_c in vif_s['vif_c']:
- if vif_c['dhcpv6_prm_only'] and vif_c['dhcpv6_temporary']:
- raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
-
- for vif in eth['vif']:
- if vif['dhcpv6_prm_only'] and vif['dhcpv6_temporary']:
- raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
+ raise ConfigError(f"Can not assign address to interface {eth['intf']} which is a member of {bond}")
+ # use common function to verify VLAN configuration
+ verify_vlan_config(eth)
return None
def generate(eth):
@@ -324,12 +346,20 @@ def apply(eth):
e.set_proxy_arp(eth['ip_proxy_arp'])
# Enable private VLAN proxy ARP on this interface
e.set_proxy_arp_pvlan(eth['ip_proxy_arp_pvlan'])
+ # IPv6 address autoconfiguration
+ e.set_ipv6_autoconf(eth['ipv6_autoconf'])
+ # IPv6 EUI-based address
+ e.set_ipv6_eui64_address(eth['ipv6_eui64_prefix'])
+ # IPv6 forwarding
+ e.set_ipv6_forwarding(eth['ipv6_forwarding'])
+ # IPv6 Duplicate Address Detection (DAD) tries
+ e.set_ipv6_dad_messages(eth['ipv6_dup_addr_detect'])
# Change interface MAC address - re-set to real hardware address (hw-id)
# if custom mac is removed
if eth['mac']:
e.set_mac(eth['mac'])
- else:
+ elif eth['hw_id']:
e.set_mac(eth['hw_id'])
# Maximum Transmission Unit (MTU)
@@ -355,9 +385,9 @@ def apply(eth):
# Enable/Disable interface
if eth['disable']:
- e.set_state('down')
+ e.set_admin_state('down')
else:
- e.set_state('up')
+ e.set_admin_state('up')
# Configure interface address(es)
# - not longer required addresses get removed first
@@ -367,6 +397,9 @@ def apply(eth):
for addr in eth['address']:
e.add_addr(addr)
+ # assign/remove VRF
+ e.set_vrf(eth['vrf'])
+
# remove no longer required service VLAN interfaces (vif-s)
for vif_s in eth['vif_s_remove']:
e.del_vlan(vif_s)
diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py
index b0c381656..e47473d76 100755
--- a/src/conf_mode/interfaces-geneve.py
+++ b/src/conf_mode/interfaces-geneve.py
@@ -18,11 +18,12 @@ import os
from sys import exit
from copy import deepcopy
+from netifaces import interfaces
from vyos.config import Config
-from vyos.ifconfig import GeneveIf, Interface
+from vyos.ifconfig import GeneveIf
+from vyos.util import is_bridge_member
from vyos import ConfigError
-from netifaces import interfaces
default_config_data = {
'address': [],
@@ -42,10 +43,10 @@ def get_config():
conf = Config()
# determine tagNode instance
- try:
- geneve['intf'] = os.environ['VYOS_TAGNODE_VALUE']
- except KeyError as E:
- print("Interface not specified")
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ geneve['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# Check if interface has been removed
if not conf.exists('interfaces geneve ' + geneve['intf']):
@@ -92,7 +93,13 @@ def get_config():
def verify(geneve):
if geneve['deleted']:
- # bail out early
+ interface = geneve['intf']
+ is_member, bridge = is_bridge_member(interface)
+ if is_member:
+ # can not use a f'' formatted-string here as bridge would not get
+ # expanded in the print statement
+ raise ConfigError('Can not delete interface "{0}" as it ' \
+ 'is a member of bridge "{1}"!'.format(interface, bridge))
return None
if not geneve['remote']:
@@ -127,7 +134,7 @@ def apply(geneve):
conf['remote'] = geneve['remote']
# Finally create the new interface
- g = GeneveIf(geneve['intf'], config=conf)
+ g = GeneveIf(geneve['intf'], **conf)
# update interface description used e.g. by SNMP
g.set_alias(geneve['description'])
# Maximum Transfer Unit (MTU)
@@ -148,7 +155,7 @@ def apply(geneve):
# parameters we will only re-enable the interface if it is not
# administratively disabled
if not geneve['disable']:
- g.set_state('up')
+ g.set_admin_state('up')
return None
diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py
index ae49dadad..11ba9acdd 100755
--- a/src/conf_mode/interfaces-l2tpv3.py
+++ b/src/conf_mode/interfaces-l2tpv3.py
@@ -22,6 +22,8 @@ from copy import deepcopy
from vyos.config import Config
from vyos.ifconfig import L2TPv3If, Interface
from vyos import ConfigError
+from vyos.util import call
+from vyos.util import is_bridge_member
from netifaces import interfaces
default_config_data = {
@@ -33,6 +35,10 @@ default_config_data = {
'local_address': '',
'local_port': 5000,
'intf': '',
+ 'ipv6_autoconf': 0,
+ 'ipv6_eui64_prefix': '',
+ 'ipv6_forwarding': 1,
+ 'ipv6_dup_addr_detect': 1,
'mtu': 1488,
'peer_session_id': '',
'peer_tunnel_id': '',
@@ -42,15 +48,22 @@ default_config_data = {
'tunnel_id': ''
}
+def check_kmod():
+ modules = ['l2tp_eth', 'l2tp_netlink', 'l2tp_ip', 'l2tp_ip6']
+ for module in modules:
+ if not os.path.exists(f'/sys/module/{module}'):
+ if call(f'modprobe {module}') != 0:
+ raise ConfigError(f'Loading Kernel module {module} failed')
+
def get_config():
l2tpv3 = deepcopy(default_config_data)
conf = Config()
# determine tagNode instance
- try:
- l2tpv3['intf'] = os.environ['VYOS_TAGNODE_VALUE']
- except KeyError as E:
- print("Interface not specified")
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ l2tpv3['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# Check if interface has been removed
if not conf.exists('interfaces l2tpv3 ' + l2tpv3['intf']):
@@ -94,6 +107,22 @@ def get_config():
if conf.exists('local-ip'):
l2tpv3['local_address'] = conf.return_value('local-ip')
+ # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
+ if conf.exists('ipv6 address autoconf'):
+ l2tpv3['ipv6_autoconf'] = 1
+
+ # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ if conf.exists('ipv6 address eui64'):
+ l2tpv3['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+
+ # Disable IPv6 forwarding on this interface
+ if conf.exists('ipv6 disable-forwarding'):
+ l2tpv3['ipv6_forwarding'] = 0
+
+ # IPv6 Duplicate Address Detection (DAD) tries
+ if conf.exists('ipv6 dup-addr-detect-transmits'):
+ l2tpv3['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+
# Maximum Transmission Unit (MTU)
if conf.exists('mtu'):
l2tpv3['mtu'] = int(conf.return_value('mtu'))
@@ -126,61 +155,41 @@ def get_config():
def verify(l2tpv3):
+ interface = l2tpv3['intf']
+
if l2tpv3['deleted']:
- # bail out early
+ is_member, bridge = is_bridge_member(interface)
+ if is_member:
+ # can not use a f'' formatted-string here as bridge would not get
+ # expanded in the print statement
+ raise ConfigError('Can not delete interface "{0}" as it ' \
+ 'is a member of bridge "{1}"!'.format(interface, bridge))
return None
if not l2tpv3['local_address']:
- raise ConfigError('Must configure the l2tpv3 local-ip for {}'.format(l2tpv3['intf']))
+ raise ConfigError(f'Must configure the l2tpv3 local-ip for {interface}')
if not l2tpv3['remote_address']:
- raise ConfigError('Must configure the l2tpv3 remote-ip for {}'.format(l2tpv3['intf']))
+ raise ConfigError(f'Must configure the l2tpv3 remote-ip for {interface}')
if not l2tpv3['tunnel_id']:
- raise ConfigError('Must configure the l2tpv3 tunnel-id for {}'.format(l2tpv3['intf']))
+ raise ConfigError(f'Must configure the l2tpv3 tunnel-id for {interface}')
if not l2tpv3['peer_tunnel_id']:
- raise ConfigError('Must configure the l2tpv3 peer-tunnel-id for {}'.format(l2tpv3['intf']))
+ raise ConfigError(f'Must configure the l2tpv3 peer-tunnel-id for {interface}')
if not l2tpv3['session_id']:
- raise ConfigError('Must configure the l2tpv3 session-id for {}'.format(l2tpv3['intf']))
+ raise ConfigError(f'Must configure the l2tpv3 session-id for {interface}')
if not l2tpv3['peer_session_id']:
- raise ConfigError('Must configure the l2tpv3 peer-session-id for {}'.format(l2tpv3['intf']))
+ raise ConfigError(f'Must configure the l2tpv3 peer-session-id for {interface}')
return None
def generate(l2tpv3):
- if l2tpv3['deleted']:
- # bail out early
- return None
-
- # initialize kernel module if not loaded
- if not os.path.isdir('/sys/module/l2tp_eth'):
- if os.system('modprobe l2tp_eth') != 0:
- raise ConfigError("failed loading l2tp_eth kernel module")
-
- if not os.path.isdir('/sys/module/l2tp_netlink'):
- if os.system('modprobe l2tp_netlink') != 0:
- raise ConfigError("failed loading l2tp_netlink kernel module")
-
- if not os.path.isdir('/sys/module/l2tp_ip'):
- if os.system('modprobe l2tp_ip') != 0:
- raise ConfigError("failed loading l2tp_ip kernel module")
-
- if l2tpv3['encapsulation'] == 'ip':
- if not os.path.isdir('/sys/module/l2tp_ip'):
- if os.system('modprobe l2tp_ip') != 0:
- raise ConfigError("failed loading l2tp_ip kernel module")
-
- if not os.path.isdir('/sys/module/l2tp_ip6 '):
- if os.system('modprobe l2tp_ip6 ') != 0:
- raise ConfigError("failed loading l2tp_ip6 kernel module")
-
return None
-
def apply(l2tpv3):
# L2TPv3 interface needs to be created/deleted on-block, instead of
# passing a ton of arguments, I just use a dict that is managed by
@@ -193,7 +202,7 @@ def apply(l2tpv3):
# always delete it first.
conf['session_id'] = l2tpv3['session_id']
conf['tunnel_id'] = l2tpv3['tunnel_id']
- l = L2TPv3If(l2tpv3['intf'], config=conf)
+ l = L2TPv3If(l2tpv3['intf'], **conf)
l.remove()
if not l2tpv3['deleted']:
@@ -208,11 +217,19 @@ def apply(l2tpv3):
conf['peer_session_id'] = l2tpv3['peer_session_id']
# Finally create the new interface
- l = L2TPv3If(l2tpv3['intf'], config=conf)
+ l = L2TPv3If(l2tpv3['intf'], **conf)
# update interface description used e.g. by SNMP
l.set_alias(l2tpv3['description'])
# Maximum Transfer Unit (MTU)
l.set_mtu(l2tpv3['mtu'])
+ # IPv6 address autoconfiguration
+ l.set_ipv6_autoconf(l2tpv3['ipv6_autoconf'])
+ # IPv6 EUI-based address
+ l.set_ipv6_eui64_address(l2tpv3['ipv6_eui64_prefix'])
+ # IPv6 forwarding
+ l.set_ipv6_forwarding(l2tpv3['ipv6_forwarding'])
+ # IPv6 Duplicate Address Detection (DAD) tries
+ l.set_ipv6_dad_messages(l2tpv3['ipv6_dup_addr_detect'])
# Configure interface address(es) - no need to implicitly delete the
# old addresses as they have already been removed by deleting the
@@ -224,12 +241,13 @@ def apply(l2tpv3):
# we will only re-enable the interface if it is not administratively
# disabled
if not l2tpv3['disable']:
- l.set_state('up')
+ l.set_admin_state('up')
return None
if __name__ == '__main__':
try:
+ check_kmod()
c = get_config()
verify(c)
generate(c)
diff --git a/src/conf_mode/interfaces-loopback.py b/src/conf_mode/interfaces-loopback.py
index 10722d137..ddd18ae24 100755
--- a/src/conf_mode/interfaces-loopback.py
+++ b/src/conf_mode/interfaces-loopback.py
@@ -37,10 +37,10 @@ def get_config():
conf = Config()
# determine tagNode instance
- try:
- loopback['intf'] = os.environ['VYOS_TAGNODE_VALUE']
- except KeyError as E:
- print("Interface not specified")
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ loopback['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# Check if interface has been removed
if not conf.exists('interfaces loopback ' + loopback['intf']):
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 6bd269e97..f34e4f7fe 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,262 +17,27 @@
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
from grp import getgrnam
from ipaddress import ip_address,ip_network,IPv4Interface
from netifaces import interfaces
-from psutil import pid_exists
from pwd import getpwnam
-from subprocess import Popen, PIPE
from time import sleep
+from shutil import rmtree
-from vyos import ConfigError
from vyos.config import Config
-from vyos.ifconfig import Interface
+from vyos.defaults import directories as vyos_data_dir
+from vyos.ifconfig import VTunIf
+from vyos.util import process_running, cmd, is_bridge_member
from vyos.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_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 ###
-
-ifconfig-push {{ ip }} {{ remote_netmask }}
-{% 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': '',
@@ -287,6 +52,10 @@ default_config_data = {
'encryption': '',
'hash': '',
'intf': '',
+ 'ipv6_autoconf': 0,
+ 'ipv6_eui64_prefix': '',
+ 'ipv6_forwarding': 1,
+ 'ipv6_dup_addr_detect': 1,
'ping_restart': '60',
'ping_interval': '10',
'local_address': '',
@@ -297,7 +66,7 @@ default_config_data = {
'ncp_ciphers': '',
'options': [],
'persistent_tunnel': False,
- 'protocol': '',
+ 'protocol': 'udp',
'redirect_gateway': '',
'remote_address': '',
'remote_host': [],
@@ -318,6 +87,7 @@ default_config_data = {
'tls_crl': '',
'tls_dh': '',
'tls_key': '',
+ 'tls_crypt': '',
'tls_role': '',
'tls_version_min': '',
'type': 'tun',
@@ -325,9 +95,6 @@ default_config_data = {
'gid': group,
}
-def subprocess_cmd(command):
- p = Popen(command, stdout=PIPE, shell=True)
- p.communicate()
def get_config_name(intf):
cfg_file = r'/opt/vyatta/etc/openvpn/openvpn-{}.conf'.format(intf)
@@ -360,7 +127,7 @@ def fixup_permission(filename, permission=S_IRUSR):
def checkCertHeader(header, filename):
"""
Verify if filename contains specified header.
- Returns True on success or on file not found to not trigger the exceptions
+ Returns True if match is found, False if no match or file is not found
"""
if not os.path.isfile(filename):
return False
@@ -370,17 +137,17 @@ def checkCertHeader(header, filename):
if re.match(header, line):
return True
- return True
+ return False
def get_config():
openvpn = deepcopy(default_config_data)
conf = Config()
# determine tagNode instance
- try:
- openvpn['intf'] = os.environ['VYOS_TAGNODE_VALUE']
- except KeyError as E:
- print("Interface not specified")
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ openvpn['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# Check if interface instance has been removed
if not conf.exists('interfaces openvpn ' + openvpn['intf']):
@@ -482,10 +249,25 @@ def get_config():
if conf.exists('local-port'):
openvpn['local_port'] = conf.return_value('local-port')
+ # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
+ if conf.exists('ipv6 address autoconf'):
+ openvpn['ipv6_autoconf'] = 1
+
+ # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ if conf.exists('ipv6 address eui64'):
+ openvpn['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+
+ # Disable IPv6 forwarding on this interface
+ if conf.exists('ipv6 disable-forwarding'):
+ openvpn['ipv6_forwarding'] = 0
+
+ # IPv6 Duplicate Address Detection (DAD) tries
+ if conf.exists('ipv6 dup-addr-detect-transmits'):
+ openvpn['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+
# 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'):
@@ -633,6 +415,11 @@ def get_config():
openvpn['tls_key'] = conf.return_value('tls key-file')
openvpn['tls'] = True
+ # File containing key to encrypt control channel packets
+ if conf.exists('tls crypt-file'):
+ openvpn['tls_crypt'] = conf.return_value('tls crypt-file')
+ openvpn['tls'] = True
+
# Role in TLS negotiation
if conf.exists('tls role'):
openvpn['tls_role'] = conf.return_value('tls role')
@@ -641,6 +428,7 @@ def get_config():
# Minimum required TLS version
if conf.exists('tls tls-version-min'):
openvpn['tls_version_min'] = conf.return_value('tls tls-version-min')
+ openvpn['tls'] = True
if conf.exists('shared-secret-key-file'):
openvpn['shared_secret_file'] = conf.return_value('shared-secret-key-file')
@@ -648,12 +436,25 @@ def get_config():
if conf.exists('use-lzo-compression'):
openvpn['compress_lzo'] = True
+ # Special case when using EC certificates:
+ # if key-file is EC and dh-file is unset, set tls_dh to 'none'
+ if not openvpn['tls_dh'] and openvpn['tls_key'] and checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls_key']):
+ openvpn['tls_dh'] = 'none'
+
return openvpn
def verify(openvpn):
if openvpn['deleted']:
+ interface = openvpn['intf']
+ is_member, bridge = is_bridge_member(interface)
+ if is_member:
+ # can not use a f'' formatted-string here as bridge would not get
+ # expanded in the print statement
+ raise ConfigError('Can not delete interface "{0}" as it ' \
+ 'is a member of bridge "{1}"!'.format(interface, bridge))
return None
+
if not openvpn['mode']:
raise ConfigError('Must specify OpenVPN operation mode')
@@ -682,7 +483,7 @@ def verify(openvpn):
if not openvpn['remote_host']:
raise ConfigError('Must specify "remote-host" in client mode')
- if openvpn['tls_dh']:
+ if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none':
raise ConfigError('Cannot specify "tls dh-file" in client mode')
#
@@ -732,8 +533,8 @@ def verify(openvpn):
if openvpn['protocol'] == 'tcp-passive' and len(openvpn['remote_host']) > 1:
raise ConfigError('Cannot specify more than 1 "remote-host" with "tcp-passive"')
- if not openvpn['tls_dh']:
- raise ConfigError('Must specify "tls dh-file" in server mode')
+ if not openvpn['tls_dh'] and not checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls_key']):
+ raise ConfigError('Must specify "tls dh-file" when not using EC keys in server mode')
if not openvpn['server_subnet']:
if not openvpn['bridge_member']:
@@ -800,6 +601,9 @@ def verify(openvpn):
if not openvpn['tls_key']:
raise ConfigError('Must specify "tls key-file"')
+ if openvpn['tls_auth'] and openvpn['tls_crypt']:
+ raise ConfigError('TLS auth and crypt are mutually exclusive')
+
if not checkCertHeader('-----BEGIN CERTIFICATE-----', openvpn['tls_ca_cert']):
raise ConfigError('Specified ca-cert-file "{}" is invalid'.format(openvpn['tls_ca_cert']))
@@ -812,14 +616,18 @@ def verify(openvpn):
raise ConfigError('Specified cert-file "{}" is invalid'.format(openvpn['tls_cert']))
if openvpn['tls_key']:
- if not checkCertHeader('-----BEGIN (?:RSA )?PRIVATE KEY-----', openvpn['tls_key']):
+ if not checkCertHeader('-----BEGIN (?:RSA |EC )?PRIVATE KEY-----', openvpn['tls_key']):
raise ConfigError('Specified key-file "{}" is not valid'.format(openvpn['tls_key']))
+ if openvpn['tls_crypt']:
+ if not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', openvpn['tls_crypt']):
+ raise ConfigError('Specified TLS crypt-file "{}" is invalid'.format(openvpn['tls_crypt']))
+
if openvpn['tls_crl']:
if not checkCertHeader('-----BEGIN X509 CRL-----', openvpn['tls_crl']):
raise ConfigError('Specified crl-file "{} not valid'.format(openvpn['tls_crl']))
- if openvpn['tls_dh']:
+ if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none':
if not checkCertHeader('-----BEGIN DH PARAMETERS-----', openvpn['tls_dh']):
raise ConfigError('Specified dh-file "{}" is not valid'.format(openvpn['tls_dh']))
@@ -832,7 +640,7 @@ def verify(openvpn):
if openvpn['protocol'] == 'tcp-passive':
raise ConfigError('Cannot specify "tcp-passive" when "tls role" is "active"')
- if openvpn['tls_dh']:
+ if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none':
raise ConfigError('Cannot specify "tls dh-file" when "tls role" is "active"')
elif openvpn['tls_role'] == 'passive':
@@ -842,6 +650,12 @@ def verify(openvpn):
if not openvpn['tls_dh']:
raise ConfigError('Must specify "tls dh-file" when "tls role" is "passive"')
+ if openvpn['tls_key'] and checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls_key']):
+ if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none':
+ print('Warning: using dh-file and EC keys simultaneously will lead to DH ciphers being used instead of ECDH')
+ else:
+ print('Diffie-Hellman prime file is unspecified, assuming ECDH')
+
#
# Auth user/pass
#
@@ -857,20 +671,27 @@ def verify(openvpn):
#
subnet = openvpn['server_subnet'].replace(' ', '/')
for client in openvpn['client']:
- if not ip_address(client['ip']) in ip_network(subnet):
+ if client['ip'] and not ip_address(client['ip']) in ip_network(subnet):
raise ConfigError('Client IP "{}" not in server subnet "{}'.format(client['ip'], subnet))
-
-
return None
def generate(openvpn):
if openvpn['deleted'] or openvpn['disable']:
return None
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'openvpn')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+
interface = openvpn['intf']
directory = os.path.dirname(get_config_name(interface))
+ # we can't know which clients were deleted, remove all client configs
+ if os.path.isdir(os.path.join(directory, 'ccd', interface)):
+ rmtree(os.path.join(directory, 'ccd', interface), ignore_errors=True)
+
# create config directory on demand
openvpn_mkdir(directory)
# create status directory on demand
@@ -892,6 +713,11 @@ def generate(openvpn):
fixup_permission(auth_file)
+ else:
+ # delete old auth file if present
+ if os.path.isfile('/tmp/openvpn-{}-pw'.format(interface)):
+ os.remove('/tmp/openvpn-{}-pw'.format(interface))
+
# get numeric uid/gid
uid = getpwnam(user).pw_uid
gid = getgrnam(group).gr_gid
@@ -899,19 +725,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("&quot;",'"')
-
with open(get_config_name(interface), 'w') as f:
f.write(config_text)
os.chown(get_config_name(interface), uid, gid)
@@ -919,20 +743,18 @@ def generate(openvpn):
return None
def apply(openvpn):
- pid = 0
pidfile = '/var/run/openvpn/{}.pid'.format(openvpn['intf'])
- if os.path.isfile(pidfile):
- pid = 0
- with open(pidfile, 'r') as f:
- pid = int(f.read())
# Always stop OpenVPN service. We can not send a SIGUSR1 for restart of the
# service as the configuration is not re-read. Stop daemon only if it's
# running - it could have died or killed by someone evil
- if pid_exists(pid):
- cmd = 'start-stop-daemon --stop --quiet'
- cmd += ' --pidfile ' + pidfile
- subprocess_cmd(cmd)
+ if process_running(pidfile):
+ command = 'start-stop-daemon'
+ command += ' --stop '
+ command += ' --quiet'
+ command += ' --oknodo'
+ command += ' --pidfile ' + pidfile
+ cmd(command)
# cleanup old PID file
if os.path.isfile(pidfile):
@@ -946,11 +768,12 @@ def apply(openvpn):
# cleanup client config dir
directory = os.path.dirname(get_config_name(openvpn['intf']))
- if os.path.isdir(directory + '/ccd/' + openvpn['intf']):
- try:
- os.remove(directory + '/ccd/' + openvpn['intf'] + '/*')
- except:
- pass
+ if os.path.isdir(os.path.join(directory, 'ccd', openvpn['intf'])):
+ rmtree(os.path.join(directory, 'ccd', openvpn['intf']), ignore_errors=True)
+
+ # cleanup auth file
+ if os.path.isfile('/tmp/openvpn-{}-pw'.format(openvpn['intf'])):
+ os.remove('/tmp/openvpn-{}-pw'.format(openvpn['intf']))
return None
@@ -962,16 +785,19 @@ def apply(openvpn):
# No matching OpenVPN process running - maybe it got killed or none
# existed - nevertheless, spawn new OpenVPN process
- cmd = 'start-stop-daemon --start --quiet'
- cmd += ' --pidfile ' + pidfile
- cmd += ' --exec /usr/sbin/openvpn'
+ command = 'start-stop-daemon'
+ command += ' --start '
+ command += ' --quiet'
+ command += ' --oknodo'
+ command += ' --pidfile ' + pidfile
+ command += ' --exec /usr/sbin/openvpn'
# now pass arguments to openvpn binary
- cmd += ' --'
- cmd += ' --daemon openvpn-' + openvpn['intf']
- cmd += ' --config ' + get_config_name(openvpn['intf'])
+ command += ' --'
+ command += ' --daemon openvpn-' + openvpn['intf']
+ command += ' --config ' + get_config_name(openvpn['intf'])
# execute assembled command
- subprocess_cmd(cmd)
+ cmd(command)
# better late then sorry ... but we can only set interface alias after
# OpenVPN has been launched and created the interface
@@ -991,14 +817,25 @@ def apply(openvpn):
try:
# we need to catch the exception if the interface is not up due to
# reason stated above
- Interface(openvpn['intf']).set_alias(openvpn['description'])
+ o = VTunIf(openvpn['intf'])
+ # update interface description used e.g. within SNMP
+ o.set_alias(openvpn['description'])
+ # IPv6 address autoconfiguration
+ o.set_ipv6_autoconf(openvpn['ipv6_autoconf'])
+ # IPv6 EUI-based address
+ o.set_ipv6_eui64_address(openvpn['ipv6_eui64_prefix'])
+ # IPv6 forwarding
+ o.set_ipv6_forwarding(openvpn['ipv6_forwarding'])
+ # IPv6 Duplicate Address Detection (DAD) tries
+ o.set_ipv6_dad_messages(openvpn['ipv6_dup_addr_detect'])
+
except:
pass
# TAP interface needs to be brought up explicitly
if openvpn['type'] == 'tap':
if not openvpn['disable']:
- Interface(openvpn['intf']).set_state('up')
+ VTunIf(openvpn['intf']).set_admin_state('up')
return None
diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py
index 6acb45d5e..353a5a12c 100755
--- a/src/conf_mode/interfaces-pppoe.py
+++ b/src/conf_mode/interfaces-pppoe.py
@@ -18,85 +18,14 @@ import os
from sys import exit
from copy import deepcopy
-from jinja2 import Template
-from subprocess import Popen, PIPE
-from time import sleep
-from pwd import getpwnam
-from grp import getgrnam
+from jinja2 import FileSystemLoader, Environment
+from netifaces import interfaces
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
from vyos.ifconfig import Interface
+from vyos.util import chown, chmod_x, cmd
from vyos 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
-
-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
-{% endif %}
-{% if service_name -%}
-rp_pppoe_service "{{ service_name }}"
-{% endif %}
-
-"""
-
-PPP_LOGFILE = '/var/log/vyatta/ppp_{}.log'
default_config_data = {
'access_concentrator': '',
@@ -105,7 +34,7 @@ default_config_data = {
'on_demand': False,
'default_route': 'auto',
'deleted': False,
- 'description': '',
+ 'description': '\0',
'disable': False,
'intf': '',
'idle_timeout': '',
@@ -117,24 +46,21 @@ default_config_data = {
'name_server': True,
'remote_address': '',
'service_name': '',
- 'source_interface': ''
+ 'source_interface': '',
+ 'vrf': ''
}
-def subprocess_cmd(command):
- p = Popen(command, stdout=PIPE, shell=True)
- p.communicate()
-
def get_config():
pppoe = deepcopy(default_config_data)
conf = Config()
base_path = ['interfaces', 'pppoe']
# determine tagNode instance
- try:
- pppoe['intf'] = os.environ['VYOS_TAGNODE_VALUE']
- pppoe['logfile'] = PPP_LOGFILE.format(pppoe['intf'])
- except KeyError as E:
- print("Interface not specified")
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ pppoe['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ pppoe['logfile'] = f"/var/log/vyatta/ppp_{pppoe['intf']}.log"
# Check if interface has been removed
if not conf.exists(base_path + [pppoe['intf']]):
@@ -190,7 +116,7 @@ def get_config():
# Physical Interface used for this PPPoE session
if conf.exists(['source-interface']):
- pppoe['source_interface'] = conf.return_value('source-interface')
+ pppoe['source_interface'] = conf.return_value(['source-interface'])
# Maximum Transmission Unit (MTU)
if conf.exists(['mtu']):
@@ -208,6 +134,10 @@ def get_config():
if conf.exists(['service-name']):
pppoe['service_name'] = conf.return_value(['service-name'])
+ # retrieve VRF instance
+ if conf.exists('vrf'):
+ pppoe['vrf'] = conf.return_value(['vrf'])
+
return pppoe
def verify(pppoe):
@@ -216,32 +146,90 @@ def verify(pppoe):
return None
if not pppoe['source_interface']:
- raise ConfigError('PPPoE source interface is missing')
+ raise ConfigError('PPPoE source interface missing')
+
+ if not pppoe['source_interface'] in interfaces():
+ raise ConfigError(f"PPPoE source interface {pppoe['source_interface']} does not exist")
+
+ vrf_name = pppoe['vrf']
+ if vrf_name and vrf_name not in interfaces():
+ raise ConfigError(f'VRF {vrf_name} does not exist')
- if pppoe['source_interface'] not in interfaces():
- raise ConfigError('PPPoE source interface does not exist')
+ if pppoe['on_demand'] and pppoe['vrf']:
+ raise ConfigError('On-demand dialing and VRF can not be used at the same time')
return None
def generate(pppoe):
- config_file_pppoe = '/etc/ppp/peers/{}'.format(pppoe['intf'])
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir["data"], "templates", "pppoe")
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader, trim_blocks=True)
+
+ # set up configuration file path variables where our templates will be
+ # rendered into
+ intf = pppoe['intf']
+ 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_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:
+ dirname = os.path.dirname(file)
+ if not os.path.isdir(dirname):
+ os.mkdir(dirname)
# Always hang-up PPPoE connection prior generating new configuration file
- cmd = 'systemctl stop ppp@{}.service'.format(pppoe['intf'])
- subprocess_cmd(cmd)
+ cmd(f'systemctl stop ppp@{intf}.service')
if pppoe['deleted']:
# Delete PPP configuration files
- if os.path.exists(config_file_pppoe):
- os.unlink(config_file_pppoe)
+ for file in config_files:
+ if os.path.exists(file):
+ os.unlink(file)
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(config_file_pppoe, 'w') as f:
+ 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(script_pppoe_ip_up, 'w') as f:
+ f.write(config_text)
+
+ # Create script for ip-down.d
+ tmpl = env.get_template('ip-down.script.tmpl')
+ config_text = tmpl.render(pppoe)
+ with open(script_pppoe_ip_down, 'w') as f:
+ f.write(config_text)
+
+ # Create script for ipv6-up.d
+ tmpl = env.get_template('ipv6-up.script.tmpl')
+ config_text = tmpl.render(pppoe)
+ with open(script_pppoe_ipv6_up, 'w') as f:
+ f.write(config_text)
+
+ # make generated script file executable
+ chmod_x(script_pppoe_pre_up)
+ chmod_x(script_pppoe_ip_up)
+ chmod_x(script_pppoe_ip_down)
+ chmod_x(script_pppoe_ipv6_up)
+
return None
def apply(pppoe):
@@ -250,33 +238,12 @@ def apply(pppoe):
return None
if not pppoe['disable']:
- # dial PPPoE connection
- cmd = 'systemctl start ppp@{}.service'.format(pppoe['intf'])
- subprocess_cmd(cmd)
+ # "dial" PPPoE connection
+ intf = pppoe['intf']
+ cmd(f'systemctl start ppp@{intf}.service')
# make logfile owned by root / vyattacfg
- if os.path.isfile(pppoe['logfile']):
- uid = getpwnam('root').pw_uid
- gid = getgrnam('vyattacfg').gr_gid
- os.chown(pppoe['logfile'], uid, gid)
-
- # better late then sorry ... but we can only set interface alias after
- # pppd has been launched and created the interface
- cnt = 0
- while pppoe['intf'] not in interfaces():
- cnt += 1
- if cnt == 50:
- break
-
- # sleep 250ms
- sleep(0.250)
-
- try:
- # we need to catch the exception if the interface is not up due to
- # reason stated above
- Interface(pppoe['intf']).set_alias(pppoe['description'])
- except:
- pass
+ chown(pppoe['logfile'], 'root', 'vyattacfg')
return None
diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py
index 864e28936..ce3d472c4 100755
--- a/src/conf_mode/interfaces-pseudo-ethernet.py
+++ b/src/conf_mode/interfaces-pseudo-ethernet.py
@@ -21,8 +21,10 @@ from sys import exit
from netifaces import interfaces
from vyos.ifconfig import MACVLANIf
-from vyos.configdict import list_diff
+from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
+from vyos.configdict import list_diff, vlan_to_dict
from vyos.config import Config
+from vyos.util import is_bridge_member
from vyos import ConfigError
default_config_data = {
@@ -44,15 +46,20 @@ default_config_data = {
'ip_enable_arp_ignore': 0,
'ip_proxy_arp': 0,
'ip_proxy_arp_pvlan': 0,
+ 'ipv6_autoconf': 0,
+ 'ipv6_eui64_prefix': '',
+ 'ipv6_forwarding': 1,
+ 'ipv6_dup_addr_detect': 1,
'intf': '',
- 'link': '',
- 'link_changed': False,
+ 'source_interface': '',
+ 'source_interface_changed': False,
'mac': '',
'mode': 'private',
'vif_s': [],
'vif_s_remove': [],
'vif': [],
- 'vif_remove': []
+ 'vif_remove': [],
+ 'vrf': ''
}
def get_config():
@@ -60,11 +67,10 @@ def get_config():
conf = Config()
# determine tagNode instance
- try:
- peth['intf'] = os.environ['VYOS_TAGNODE_VALUE']
- except KeyError as E:
- print("Interface not specified")
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+ peth['intf'] = os.environ['VYOS_TAGNODE_VALUE']
cfg_base = ['interfaces', 'pseudo-ethernet', peth['intf']]
# Check if interface has been removed
@@ -144,12 +150,28 @@ def get_config():
if conf.exists(['ip', 'proxy-arp-pvlan']):
peth['ip_proxy_arp_pvlan'] = 1
- # Lower link device
- if conf.exists(['link']):
- peth['link'] = conf.return_value(['link'])
- tmp = conf.return_effective_value(['link'])
- if tmp != peth['link']:
- peth['link_changed'] = True
+ # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
+ if conf.exists('ipv6 address autoconf'):
+ peth['ipv6_autoconf'] = 1
+
+ # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ if conf.exists('ipv6 address eui64'):
+ peth['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+
+ # Disable IPv6 forwarding on this interface
+ if conf.exists('ipv6 disable-forwarding'):
+ peth['ipv6_forwarding'] = 0
+
+ # IPv6 Duplicate Address Detection (DAD) tries
+ if conf.exists('ipv6 dup-addr-detect-transmits'):
+ peth['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+
+ # Physical interface
+ if conf.exists(['source-interface']):
+ peth['source_interface'] = conf.return_value(['source-interface'])
+ tmp = conf.return_effective_value(['source-interface'])
+ if tmp != peth['source_interface']:
+ peth['source_interface_changed'] = True
# Media Access Control (MAC) address
if conf.exists(['mac']):
@@ -159,6 +181,10 @@ def get_config():
if conf.exists(['mode']):
peth['mode'] = conf.return_value(['mode'])
+ # retrieve VRF instance
+ if conf.exists('vrf'):
+ peth['vrf'] = conf.return_value('vrf')
+
# re-set configuration level to parse new nodes
conf.set_level(cfg_base)
# get vif-s interfaces (currently effective) - to determine which vif-s
@@ -192,11 +218,27 @@ def get_config():
def verify(peth):
if peth['deleted']:
+ interface = peth['intf']
+ is_member, bridge = is_bridge_member(interface)
+ if is_member:
+ # can not use a f'' formatted-string here as bridge would not get
+ # expanded in the print statement
+ raise ConfigError('Can not delete interface "{0}" as it ' \
+ 'is a member of bridge "{1}"!'.format(interface, bridge))
return None
- if not peth['link']:
+ if not peth['source_interface']:
raise ConfigError('Link device must be set for virtual ethernet {}'.format(peth['intf']))
+ if not peth['source_interface'] in interfaces():
+ raise ConfigError('Pseudo-ethernet source interface does not exist')
+
+ vrf_name = peth['vrf']
+ if vrf_name and vrf_name not in interfaces():
+ raise ConfigError(f'VRF "{vrf_name}" does not exist')
+
+ # use common function to verify VLAN configuration
+ verify_vlan_config(peth)
return None
def generate(peth):
@@ -211,12 +253,12 @@ def apply(peth):
p.remove()
return None
- elif peth['link_changed']:
+ elif peth['source_interface_changed']:
# Check if MACVLAN interface already exists. Parameters like the
- # underlaying link device can not be changed on the fly and the
- # interface needs to be recreated from the bottom.
+ # underlaying source-interface device can not be changed on the fly
+ # and the interface needs to be recreated from the bottom.
#
- # link_changed also means - the interface was not present in the
+ # source_interface_changed also means - the interface was not present in the
# beginning and is newly created
if peth['intf'] in interfaces():
p = MACVLANIf(peth['intf'])
@@ -227,12 +269,12 @@ def apply(peth):
conf = deepcopy(MACVLANIf.get_config())
# Assign MACVLAN instance configuration parameters to config dict
- conf['link'] = peth['link']
+ conf['source_interface'] = peth['source_interface']
conf['mode'] = peth['mode']
# It is safe to "re-create" the interface always, there is a sanity check
# that the interface will only be create if its non existent
- p = MACVLANIf(peth['intf'], config=conf)
+ p = MACVLANIf(peth['intf'], **conf)
else:
p = MACVLANIf(peth['intf'])
@@ -282,6 +324,17 @@ def apply(peth):
p.set_proxy_arp(peth['ip_proxy_arp'])
# Enable private VLAN proxy ARP on this interface
p.set_proxy_arp_pvlan(peth['ip_proxy_arp_pvlan'])
+ # IPv6 address autoconfiguration
+ p.set_ipv6_autoconf(peth['ipv6_autoconf'])
+ # IPv6 EUI-based address
+ p.set_ipv6_eui64_address(peth['ipv6_eui64_prefix'])
+ # IPv6 forwarding
+ p.set_ipv6_forwarding(peth['ipv6_forwarding'])
+ # IPv6 Duplicate Address Detection (DAD) tries
+ p.set_ipv6_dad_messages(peth['ipv6_dup_addr_detect'])
+
+ # assign/remove VRF
+ p.set_vrf(peth['vrf'])
# Change interface MAC address
if peth['mac']:
@@ -292,9 +345,9 @@ def apply(peth):
# Enable/Disable interface
if peth['disable']:
- p.set_state('down')
+ p.set_admin_state('down')
else:
- p.set_state('up')
+ p.set_admin_state('up')
# Configure interface address(es)
# - not longer required addresses get removed first
@@ -304,6 +357,35 @@ def apply(peth):
for addr in peth['address']:
p.add_addr(addr)
+ # remove no longer required service VLAN interfaces (vif-s)
+ for vif_s in peth['vif_s_remove']:
+ p.del_vlan(vif_s)
+
+ # create service VLAN interfaces (vif-s)
+ for vif_s in peth['vif_s']:
+ s_vlan = p.add_vlan(vif_s['id'], ethertype=vif_s['ethertype'])
+ apply_vlan_config(s_vlan, vif_s)
+
+ # remove no longer required client VLAN interfaces (vif-c)
+ # on lower service VLAN interface
+ for vif_c in vif_s['vif_c_remove']:
+ s_vlan.del_vlan(vif_c)
+
+ # create client VLAN interfaces (vif-c)
+ # on lower service VLAN interface
+ for vif_c in vif_s['vif_c']:
+ c_vlan = s_vlan.add_vlan(vif_c['id'])
+ apply_vlan_config(c_vlan, vif_c)
+
+ # remove no longer required VLAN interfaces (vif)
+ for vif in peth['vif_remove']:
+ p.del_vlan(vif)
+
+ # create VLAN interfaces (vif)
+ for vif in peth['vif']:
+ vlan = p.add_vlan(vif['id'])
+ apply_vlan_config(vlan, vif)
+
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
new file mode 100755
index 000000000..28b1cf60f
--- /dev/null
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -0,0 +1,646 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import netifaces
+
+from sys import exit
+from copy import deepcopy
+
+from vyos.config import Config
+from vyos.ifconfig import Interface, GREIf, GRETapIf, IPIPIf, IP6GREIf, IPIP6If, IP6IP6If, SitIf, Sit6RDIf
+from vyos.ifconfig.afi import IP4, IP6
+from vyos.configdict import list_diff
+from vyos.validate import is_ipv4, is_ipv6
+from vyos import ConfigError
+
+
+class FixedDict(dict):
+ """
+ FixedDict: A dictionnary not allowing new keys to be created after initialisation.
+
+ >>> f = FixedDict(**{'count':1})
+ >>> f['count'] = 2
+ >>> f['king'] = 3
+ File "...", line ..., in __setitem__
+ raise ConfigError(f'Option "{k}" has no defined default')
+ """
+ def __init__ (self, **options):
+ self._allowed = options.keys()
+ super().__init__(**options)
+
+ def __setitem__ (self, k, v):
+ """
+ __setitem__ is a builtin which is called by python when setting dict values:
+ >>> d = dict()
+ >>> d['key'] = 'value'
+ >>> d
+ {'key': 'value'}
+
+ is syntaxic sugar for
+
+ >>> d = dict()
+ >>> d.__setitem__('key','value')
+ >>> d
+ {'key': 'value'}
+ """
+ if k not in self._allowed:
+ raise ConfigError(f'Option "{k}" has no defined default')
+ super().__setitem__(k, v)
+
+
+class ConfigurationState(Config):
+ """
+ The current API require a dict to be generated by get_config()
+ which is then consumed by verify(), generate() and apply()
+
+ ConfiguartionState is an helper class wrapping Config and providing
+ an common API to this dictionary structure
+
+ Its to_dict() function return a dictionary containing three fields,
+ each a dict, called options, changes, actions.
+
+ options:
+
+ contains the configuration options for the dict and its value
+ {'options': {'commment': 'test'}} will be set if
+ 'set interface dummy dum1 description test' was used and
+ the key 'commment' is used to index the description info.
+
+ changes:
+
+ per key, let us know how the data was modified using one of the action
+ a special key called 'section' is used to indicate what happened to the
+ section. for example:
+
+ 'set interface dummy dum1 description test' when no interface was setup
+ will result in the following changes
+ {'changes': {'section': 'create', 'comment': 'create'}}
+
+ on an existing interface, depending if there was a description
+ 'set interface dummy dum1 description test' will result in one of
+ {'changes': {'comment': 'create'}} (not present before)
+ {'changes': {'comment': 'static'}} (unchanged)
+ {'changes': {'comment': 'modify'}} (changed from half)
+
+ and 'delete interface dummy dummy1 description' will result in:
+ {'changes': {'comment': 'delete'}}
+
+ actions:
+
+ for each action list the configuration key which were changes
+ in our example if we added the 'description' and added an IP we would have
+ {'actions': { 'create': ['comment'], 'modify': ['addresses-add']}}
+
+ the actions are:
+ 'create': it did not exist previously and was created
+ 'modify': it did exist previously but its content changed
+ 'static': it did exist and did not change
+ 'delete': it was present but was removed from the configuration
+ 'absent': it was not and is not present
+ which for each field represent how it was modified since the last commit
+ """
+
+ def __init__ (self, section, default):
+ """
+ initialise the class for a given configuration path:
+
+ >>> conf = ConfigurationState('interfaces ethernet eth1')
+ all further references to get_value(s) and get_effective(s)
+ will be for this part of the configuration (eth1)
+ """
+ super().__init__()
+ self.section = section
+ self.default = deepcopy(default)
+ self.options = FixedDict(**default)
+ self.actions = {
+ 'create': [], # the key did not exist and was added
+ 'static': [], # the key exists and its value was not modfied
+ 'modify': [], # the key exists and its value was modified
+ 'absent': [], # the key is not present
+ 'delete': [], # the key was present and was deleted
+ }
+ self.changes = {}
+ if not self.exists(section):
+ self.changes['section'] = 'delete'
+ elif self.exists_effective(section):
+ self.changes['section'] = 'modify'
+ else:
+ self.changes['section'] = 'create'
+
+ def _act(self, section):
+ """
+ Returns for a given configuration field determine what happened to it
+
+ 'create': it did not exist previously and was created
+ 'modify': it did exist previously but its content changed
+ 'static': it did exist and did not change
+ 'delete': it was present but was removed from the configuration
+ 'absent': it was not and is not present
+ """
+ if self.exists(section):
+ if self.exists_effective(section):
+ if self.return_value(section) != self.return_effective_value(section):
+ return 'modify'
+ return 'static'
+ return 'create'
+ else:
+ if self.exists_effective(section):
+ return 'delete'
+ return 'absent'
+
+ def _action (self, name, key):
+ action = self._act(key)
+ self.changes[name] = action
+ self.actions[action].append(name)
+ return action
+
+ def _get(self, name, key, default, getter):
+ value = getter(key)
+ if not value:
+ if default:
+ self.options[name] = default
+ return
+ self.options[name] = self.default[name]
+ return
+ self.options[name] = value
+
+ def get_value(self, name, key, default=None):
+ """
+ >>> conf.get_value('comment', 'description')
+ will place the string of 'interface dummy description test'
+ into the dictionnary entry 'comment' using Config.return_value
+ (the data in the configuration to apply)
+ """
+ if self._action(name, key) in ('delete', 'absent'):
+ return
+ return self._get(name, key, default, self.return_value)
+
+ def get_values(self, name, key, default=None):
+ """
+ >>> conf.get_values('addresses-add', 'address')
+ will place a list made of the IP present in 'interface dummy dum1 address'
+ into the dictionnary entry 'addr' using Config.return_values
+ (the data in the configuration to apply)
+ """
+ if self._action(name, key) in ('delete', 'absent'):
+ return
+ return self._get(name, key, default, self.return_values)
+
+ def get_effective(self, name, key, default=None):
+ """
+ >>> conf.get_value('comment', 'description')
+ will place the string of 'interface dummy description test'
+ into the dictionnary entry 'comment' using Config.return_effective_value
+ (the data in the configuration to apply)
+ """
+ self._action(name, key)
+ return self._get(name, key, default, self.return_effective_value)
+
+ def get_effectives(self, name, key, default=None):
+ """
+ >>> conf.get_effectives('addresses-add', 'address')
+ will place a list made of the IP present in 'interface ethernet eth1 address'
+ into the dictionnary entry 'addresses-add' using Config.return_effectives_value
+ (the data in the un-modified configuration)
+ """
+ self._action(name, key)
+ return self._get(name, key, default, self.return_effectives_value)
+
+ def load(self, mapping):
+ """
+ load will take a dictionary defining how we wish the configuration
+ to be parsed and apply this definition to set the data.
+
+ >>> mapping = {
+ 'addresses-add' : ('address', True, None),
+ 'comment' : ('description', False, 'auto'),
+ }
+ >>> conf.load(mapping)
+
+ mapping is a dictionary where each key represents the name we wish
+ to have (such as 'addresses-add'), with a list a content representing
+ how the data should be parsed:
+ - the configuration section name
+ such as 'address' under 'interface ethernet eth1'
+ - boolean indicating if this data can have multiple values
+ for 'address', True, as multiple IPs can be set
+ for 'description', False, as it is a single string
+ - default represent the default value if absent from the configuration
+ 'None' indicate that no default should be set if the configuration
+ does not have the configuration section
+
+ """
+ for local_name, (config_name, multiple, default) in mapping.items():
+ if multiple:
+ self.get_values(local_name, config_name, default)
+ else:
+ self.get_value(local_name, config_name, default)
+
+ def remove_default (self,*options):
+ """
+ remove all the values which were not changed from the default
+ """
+ for option in options:
+ if self.exists(option) and self_return_value(option) != self.default[option]:
+ continue
+ del self.options[option]
+
+ def to_dict (self):
+ """
+ provide a dictionary with the generated data for the configuration
+ options: the configuration value for the key
+ changes: per key how they changed from the previous configuration
+ actions: per changes all the options which were changed
+ """
+ # as we have to use a dict() for the API for verify and apply the options
+ return {
+ 'options': self.options,
+ 'changes': self.changes,
+ 'actions': self.actions,
+ }
+
+default_config_data = {
+ # interface definition
+ 'vrf': '',
+ 'addresses-add': [],
+ 'addresses-del': [],
+ 'state': 'up',
+ 'dhcp-interface': '',
+ 'link_detect': 1,
+ 'ip': False,
+ 'ipv6': False,
+ 'nhrp': [],
+ 'ipv6_autoconf': 0,
+ 'ipv6_forwarding': 1,
+ 'ipv6_dad_transmits': 1,
+ # internal
+ 'tunnel': {},
+ # the following names are exactly matching the name
+ # for the ip command and must not be changed
+ 'ifname': '',
+ 'type': '',
+ 'alias': '',
+ 'mtu': '1476',
+ 'local': '',
+ 'remote': '',
+ 'multicast': 'disable',
+ 'allmulticast': 'disable',
+ 'ttl': '255',
+ 'tos': 'inherit',
+ 'key': '',
+ 'encaplimit': '4',
+ 'flowlabel': 'inherit',
+ 'hoplimit': '64',
+ 'tclass': 'inherit',
+ '6rd-prefix': '',
+ '6rd-relay-prefix': '',
+}
+
+# dict name -> config name, multiple values, default
+mapping = {
+ 'type': ('encapsulation', False, None),
+ 'alias': ('description', False, None),
+ 'mtu': ('mtu', False, None),
+ 'local': ('local-ip', False, None),
+ 'remote': ('remote-ip', False, None),
+ 'multicast': ('multicast', False, None),
+ 'ttl': ('parameters ip ttl', False, None),
+ 'tos': ('parameters ip tos', False, None),
+ 'key': ('parameters ip key', False, None),
+ 'encaplimit': ('parameters ipv6 encaplimit', False, None),
+ 'flowlabel': ('parameters ipv6 flowlabel', False, None),
+ 'hoplimit': ('parameters ipv6 hoplimit', False, None),
+ 'tclass': ('parameters ipv6 tclass', False, None),
+ '6rd-prefix': ('6rd-prefix', False, None),
+ '6rd-relay-prefix': ('6rd-relay-prefix', False, None),
+ 'dhcp-interface': ('dhcp-interface', False, None),
+ 'state': ('disable', False, 'down'),
+ 'link_detect': ('disable-link-detect', False, 2),
+ 'vrf': ('vrf', False, None),
+ 'addresses-add': ('address', True, None),
+ 'ipv6_autoconf': ('ipv6 address autoconf', False, 1),
+ 'ipv6_forwarding': ('ipv6 disable-forwarding', False, 0),
+ 'ipv6_dad_transmits:': ('ipv6 dup-addr-detect-transmits', False, None)
+}
+
+def get_class (options):
+ dispatch = {
+ 'gre': GREIf,
+ 'gre-bridge': GRETapIf,
+ 'ipip': IPIPIf,
+ 'ipip6': IPIP6If,
+ 'ip6ip6': IP6IP6If,
+ 'ip6gre': IP6GREIf,
+ 'sit': SitIf,
+ }
+
+ kls = dispatch[options['type']]
+ if options['type'] == 'gre' and not options['remote'] \
+ and not options['key'] and not options['multicast']:
+ # will use GreTapIf on GreIf deletion but it does not matter
+ return GRETapIf
+ elif options['type'] == 'sit' and options['6rd-prefix']:
+ # will use SitIf on Sit6RDIf deletion but it does not matter
+ return Sit6RDIf
+ return kls
+
+def get_interface_ip (ifname):
+ if not ifname:
+ return ''
+ try:
+ addrs = Interface(ifname).get_addr()
+ if addrs:
+ return addrs[0].split('/')[0]
+ except Exception:
+ return ''
+
+def get_afi (ip):
+ return IP6 if is_ipv6(ip) else IP4
+
+def ip_proto (afi):
+ return 6 if afi == IP6 else 4
+
+
+def get_config():
+ ifname = os.environ.get('VYOS_TAGNODE_VALUE','')
+ if not ifname:
+ raise ConfigError('Interface not specified')
+
+ conf = ConfigurationState('interfaces tunnel ' + ifname, default_config_data)
+ options = conf.options
+ changes = conf.changes
+ options['ifname'] = ifname
+
+ # set new configuration level
+ conf.set_level(conf.section)
+
+ if changes['section'] == 'delete':
+ conf.get_effective('type', mapping['type'][0])
+ conf.set_level('protocols nhrp tunnel')
+ options['nhrp'] = conf.list_nodes('')
+ return conf.to_dict()
+
+ # load all the configuration option according to the mapping
+ conf.load(mapping)
+
+ # remove default value if not set and not required
+ afi_local = get_afi(options['local'])
+ if afi_local == IP6:
+ conf.remove_default('ttl', 'tos', 'key')
+ if afi_local == IP4:
+ conf.remove_default('encaplimit', 'flowlabel', 'hoplimit', 'tclass')
+
+ # if the local-ip is not set, pick one from the interface !
+ # hopefully there is only one, otherwise it will not be very deterministic
+ # at time of writing the code currently returns ipv4 before ipv6 in the list
+
+ # XXX: There is no way to trigger an update of the interface source IP if
+ # XXX: the underlying interface IP address does change, I believe this
+ # XXX: limit/issue is present in vyatta too
+
+ if not options['local'] and options['dhcp-interface']:
+ # XXX: This behaviour changes from vyatta which would return 127.0.0.1 if
+ # XXX: the interface was not DHCP. As there is no easy way to find if an
+ # XXX: interface is using DHCP, and using this feature to get 127.0.0.1
+ # XXX: makes little sense, I feel the change in behaviour is acceptable
+ picked = get_interface_ip(options['dhcp-interface'])
+ if picked == '':
+ picked = '127.0.0.1'
+ print('Could not get an IP address from {dhcp-interface} using 127.0.0.1 instead')
+ options['local'] = picked
+ options['dhcp-interface'] = ''
+
+ # get interface addresses (currently effective) - to determine which
+ # address is no longer valid and needs to be removed
+ # could be done within ConfigurationState
+ eff_addr = conf.return_effective_values('address')
+ options['addresses-del'] = list_diff(eff_addr, options['addresses-add'])
+
+ # allmulticast fate is linked to multicast
+ options['allmulticast'] = options['multicast']
+
+ # check that per encapsulation all local-remote pairs are unique
+ conf.set_level('interfaces tunnel')
+ ct = conf.get_config_dict()['tunnel']
+ options['tunnel'] = {}
+
+ for name in ct:
+ tunnel = ct[name]
+ encap = tunnel.get('encapsulation', '')
+ local = tunnel.get('local-ip', '')
+ if not local:
+ local = get_interface_ip(tunnel.get('dhcp-interface', ''))
+ remote = tunnel.get('remote-ip', '<unset>')
+ pair = f'{local}-{remote}'
+ options['tunnel'][encap][pair] = options['tunnel'].setdefault(encap, {}).get(pair, 0) + 1
+
+ return conf.to_dict()
+
+
+def verify(conf):
+ options = conf['options']
+ changes = conf['changes']
+ actions = conf['actions']
+
+ ifname = options['ifname']
+ iftype = options['type']
+
+ if changes['section'] == 'delete':
+ if ifname in options['nhrp']:
+ raise ConfigError(f'Can not delete interface tunnel {iftype} {ifname}, it is used by nhrp')
+ # done, bail out early
+ return None
+
+ # tunnel encapsulation checks
+
+ if not iftype:
+ raise ConfigError(f'Must provide an "encapsulation" for tunnel {iftype} {ifname}')
+
+ if changes['type'] in ('modify', 'delete'):
+ # TODO: we could now deal with encapsulation modification by deleting / recreating
+ raise ConfigError(f'Encapsulation can only be set at tunnel creation for tunnel {iftype} {ifname}')
+
+ if iftype != 'sit' and options['6rd-prefix']:
+ # XXX: should be able to remove this and let the definition catch it
+ print(f'6RD can only be configured for sit interfaces not tunnel {iftype} {ifname}')
+
+ # what are the tunnel options we can set / modified / deleted
+
+ kls = get_class(options)
+ valid = kls.updates + ['alias', 'addresses-add', 'addresses-del', 'vrf']
+
+ if changes['section'] == 'create':
+ valid.extend(['type',])
+ valid.extend([o for o in kls.options if o not in kls.updates])
+
+ for create in actions['create']:
+ if create not in valid:
+ raise ConfigError(f'Can not set "{create}" for tunnel {iftype} {ifname} at tunnel creation')
+
+ for modify in actions['modify']:
+ if modify not in valid:
+ raise ConfigError(f'Can not modify "{modify}" for tunnel {iftype} {ifname}. it must be set at tunnel creation')
+
+ for delete in actions['delete']:
+ if delete in kls.required:
+ raise ConfigError(f'Can not remove "{delete}", it is an mandatory option for tunnel {iftype} {ifname}')
+
+ # tunnel information
+
+ tun_local = options['local']
+ afi_local = get_afi(tun_local)
+ tun_remote = options['remote'] or tun_local
+ afi_remote = get_afi(tun_remote)
+ tun_ismgre = iftype == 'gre' and not options['remote']
+ tun_is6rd = iftype == 'sit' and options['6rd-prefix']
+
+ # incompatible options
+
+ if not tun_local and not options['dhcp-interface'] and not tun_is6rd:
+ raise ConfigError(f'Must configure either local-ip or dhcp-interface for tunnel {iftype} {ifname}')
+
+ if tun_local and options['dhcp-interface']:
+ raise ConfigError(f'Must configure only one of local-ip or dhcp-interface for tunnel {iftype} {ifname}')
+
+ # tunnel endpoint
+
+ if afi_local != afi_remote:
+ raise ConfigError(f'IPv4/IPv6 mismatch between local-ip and remote-ip for tunnel {iftype} {ifname}')
+
+ if afi_local != kls.tunnel:
+ version = 4 if tun_local == IP4 else 6
+ raise ConfigError(f'Invalid IPv{version} local-ip for tunnel {iftype} {ifname}')
+
+ ipv4_count = len([ip for ip in options['addresses-add'] if is_ipv4(ip)])
+ ipv6_count = len([ip for ip in options['addresses-add'] if is_ipv6(ip)])
+
+ if tun_ismgre and afi_local == IP6:
+ raise ConfigError(f'Using an IPv6 address is forbidden for mGRE tunnels such as tunnel {iftype} {ifname}')
+
+ # check address family use
+ # checks are not enforced (but ip command failing) for backward compatibility
+
+ if ipv4_count and not IP4 in kls.ip:
+ print(f'Should not use IPv4 addresses on tunnel {iftype} {ifname}')
+
+ if ipv6_count and not IP6 in kls.ip:
+ print(f'Should not use IPv6 addresses on tunnel {iftype} {ifname}')
+
+ # tunnel encapsulation check
+
+ convert = {
+ (6, 4, 'gre'): 'ip6gre',
+ (6, 6, 'gre'): 'ip6gre',
+ (4, 6, 'ipip'): 'ipip6',
+ (6, 6, 'ipip'): 'ip6ip6',
+ }
+
+ iprotos = []
+ if ipv4_count:
+ iprotos.append(4)
+ if ipv6_count:
+ iprotos.append(6)
+
+ for iproto in iprotos:
+ replace = convert.get((kls.tunnel, iproto, iftype), '')
+ if replace:
+ raise ConfigError(
+ f'Using IPv6 address in local-ip or remote-ip is not possible with "encapsulation {iftype}". ' +
+ f'Use "encapsulation {replace}" for tunnel {iftype} {ifname} instead.'
+ )
+
+ # tunnel options
+
+ incompatible = []
+ if afi_local == IP6:
+ incompatible.extend(['ttl', 'tos', 'key',])
+ if afi_local == IP4:
+ incompatible.extend(['encaplimit', 'flowlabel', 'hoplimit', 'tclass'])
+
+ for option in incompatible:
+ if option in options:
+ # TODO: raise converted to print as not enforced by vyatta
+ # raise ConfigError(f'{option} is not valid for tunnel {iftype} {ifname}')
+ print(f'Using "{option}" is invalid for tunnel {iftype} {ifname}')
+
+ # duplicate tunnel pairs
+
+ pair = '{}-{}'.format(options['local'], options['remote'])
+ if options['tunnel'].get(iftype, {}).get(pair, 0) > 1:
+ raise ConfigError(f'More than one tunnel configured for with the same encapulation and IPs for tunnel {iftype} {ifname}')
+
+ return None
+
+
+def generate(gre):
+ return None
+
+def apply(conf):
+ options = conf['options']
+ changes = conf['changes']
+ actions = conf['actions']
+ kls = get_class(options)
+
+ # extract ifname as otherwise it is duplicated on the interface creation
+ ifname = options.pop('ifname')
+
+ # only the valid keys for creation of a Interface
+ config = dict((k, options[k]) for k in kls.options if options[k])
+
+ # setup or create the tunnel interface if it does not exist
+ tunnel = kls(ifname, **config)
+
+ if changes['section'] == 'delete':
+ tunnel.remove()
+ # The perl code was calling/opt/vyatta/sbin/vyatta-tunnel-cleanup
+ # which identified tunnels type which were not used anymore to remove them
+ # (ie: gre0, gretap0, etc.) The perl code did however nothing
+ # This feature is also not implemented yet
+ return
+
+ # A GRE interface without remote will be mGRE
+ # if the interface does not suppor the option, it skips the change
+ for option in tunnel.updates:
+ if changes['section'] in 'create' and option in tunnel.options:
+ # it was setup at creation
+ continue
+ tunnel.set_interface(option, options[option])
+
+ # set other interface properties
+ for option in ('alias', 'mtu', 'link_detect', 'multicast', 'allmulticast',
+ 'vrf', 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits'):
+ tunnel.set_interface(option, options[option])
+
+ # Configure interface address(es)
+ for addr in options['addresses-del']:
+ tunnel.del_addr(addr)
+ for addr in options['addresses-add']:
+ tunnel.add_addr(addr)
+
+ # now bring it up (or not)
+ tunnel.set_admin_state(options['state'])
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index efdc21f89..6639a9b0d 100755
--- a/src/conf_mode/interfaces-vxlan.py
+++ b/src/conf_mode/interfaces-vxlan.py
@@ -18,11 +18,12 @@ import os
from sys import exit
from copy import deepcopy
+from netifaces import interfaces
from vyos.config import Config
from vyos.ifconfig import VXLANIf, Interface
+from vyos.util import is_bridge_member
from vyos import ConfigError
-from netifaces import interfaces
default_config_data = {
'address': [],
@@ -37,7 +38,12 @@ default_config_data = {
'ip_enable_arp_announce': 0,
'ip_enable_arp_ignore': 0,
'ip_proxy_arp': 0,
- 'link': '',
+ 'ipv6_autoconf': 0,
+ 'ipv6_eui64_prefix': '',
+ 'ipv6_forwarding': 1,
+ 'ipv6_dup_addr_detect': 1,
+ 'source_address': '',
+ 'source_interface': '',
'mtu': 1450,
'remote': '',
'remote_port': 8472, # The Linux implementation of VXLAN pre-dates
@@ -50,10 +56,10 @@ def get_config():
conf = Config()
# determine tagNode instance
- try:
- vxlan['intf'] = os.environ['VYOS_TAGNODE_VALUE']
- except KeyError as E:
- print("Interface not specified")
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ vxlan['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# Check if interface has been removed
if not conf.exists('interfaces vxlan ' + vxlan['intf']):
@@ -103,9 +109,29 @@ def get_config():
if conf.exists('ip enable-proxy-arp'):
vxlan['ip_proxy_arp'] = 1
+ # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
+ if conf.exists('ipv6 address autoconf'):
+ vxlan['ipv6_autoconf'] = 1
+
+ # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ if conf.exists('ipv6 address eui64'):
+ vxlan['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+
+ # Disable IPv6 forwarding on this interface
+ if conf.exists('ipv6 disable-forwarding'):
+ vxlan['ipv6_forwarding'] = 0
+
+ # IPv6 Duplicate Address Detection (DAD) tries
+ if conf.exists('ipv6 dup-addr-detect-transmits'):
+ vxlan['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+
+ # VXLAN source address
+ if conf.exists('source-address'):
+ vxlan['source_address'] = conf.return_value('source-address')
+
# VXLAN underlay interface
- if conf.exists('link'):
- vxlan['link'] = conf.return_value('link')
+ if conf.exists('source-interface'):
+ vxlan['source_interface'] = conf.return_value('source-interface')
# Maximum Transmission Unit (MTU)
if conf.exists('mtu'):
@@ -128,25 +154,35 @@ def get_config():
def verify(vxlan):
if vxlan['deleted']:
- # bail out early
+ interface = vxlan['intf']
+ is_member, bridge = is_bridge_member(interface)
+ if is_member:
+ # can not use a f'' formatted-string here as bridge would not get
+ # expanded in the print statement
+ raise ConfigError('Can not delete interface "{0}" as it ' \
+ 'is a member of bridge "{1}"!'.format(interface, bridge))
return None
if vxlan['mtu'] < 1500:
print('WARNING: RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU')
- if vxlan['group'] and not vxlan['link']:
- raise ConfigError('Multicast VXLAN requires an underlaying interface ')
+ if vxlan['group']:
+ if not vxlan['source_interface']:
+ raise ConfigError('Multicast VXLAN requires an underlaying interface ')
+
+ if not vxlan['source_interface'] 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['group'] or vxlan['remote'] or vxlan['source_address']):
+ raise ConfigError('Group, remote or source-address must be configured')
if not vxlan['vni']:
raise ConfigError('Must configure VNI for VXLAN')
- if vxlan['link']:
+ if vxlan['source_interface']:
# VXLAN adds a 50 byte overhead - we need to check the underlaying MTU
# if our configured MTU is at least 50 bytes less
- underlay_mtu = int(Interface(vxlan['link']).get_mtu())
+ underlay_mtu = int(Interface(vxlan['source_interface']).get_mtu())
if underlay_mtu < (vxlan['mtu'] + 50):
raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \
'MTU is to small ({})'.format(underlay_mtu))
@@ -175,12 +211,13 @@ def apply(vxlan):
# Assign VXLAN instance configuration parameters to config dict
conf['vni'] = vxlan['vni']
conf['group'] = vxlan['group']
- conf['dev'] = vxlan['link']
+ conf['src_address'] = vxlan['source_address']
+ conf['src_interface'] = vxlan['source_interface']
conf['remote'] = vxlan['remote']
conf['port'] = vxlan['remote_port']
# Finally create the new interface
- v = VXLANIf(vxlan['intf'], config=conf)
+ v = VXLANIf(vxlan['intf'], **conf)
# update interface description used e.g. by SNMP
v.set_alias(vxlan['description'])
# Maximum Transfer Unit (MTU)
@@ -198,6 +235,14 @@ def apply(vxlan):
v.set_arp_ignore(vxlan['ip_enable_arp_ignore'])
# Enable proxy-arp on this interface
v.set_proxy_arp(vxlan['ip_proxy_arp'])
+ # IPv6 address autoconfiguration
+ v.set_ipv6_autoconf(vxlan['ipv6_autoconf'])
+ # IPv6 EUI-based address
+ v.set_ipv6_eui64_address(vxlan['ipv6_eui64_prefix'])
+ # IPv6 forwarding
+ v.set_ipv6_forwarding(vxlan['ipv6_forwarding'])
+ # IPv6 Duplicate Address Detection (DAD) tries
+ v.set_ipv6_dad_messages(vxlan['ipv6_dup_addr_detect'])
# Configure interface address(es) - no need to implicitly delete the
# old addresses as they have already been removed by deleting the
@@ -209,7 +254,7 @@ def apply(vxlan):
# parameters we will only re-enable the interface if it is not
# administratively disabled
if not vxlan['disable']:
- v.set_state('up')
+ v.set_admin_state('up')
return None
diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py
index ff12a5172..8bf81c747 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,263 +13,284 @@
#
# 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 sys import exit
from copy import deepcopy
from netifaces import interfaces
-from vyos import ConfigError
from vyos.config import Config
from vyos.configdict import list_diff
from vyos.ifconfig import WireGuardIf
+from vyos.util import chown, is_bridge_member, chmod_750
+from vyos.util import call
+from vyos import ConfigError
kdir = r'/config/auth/wireguard'
+default_config_data = {
+ 'intfc': '',
+ 'address': [],
+ 'address_remove': [],
+ 'description': '',
+ 'lport': None,
+ 'deleted': False,
+ 'disable': False,
+ 'fwmark': 0x00,
+ 'mtu': 1420,
+ 'peer': [],
+ 'peer_remove': [], # stores public keys of peers to remove
+ 'pk': f'{kdir}/default/private.key',
+ 'vrf': ''
+}
def _check_kmod():
- if not os.path.exists('/sys/module/wireguard'):
- if os.system('sudo modprobe wireguard') != 0:
- raise ConfigError("modprobe wireguard failed")
+ modules = ['wireguard']
+ for module in modules:
+ if not os.path.exists(f'/sys/module/{module}'):
+ if call(f'modprobe {module}') != 0:
+ raise ConfigError(f'Loading Kernel module {module} failed')
def _migrate_default_keys():
- if os.path.exists('{}/private.key'.format(kdir)) and not os.path.exists('{}/default/private.key'.format(kdir)):
- old_umask = os.umask(0o027)
- location = '{}/default'.format(kdir)
- subprocess.call(['sudo mkdir -p ' + location], shell=True)
- subprocess.call(['sudo chgrp vyattacfg ' + location], shell=True)
- subprocess.call(['sudo chmod 750 ' + location], shell=True)
- os.rename('{}/private.key'.format(kdir),
- '{}/private.key'.format(location))
- os.rename('{}/public.key'.format(kdir),
- '{}/public.key'.format(location))
- os.umask(old_umask)
+ if os.path.exists(f'{kdir}/private.key') and not os.path.exists(f'{kdir}/default/private.key'):
+ location = f'{kdir}/default'
+ if not os.path.exists(location):
+ os.makedirs(location)
+
+ chown(location, 'root', 'vyattacfg')
+ chmod_750(location)
+ os.rename(f'{kdir}/private.key', f'{location}/private.key')
+ os.rename(f'{kdir}/public.key', f'{location}/public.key')
def get_config():
- c = Config()
- if not c.exists(['interfaces', 'wireguard']):
- return None
+ conf = Config()
+ base = ['interfaces', 'wireguard']
+
+ # determine tagNode instance
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ wg = deepcopy(default_config_data)
+ wg['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+
+ # Check if interface has been removed
+ if not conf.exists(base + [wg['intf']]):
+ wg['deleted'] = True
+ return wg
+
+ conf.set_level(base + [wg['intf']])
+
+ # retrieve configured interface addresses
+ if conf.exists(['address']):
+ wg['address'] = conf.return_values(['address'])
+
+ # get interface addresses (currently effective) - to determine which
+ # address is no longer valid and needs to be removed
+ eff_addr = conf.return_effective_values(['address'])
+ wg['address_remove'] = list_diff(eff_addr, wg['address'])
+
+ # retrieve interface description
+ if conf.exists(['description']):
+ wg['description'] = conf.return_value(['description'])
+
+ # disable interface
+ if conf.exists(['disable']):
+ wg['disable'] = True
+
+ # local port to listen on
+ if conf.exists(['port']):
+ wg['lport'] = conf.return_value(['port'])
+
+ # fwmark value
+ if conf.exists(['fwmark']):
+ wg['fwmark'] = int(conf.return_value(['fwmark']))
+
+ # Maximum Transmission Unit (MTU)
+ if conf.exists('mtu'):
+ wg['mtu'] = int(conf.return_value(['mtu']))
+
+ # retrieve VRF instance
+ if conf.exists('vrf'):
+ wg['vrf'] = conf.return_value('vrf')
+
+ # private key
+ if conf.exists(['private-key']):
+ wg['pk'] = "{0}/{1}/private.key".format(
+ kdir, conf.return_value(['private-key']))
+
+ # peer removal, wg identifies peers by its pubkey
+ peer_eff = conf.list_effective_nodes(['peer'])
+ peer_rem = list_diff(peer_eff, conf.list_nodes(['peer']))
+ for peer in peer_rem:
+ wg['peer_remove'].append(
+ conf.return_effective_value(['peer', peer, 'pubkey']))
+
+ # peer settings
+ if conf.exists(['peer']):
+ for p in conf.list_nodes(['peer']):
+ # set new config level for this peer
+ conf.set_level(base + [wg['intf'], 'peer', p])
+ peer = {
+ 'allowed-ips': [],
+ 'address': '',
+ 'name': p,
+ 'persistent_keepalive': '',
+ 'port': '',
+ 'psk': '',
+ 'pubkey': ''
+ }
+
+ # peer allowed-ips
+ if conf.exists(['allowed-ips']):
+ peer['allowed-ips'] = conf.return_values(['allowed-ips'])
+
+ # peer address
+ if conf.exists(['address']):
+ peer['address'] = conf.return_value(['address'])
+
+ # peer port
+ if conf.exists(['port']):
+ peer['port'] = conf.return_value(['port'])
+
+ # persistent-keepalive
+ if conf.exists(['persistent-keepalive']):
+ peer['persistent_keepalive'] = conf.return_value(['persistent-keepalive'])
+
+ # preshared-key
+ if conf.exists(['preshared-key']):
+ peer['psk'] = conf.return_value(['preshared-key'])
+
+ # peer pubkeys
+ if conf.exists(['pubkey']):
+ key_eff = conf.return_effective_value(['pubkey'])
+ key_cfg = conf.return_value(['pubkey'])
+ peer['pubkey'] = key_cfg
+
+ # on a pubkey change we need to remove the pubkey first
+ # peers are identified by pubkey, so key update means
+ # peer removal and re-add
+ if key_eff != key_cfg and key_eff != None:
+ wg['peer_remove'].append(key_cfg)
+
+ # if a peer is disabled, we have to exec a remove for it's pubkey
+ if conf.exists(['disable']):
+ wg['peer_remove'].append(peer['pubkey'])
+ else:
+ wg['peer'].append(peer)
- dflt_cnf = {
- 'intfc': '',
- 'addr': [],
- 'addr_remove': [],
- 'descr': '',
- 'lport': None,
- 'delete': False,
- 'state': 'up',
- 'fwmark': 0x00,
- 'mtu': 1420,
- 'peer': {},
- 'peer_remove': [],
- 'pk': '{}/default/private.key'.format(kdir)
- }
-
- if os.getenv('VYOS_TAGNODE_VALUE'):
- ifname = str(os.environ['VYOS_TAGNODE_VALUE'])
- wg = deepcopy(dflt_cnf)
- wg['intfc'] = ifname
- wg['descr'] = ifname
- else:
- print("ERROR: VYOS_TAGNODE_VALUE undefined")
- sys.exit(1)
-
- c.set_level(['interfaces', 'wireguard'])
-
- # interface removal state
- if not c.exists(ifname) and c.exists_effective(ifname):
- wg['delete'] = True
-
- if not wg['delete']:
- c.set_level(['interfaces', 'wireguard', ifname])
- if c.exists(['address']):
- wg['addr'] = c.return_values(['address'])
-
- # determine addresses which need to be removed
- eff_addr = c.return_effective_values(['address'])
- wg['addr_remove'] = list_diff(eff_addr, wg['addr'])
-
- # ifalias description
- if c.exists(['description']):
- wg['descr'] = c.return_value(['description'])
-
- # link state
- if c.exists(['disable']):
- wg['state'] = 'down'
-
- # local port to listen on
- if c.exists(['port']):
- wg['lport'] = c.return_value(['port'])
-
- # fwmark value
- if c.exists(['fwmark']):
- wg['fwmark'] = c.return_value(['fwmark'])
-
- # mtu
- if c.exists('mtu'):
- wg['mtu'] = c.return_value('mtu')
-
- # private key
- if c.exists(['private-key']):
- wg['pk'] = "{0}/{1}/private.key".format(
- kdir, c.return_value(['private-key']))
-
- # peer removal, wg identifies peers by its pubkey
- peer_eff = c.list_effective_nodes(['peer'])
- peer_rem = list_diff(peer_eff, c.list_nodes(['peer']))
- for p in peer_rem:
- wg['peer_remove'].append(
- c.return_effective_value(['peer', p, 'pubkey']))
-
- # peer settings
- if c.exists(['peer']):
- for p in c.list_nodes(['peer']):
- if not c.exists(['peer', p, 'disable']):
- wg['peer'].update(
- {
- p: {
- 'allowed-ips': [],
- 'endpoint': '',
- 'pubkey': ''
- }
- }
- )
- # peer allowed-ips
- 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'])
- # persistent-keepalive
- if c.exists(['peer', p, 'persistent-keepalive']):
- wg['peer'][p]['persistent-keepalive'] = c.return_value(
- ['peer', p, 'persistent-keepalive'])
- # preshared-key
- if c.exists(['peer', p, 'preshared-key']):
- wg['peer'][p]['psk'] = c.return_value(
- ['peer', p, 'preshared-key'])
- # peer pubkeys
- key_eff = c.return_effective_value(['peer', p, 'pubkey'])
- key_cfg = c.return_value(['peer', p, 'pubkey'])
- wg['peer'][p]['pubkey'] = key_cfg
-
- # on a pubkey change we need to remove the pubkey first
- # peers are identified by pubkey, so key update means
- # peer removal and re-add
- if key_eff != key_cfg and key_eff != None:
- wg['peer_remove'].append(key_cfg)
-
- # if a peer is disabled, we have to exec a remove for it's pubkey
- else:
- peer_key = c.return_value(['peer', p, 'pubkey'])
- wg['peer_remove'].append(peer_key)
return wg
-def verify(c):
- if not c:
- return None
+def verify(wg):
+ interface = wg['intf']
- if not os.path.exists(c['pk']):
- raise ConfigError(
- "No keys found, generate them by executing: \'run generate wireguard [keypair|named-keypairs]\'")
-
- if not c['delete']:
- if not c['addr']:
- raise ConfigError("ERROR: IP address required")
- if not c['peer']:
- raise ConfigError("ERROR: peer required")
- for p in c['peer']:
- if not c['peer'][p]['allowed-ips']:
- raise ConfigError("ERROR: allowed-ips required for peer " + p)
- if not c['peer'][p]['pubkey']:
- raise ConfigError("peer pubkey required for peer " + p)
-
-
-def apply(c):
- # no wg configs left, remove all interface from system
- # maybe move it into ifconfig.py
- if not c:
- net_devs = os.listdir('/sys/class/net/')
- for dev in net_devs:
- if os.path.isdir('/sys/class/net/' + dev):
- buf = open('/sys/class/net/' + dev + '/uevent', 'r').read()
- if re.search("DEVTYPE=wireguard", buf, re.I | re.M):
- wg_intf = re.sub("INTERFACE=", "", re.search(
- "INTERFACE=.*", buf, re.I | re.M).group(0))
- subprocess.call(
- ['ip l d dev ' + wg_intf + ' >/dev/null'], shell=True)
+ if wg['deleted']:
+ is_member, bridge = is_bridge_member(interface)
+ if is_member:
+ # can not use a f'' formatted-string here as bridge would not get
+ # expanded in the print statement
+ raise ConfigError('Can not delete interface "{0}" as it ' \
+ 'is a member of bridge "{1}"!'.format(interface, bridge))
return None
+ vrf_name = wg['vrf']
+ if vrf_name and vrf_name not in interfaces():
+ raise ConfigError(f'VRF "{vrf_name}" does not exist')
+
+ if not os.path.exists(wg['pk']):
+ raise ConfigError('No keys found, generate them by executing:\n' \
+ '"run generate wireguard [keypair|named-keypairs]"')
+
+ if not wg['address']:
+ raise ConfigError(f'IP address required for interface "{interface}"!')
+
+ if not wg['peer']:
+ raise ConfigError(f'Peer required for interface "{interface}"!')
+
+ # run checks on individual configured WireGuard peer
+ for peer in wg['peer']:
+ peer_name = peer['name']
+ if not peer['allowed-ips']:
+ raise ConfigError(f'Peer allowed-ips required for peer "{peer_name}"!')
+
+ if not peer['pubkey']:
+ raise ConfigError(f'Peer public-key required for peer "{peer_name}"!')
+
+
+def apply(wg):
# init wg class
- intfc = WireGuardIf(c['intfc'])
+ w = WireGuardIf(wg['intf'])
# single interface removal
- if c['delete']:
- intfc.remove()
+ if wg['deleted']:
+ w.remove()
return None
- # remove IP addresses
- for ip in c['addr_remove']:
- intfc.del_addr(ip)
+ # Configure interface address(es)
+ # - not longer required addresses get removed first
+ # - newly addresses will be added second
+ for addr in wg['address_remove']:
+ w.del_addr(addr)
+ for addr in wg['address']:
+ w.add_addr(addr)
- # add IP addresses
- for ip in c['addr']:
- intfc.add_addr(ip)
+ # Maximum Transmission Unit (MTU)
+ w.set_mtu(wg['mtu'])
- # interface mtu
- intfc.set_mtu(int(c['mtu']))
+ # update interface description used e.g. within SNMP
+ w.set_alias(wg['description'])
- # ifalias for snmp from description
- intfc.set_alias(str(c['descr']))
+ # assign/remove VRF
+ w.set_vrf(wg['vrf'])
# remove peers
- if c['peer_remove']:
- for pkey in c['peer_remove']:
- intfc.remove_peer(pkey)
+ for pub_key in wg['peer_remove']:
+ w.remove_peer(pub_key)
# peer pubkey
# setting up the wg interface
- intfc.config['private-key'] = c['pk']
- for p in c['peer']:
+ w.config['private-key'] = c['pk']
+
+ for peer in wg['peer']:
# peer pubkey
- intfc.config['pubkey'] = str(c['peer'][p]['pubkey'])
+ w.config['pubkey'] = peer['pubkey']
# peer allowed-ips
- intfc.config['allowed-ips'] = c['peer'][p]['allowed-ips']
+ w.config['allowed-ips'] = peer['allowed-ips']
# local listen port
- if c['lport']:
- intfc.config['port'] = c['lport']
+ if wg['lport']:
+ w.config['port'] = wg['lport']
# fwmark
if c['fwmark']:
- intfc.config['fwmark'] = c['fwmark']
+ w.config['fwmark'] = wg['fwmark']
+
# endpoint
- if c['peer'][p]['endpoint']:
- intfc.config['endpoint'] = c['peer'][p]['endpoint']
+ if peer['address'] and peer['port']:
+ w.config['endpoint'] = '{}:{}'.format(
+ peer['address'], peer['port'])
# persistent-keepalive
- if 'persistent-keepalive' in c['peer'][p]:
- intfc.config['keepalive'] = c['peer'][p]['persistent-keepalive']
+ if peer['persistent_keepalive']:
+ w.config['keepalive'] = peer['persistent_keepalive']
# maybe move it into ifconfig.py
# preshared-key - needs to be read from a file
- if 'psk' in c['peer'][p]:
+ if peer['psk']:
psk_file = '/config/auth/wireguard/psk'
- old_umask = os.umask(0o077)
- open(psk_file, 'w').write(str(c['peer'][p]['psk']))
- os.umask(old_umask)
- intfc.config['psk'] = psk_file
- intfc.update()
+ with open(psk_file, 'w') as f:
+ f.write(peer['psk'])
+ w.config['psk'] = psk_file
- # interface state
- intfc.set_state(c['state'])
+ w.update()
+
+ # Enable/Disable interface
+ if wg['disable']:
+ w.set_admin_state('down')
+ else:
+ w.set_admin_state('up')
return None
@@ -282,4 +303,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py
index 098aa8d97..138f27755 100755
--- a/src/conf_mode/interfaces-wireless.py
+++ b/src/conf_mode/interfaces-wireless.py
@@ -15,756 +15,26 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
+from sys import exit
+from re import findall
-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 jinja2 import FileSystemLoader, Environment
-from subprocess import Popen, PIPE
-from psutil import pid_exists
+from netifaces import interfaces
+from netaddr import EUI, mac_unix_expanded
-from vyos.ifconfig import EthernetIf
-from vyos.ifconfig_vlan import apply_vlan_config
-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, chmod_x, chown, run, is_bridge_member
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
-
-#
-# What about bridge?
-# bridge=br0
-# wds_sta=1
-#
-
-# 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 }}
-{% endif %}
-
-{% for radius in sec_wpa_radius -%}
-# 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 %}
-{% 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 }}"
-{% endif %}
-}
-
-"""
-
default_config_data = {
'address': [],
'address_remove': [],
@@ -792,7 +62,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' : [],
@@ -820,6 +90,10 @@ default_config_data = {
'ip_enable_arp_accept': 0,
'ip_enable_arp_announce': 0,
'ip_enable_arp_ignore': 0,
+ 'ipv6_autoconf': 0,
+ 'ipv6_eui64_prefix': '',
+ 'ipv6_forwarding': 1,
+ 'ipv6_dup_addr_detect': 1,
'mac' : '',
'max_stations' : '',
'mgmt_frame_protection' : 'disabled',
@@ -834,9 +108,10 @@ default_config_data = {
'sec_wpa_passphrase' : '',
'sec_wpa_radius' : [],
'ssid' : '',
- 'type' : 'monitor',
+ 'op_mode' : 'monitor',
'vif': [],
- 'vif_remove': []
+ 'vif_remove': [],
+ 'vrf': ''
}
def get_conf_file(conf_type, intf):
@@ -845,11 +120,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(cfg_dir, user, group)
cfg_file = cfg_dir + r'/{}.cfg'.format(intf)
return cfg_file
@@ -860,11 +132,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(cfg_dir, user, group)
cfg_file = cfg_dir + r'/{}.pid'.format(intf)
return cfg_file
@@ -876,28 +145,22 @@ 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(cfg_dir, user, group)
cfg_file = cfg_dir + r'/{}.cfg'.format(intf)
return cfg_file
-def subprocess_cmd(command):
- p = Popen(command, stdout=PIPE, shell=True)
- p.communicate()
def get_config():
wifi = deepcopy(default_config_data)
conf = Config()
# determine tagNode instance
- try:
- wifi['intf'] = os.environ['VYOS_TAGNODE_VALUE']
- except KeyError as E:
- print("Interface not specified")
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ wifi['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# check if wireless interface has been removed
cfg_base = 'interfaces wireless ' + wifi['intf']
@@ -1031,7 +294,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'):
@@ -1128,10 +391,30 @@ def get_config():
if conf.exists('ip enable-arp-announce'):
wifi['ip_enable_arp_announce'] = 1
+ # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
+ if conf.exists('ipv6 address autoconf'):
+ wifi['ipv6_autoconf'] = 1
+
+ # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ if conf.exists('ipv6 address eui64'):
+ wifi['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+
# ARP enable ignore
if conf.exists('ip enable-arp-ignore'):
wifi['ip_enable_arp_ignore'] = 1
+ # Disable IPv6 forwarding on this interface
+ if conf.exists('ipv6 disable-forwarding'):
+ wifi['ipv6_forwarding'] = 0
+
+ # IPv6 Duplicate Address Detection (DAD) tries
+ if conf.exists('ipv6 dup-addr-detect-transmits'):
+ wifi['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+
+ # Wireless physical device
+ if conf.exists('physical-device'):
+ wifi['phy'] = conf.return_value('physical-device')
+
# Media Access Control (MAC) address
if conf.exists('mac'):
wifi['mac'] = conf.return_value('mac')
@@ -1148,9 +431,9 @@ def get_config():
if conf.exists('mode'):
wifi['mode'] = conf.return_value('mode')
- # Wireless physical device
- if conf.exists('phy'):
- wifi['phy'] = conf.return_value('phy')
+ # retrieve VRF instance
+ if conf.exists('vrf'):
+ wifi['vrf'] = conf.return_value('vrf')
# Transmission power reduction in dBm
if conf.exists('reduce-transmit-power'):
@@ -1204,6 +487,7 @@ def get_config():
radius = {
'server' : server,
'acc_port' : '',
+ 'disabled': False,
'port' : 1812,
'key' : ''
}
@@ -1216,6 +500,10 @@ def get_config():
if conf.exists('accounting'):
radius['acc_port'] = radius['port'] + 1
+ # Check if RADIUS server was temporary disabled
+ if conf.exists(['disable']):
+ radius['disabled'] = True
+
# RADIUS server shared-secret
if conf.exists('key'):
radius['key'] = conf.return_value('key')
@@ -1232,7 +520,11 @@ def get_config():
# Wireless device type for this interface
if conf.exists('type'):
- wifi['type'] = conf.return_value('type')
+ tmp = conf.return_value('type')
+ if tmp == 'access-point':
+ tmp = 'ap'
+
+ wifi['op_mode'] = tmp
# re-set configuration level to parse new nodes
conf.set_level(cfg_base)
@@ -1248,6 +540,9 @@ def get_config():
conf.set_level(cfg_base + ' vif ' + vif)
wifi['vif'].append(vlan_to_dict(conf))
+ # disable interface
+ if conf.exists('disable'):
+ wifi['disable'] = True
# retrieve configured regulatory domain
conf.set_level('system')
@@ -1259,12 +554,23 @@ def get_config():
def verify(wifi):
if wifi['deleted']:
+ interface = wifi['intf']
+ is_member, bridge = is_bridge_member(interface)
+ if is_member:
+ # can not use a f'' formatted-string here as bridge would not get
+ # expanded in the print statement
+ raise ConfigError('Can not delete interface "{0}" as it ' \
+ 'is a member of bridge "{1}"!'.format(interface, bridge))
return None
- if wifi['type'] != 'monitor' and not wifi['ssid']:
+
+ if wifi['op_mode'] != 'monitor' and not wifi['ssid']:
raise ConfigError('SSID must be set for {}'.format(wifi['intf']))
- if wifi['type'] == 'access-point':
+ if not wifi['phy']:
+ raise ConfigError('You must specify physical-device')
+
+ if wifi['op_mode'] == 'ap':
c = Config()
if not c.exists('system wifi-regulatory-domain'):
raise ConfigError('Wireless regulatory domain is mandatory,\n' \
@@ -1273,7 +579,6 @@ def verify(wifi):
if not wifi['channel']:
raise ConfigError('Channel must be set for {}'.format(wifi['intf']))
-
if len(wifi['sec_wep_key']) > 4:
raise ConfigError('No more then 4 WEP keys configurable')
@@ -1283,6 +588,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')
@@ -1293,35 +602,55 @@ def verify(wifi):
if not radius['key']:
raise ConfigError('Misssing RADIUS shared secret key for server: {}'.format(radius['server']))
+ vrf_name = wifi['vrf']
+ if vrf_name and vrf_name not in interfaces():
+ raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ # use common function to verify VLAN configuration
+ verify_vlan_config(wifi)
+
+ conf = Config()
+ # Only one wireless interface per phy can be in station mode
+ base = ['interfaces', 'wireless']
+ for phy in os.listdir('/sys/class/ieee80211'):
+ stations = []
+ for wlan in conf.list_nodes(base):
+ # the following node is mandatory
+ if conf.exists(base + [wlan, 'physical-device', phy]):
+ tmp = conf.return_value(base + [wlan, 'type'])
+ if tmp == 'station':
+ stations.append(wlan)
+
+ if len(stations) > 1:
+ raise ConfigError('Only one station per wireless physical interface possible!')
return None
def generate(wifi):
- pid = 0
+ # 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 os.path.isfile(pidfile):
- pid = 0
- with open(pidfile, 'r') as f:
- pid = int(f.read())
-
- if pid_exists(pid):
- cmd = 'start-stop-daemon --stop --quiet'
- cmd += ' --pidfile ' + pidfile
- subprocess_cmd(cmd)
+ if process_running(pidfile):
+ command = 'start-stop-daemon'
+ command += ' --stop '
+ command += ' --quiet'
+ command += ' --oknodo'
+ command += ' --pidfile ' + pidfile
+ run(command)
# always stop wpa_supplicant service first before reconfiguring it
pidfile = get_pid('wpa_supplicant', wifi['intf'])
- if os.path.isfile(pidfile):
- pid = 0
- with open(pidfile, 'r') as f:
- pid = int(f.read())
-
- if pid_exists(pid):
- cmd = 'start-stop-daemon --stop --quiet'
- cmd += ' --pidfile ' + pidfile
- subprocess_cmd(cmd)
+ if process_running(pidfile):
+ command = 'start-stop-daemon'
+ command += ' --stop '
+ command += ' --quiet'
+ command += ' --oknodo'
+ command += ' --pidfile ' + pidfile
+ run(command)
# Delete config files if interface is removed
if wifi['deleted']:
@@ -1333,15 +662,37 @@ def generate(wifi):
return None
+ if not wifi['mac']:
+ # http://wiki.stocksy.co.uk/wiki/Multiple_SSIDs_with_hostapd
+ # generate locally administered MAC address from used phy interface
+ with open('/sys/class/ieee80211/{}/addresses'.format(wifi['phy']), 'r') as f:
+ # some PHYs tend to have multiple interfaces and thus supply multiple MAC
+ # addresses - we only need the first one for our calculation
+ tmp = f.readline().rstrip()
+ tmp = EUI(tmp).value
+ # mask last nibble from the MAC address
+ tmp &= 0xfffffffffff0
+ # set locally administered bit in MAC address
+ tmp |= 0x020000000000
+ # we now need to add an offset to our MAC address indicating this
+ # subinterfaces index
+ tmp += int(findall(r'\d+', wifi['intf'])[0])
+
+ # convert integer to "real" MAC address representation
+ mac = EUI(hex(tmp).split('x')[-1])
+ # change dialect to use : as delimiter instead of -
+ mac.dialect = mac_unix_expanded
+ wifi['mac'] = str(mac)
+
# render appropriate new config files depending on access-point or station mode
- if wifi['type'] == 'access-point':
- tmpl = Template(config_hostapd_tmpl)
+ if wifi['op_mode'] == 'ap':
+ tmpl = env.get_template('hostapd.conf.tmpl')
config_text = tmpl.render(wifi)
with open(get_conf_file('hostapd', wifi['intf']), 'w') as f:
f.write(config_text)
- elif wifi['type'] == 'station':
- tmpl = Template(config_wpa_suppl_tmpl)
+ elif wifi['op_mode'] == 'station':
+ tmpl = env.get_template('wpa_supplicant.conf.tmpl')
config_text = tmpl.render(wifi)
with open(get_conf_file('wpa_supplicant', wifi['intf']), 'w') as f:
f.write(config_text)
@@ -1349,13 +700,24 @@ def generate(wifi):
return None
def apply(wifi):
- w = EthernetIf(wifi['intf'])
if wifi['deleted']:
+ w = WiFiIf(wifi['intf'])
# delete interface
w.remove()
else:
- # Some parts e.g. MAC address can't be changed when interface is up
- w.set_state('down')
+ # WiFi interface needs to be created on-block (e.g. mode or physical
+ # interface) instead of passing a ton of arguments, I just use a dict
+ # that is managed by vyos.ifconfig
+ conf = deepcopy(WiFiIf.get_config())
+
+ # Assign WiFi instance configuration parameters to config dict
+ conf['phy'] = wifi['phy']
+
+ # Finally create the new interface
+ w = WiFiIf(wifi['intf'], **conf)
+
+ # assign/remove VRF
+ w.set_vrf(wifi['vrf'])
# update interface description used e.g. within SNMP
w.set_alias(wifi['description'])
@@ -1394,7 +756,7 @@ def apply(wifi):
# if custom mac is removed
if wifi['mac']:
w.set_mac(wifi['mac'])
- else:
+ elif wifi['hw_id']:
w.set_mac(wifi['hw_id'])
# configure ARP filter configuration
@@ -1405,10 +767,14 @@ def apply(wifi):
w.set_arp_announce(wifi['ip_enable_arp_announce'])
# configure ARP ignore
w.set_arp_ignore(wifi['ip_enable_arp_ignore'])
-
- # enable interface
- if not wifi['disable']:
- w.set_state('up')
+ # IPv6 address autoconfiguration
+ w.set_ipv6_autoconf(wifi['ipv6_autoconf'])
+ # IPv6 EUI-based address
+ w.set_ipv6_eui64_address(wifi['ipv6_eui64_prefix'])
+ # IPv6 forwarding
+ w.set_ipv6_forwarding(wifi['ipv6_forwarding'])
+ # IPv6 Duplicate Address Detection (DAD) tries
+ w.set_ipv6_dad_messages(wifi['ipv6_dup_addr_detect'])
# Configure interface address(es)
# - not longer required addresses get removed first
@@ -1437,30 +803,46 @@ def apply(wifi):
vlan = e.add_vlan(vif['id'])
apply_vlan_config(vlan, vif)
- # Physical interface is now configured. Proceed by starting hostapd or
- # wpa_supplicant daemon. When type is monitor we can just skip this.
- if wifi['type'] == 'access-point':
- cmd = 'start-stop-daemon --start --quiet'
- cmd += ' --exec /usr/sbin/hostapd'
- # now pass arguments to hostapd binary
- cmd += ' -- -B'
- cmd += ' -P {}'.format(get_pid('hostapd', wifi['intf']))
- cmd += ' {}'.format(get_conf_file('hostapd', wifi['intf']))
-
- # execute assembled command
- subprocess_cmd(cmd)
-
- elif wifi['type'] == 'station':
- cmd = 'start-stop-daemon --start --quiet'
- cmd += ' --exec /sbin/wpa_supplicant'
- # now pass arguments to hostapd binary
- cmd += ' -- -s -B -D nl80211'
- cmd += ' -P {}'.format(get_pid('wpa_supplicant', wifi['intf']))
- cmd += ' -i {}'.format(wifi['intf'])
- cmd += ' -c {}'.format(get_conf_file('wpa_supplicant', wifi['intf']))
-
- # execute assembled command
- subprocess_cmd(cmd)
+ # Enable/Disable interface - interface is always placed in
+ # administrative down state in WiFiIf class
+ if not wifi['disable']:
+ w.set_admin_state('up')
+
+ # Physical interface is now configured. Proceed by starting hostapd or
+ # wpa_supplicant daemon. When type is monitor we can just skip this.
+ if wifi['op_mode'] == 'ap':
+ command = 'start-stop-daemon'
+ command += ' --start '
+ command += ' --quiet'
+ command += ' --oknodo'
+ command += ' --pidfile ' + get_pid('hostapd', wifi['intf'])
+ command += ' --exec /usr/sbin/hostapd'
+ # now pass arguments to hostapd binary
+ command += ' -- '
+ command += ' -B'
+ command += ' -P ' + get_pid('hostapd', wifi['intf'])
+ command += ' ' + get_conf_file('hostapd', wifi['intf'])
+
+ # execute assembled command
+ run(command)
+
+ elif wifi['op_mode'] == 'station':
+ command = 'start-stop-daemon'
+ command += ' --start '
+ command += ' --quiet'
+ command += ' --oknodo'
+ command += ' --pidfile ' + get_pid('hostapd', wifi['intf'])
+ command += ' --exec /sbin/wpa_supplicant'
+ # now pass arguments to hostapd binary
+ command += ' -- '
+ command += ' -s -B -D nl80211'
+ command += ' -P ' + get_pid('wpa_supplicant', wifi['intf'])
+ command += ' -i ' + wifi['intf']
+ command += ' -c ' + \
+ get_conf_file('wpa_supplicant', wifi['intf'])
+
+ # execute assembled command
+ run(command)
return None
diff --git a/src/conf_mode/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py
new file mode 100755
index 000000000..c44a993c4
--- /dev/null
+++ b/src/conf_mode/interfaces-wirelessmodem.py
@@ -0,0 +1,237 @@
+#!/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/>.
+
+import os
+
+from sys import exit
+from copy import deepcopy
+from jinja2 import FileSystemLoader, Environment
+from netifaces import interfaces
+
+from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
+from vyos.util import chown, chmod_x, is_bridge_member
+from vyos.util import cmd
+from vyos.util import call
+from vyos import ConfigError
+
+default_config_data = {
+ 'address': [],
+ 'apn': '',
+ 'chat_script': '',
+ 'deleted': False,
+ 'description': '',
+ 'device': 'ttyUSB0',
+ 'disable': False,
+ 'disable_link_detect': 1,
+ 'on_demand': False,
+ 'logfile': '',
+ 'metric': '10',
+ 'mtu': '1500',
+ 'name_server': True,
+ 'intf': '',
+ 'vrf': ''
+}
+
+def check_kmod():
+ modules = ['option', 'usb_wwan', 'usbserial']
+ for module in modules:
+ if not os.path.exists(f'/sys/module/{module}'):
+ if call(f'modprobe {module}') != 0:
+ raise ConfigError(f'Loading Kernel module {module} failed')
+
+def get_config():
+ wwan = deepcopy(default_config_data)
+ conf = Config()
+
+ # determine tagNode instance
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ wwan['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ wwan['logfile'] = f"/var/log/vyatta/ppp_{wwan['intf']}.log"
+ wwan['chat_script'] = f"/etc/ppp/peers/chat.{wwan['intf']}"
+
+ # Check if interface has been removed
+ if not conf.exists('interfaces wirelessmodem ' + wwan['intf']):
+ wwan['deleted'] = True
+ return wwan
+
+ # set new configuration level
+ conf.set_level('interfaces wirelessmodem ' + wwan['intf'])
+
+ # get metrick for backup default route
+ if conf.exists(['apn']):
+ wwan['apn'] = conf.return_value(['apn'])
+
+ # get metrick for backup default route
+ if conf.exists(['backup', 'distance']):
+ wwan['metric'] = conf.return_value(['backup', 'distance'])
+
+ # Retrieve interface description
+ if conf.exists(['description']):
+ wwan['description'] = conf.return_value(['description'])
+
+ # System device name
+ if conf.exists(['device']):
+ wwan['device'] = conf.return_value(['device'])
+
+ # disable interface
+ if conf.exists('disable'):
+ wwan['disable'] = True
+
+ # ignore link state changes
+ if conf.exists('disable-link-detect'):
+ wwan['disable_link_detect'] = 2
+
+ # Do not use DNS servers provided by the peer
+ if conf.exists(['mtu']):
+ wwan['mtu'] = conf.return_value(['mtu'])
+
+ # Do not use DNS servers provided by the peer
+ if conf.exists(['no-peer-dns']):
+ wwan['name_server'] = False
+
+ # Access concentrator name (only connect to this concentrator)
+ if conf.exists(['ondemand']):
+ wwan['on_demand'] = True
+
+ # retrieve VRF instance
+ if conf.exists('vrf'):
+ wwan['vrf'] = conf.return_value(['vrf'])
+
+ return wwan
+
+def verify(wwan):
+ if wwan['deleted']:
+ interface = wwan['intf']
+ is_member, bridge = is_bridge_member(interface)
+ if is_member:
+ # can not use a f'' formatted-string here as bridge would not get
+ # expanded in the print statement
+ raise ConfigError('Can not delete interface "{0}" as it ' \
+ 'is a member of bridge "{1}"!'.format(interface, bridge))
+ return None
+
+ if not wwan['apn']:
+ raise ConfigError(f"APN for {wwan['intf']} not configured")
+
+ # we can not use isfile() here as Linux device files are no regular files
+ # thus the check will return False
+ if not os.path.exists(f"/dev/{wwan['device']}"):
+ raise ConfigError(f"Device {wwan['device']} does not exist")
+
+ vrf_name = wwan['vrf']
+ if vrf_name and vrf_name not in interfaces():
+ raise ConfigError(f'VRF {vrf_name} does not exist')
+
+ 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_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_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:
+ dirname = os.path.dirname(file)
+ if not os.path.isdir(dirname):
+ os.mkdir(dirname)
+
+ # Always hang-up WWAN connection prior generating new configuration file
+ cmd(f'systemctl stop ppp@{intf}.service')
+
+ if wwan['deleted']:
+ # Delete PPP configuration files
+ for file in config_files:
+ if os.path.exists(file):
+ os.unlink(file)
+
+ else:
+ # Create PPP configuration files
+ tmpl = env.get_template('peer.tmpl')
+ config_text = tmpl.render(wwan)
+ with open(config_wwan, 'w') as f:
+ f.write(config_text)
+
+ # Create PPP chat script
+ tmpl = env.get_template('chat.tmpl')
+ config_text = tmpl.render(wwan)
+ with open(config_wwan_chat, 'w') as f:
+ f.write(config_text)
+
+ # 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(script_wwan_ip_up, 'w') as f:
+ f.write(config_text)
+
+ # Create script for ip-down.d
+ tmpl = env.get_template('ip-down.script.tmpl')
+ config_text = tmpl.render(wwan)
+ with open(script_wwan_ip_down, 'w') as f:
+ f.write(config_text)
+
+ # make generated script file executable
+ chmod_x(script_wwan_pre_up)
+ chmod_x(script_wwan_ip_up)
+ chmod_x(script_wwan_ip_down)
+
+ return None
+
+def apply(wwan):
+ if wwan['deleted']:
+ # bail out early
+ return None
+
+ if not wwan['disable']:
+ # "dial" WWAN connection
+ intf = wwan['intf']
+ cmd(f'systemctl start ppp@{intf}.service')
+ # make logfile owned by root / vyattacfg
+ chown(wwan['logfile'], 'root', 'vyattacfg')
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ check_kmod()
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py
index e80c6caf0..dc04e9131 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,21 +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
-
+from vyos.util import call
ra_conn_name = "remote-access"
charon_conf_file = "/etc/strongswan.d/charon.conf"
@@ -42,55 +39,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 +96,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)
+ call("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):
@@ -194,11 +112,9 @@ def check_cert_file_store(cert_name, file_path, dts_path):
else:
### Cpy file to /etc/ipsec.d/certs/ /etc/ipsec.d/cacerts/
# todo make check
- ret = os.system('cp -f '+file_path+' '+dts_path)
+ ret = call('cp -f '+file_path+' '+dts_path)
if ret:
raise ConfigError("L2TP VPN configuration error: Cannot copy "+file_path)
- else:
- sl.syslog(sl.LOG_NOTICE, file_path + ' copied to '+dts_path)
def verify(data):
# l2tp ipsec check
@@ -231,22 +147,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 +193,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")
+ call('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")
+ call('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 +217,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/le_cert.py b/src/conf_mode/le_cert.py
index c657098e1..4b365a566 100755
--- a/src/conf_mode/le_cert.py
+++ b/src/conf_mode/le_cert.py
@@ -18,11 +18,13 @@
import sys
import os
-import subprocess
import vyos.defaults
from vyos.config import Config
from vyos import ConfigError
+from vyos.util import cmd
+from vyos.util import call
+
vyos_conf_scripts_dir = vyos.defaults.directories['conf_mode']
@@ -45,9 +47,9 @@ def request_certbot(cert):
certbot_cmd = 'certbot certonly -n --nginx --agree-tos --no-eff-email --expand {0} {1}'.format(email_flag, domain_flag)
- completed = subprocess.run(certbot_cmd, shell=True)
-
- return completed.returncode
+ cmd(certbot_cmd,
+ raising=ConfigError,
+ message="The certbot request failed for the specified domains.")
def get_config():
conf = Config()
@@ -84,28 +86,21 @@ def generate(cert):
# certbot will attempt to reload nginx, even with 'certonly';
# start nginx if not active
- ret = os.system('systemctl is-active --quiet nginx.ervice')
+ ret = call('systemctl is-active --quiet nginx.ervice')
if ret:
- os.system('sudo systemctl start nginx.service')
+ call('sudo systemctl start nginx.service')
- ret = request_certbot(cert)
- if ret:
- raise ConfigError("The certbot request failed for the"
- " specified domains.")
+ request_certbot(cert)
def apply(cert):
if cert is not None:
- os.system('sudo systemctl restart certbot.timer')
+ call('sudo systemctl restart certbot.timer')
else:
- os.system('sudo systemctl stop certbot.timer')
+ call('sudo systemctl stop certbot.timer')
return None
for dep in dependencies:
- cmd = '{0}/{1}'.format(vyos_conf_scripts_dir, dep)
- try:
- subprocess.check_call(cmd, shell=True)
- except subprocess.CalledProcessError as err:
- raise ConfigError(str(err))
+ cmd(f'{vyos_conf_scripts_dir}/{dep}', raising=ConfigError)
if __name__ == '__main__':
try:
diff --git a/src/conf_mode/lldp.py b/src/conf_mode/lldp.py
index b72916ab8..ec59c68d0 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,24 @@
# 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
+from vyos.util import call
-# 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 %}"
-
-"""
+config_file = "/etc/default/lldpd"
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 +41,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 +61,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 +95,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 +106,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 +129,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 +148,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 +210,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 +226,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)
@@ -263,10 +241,10 @@ def generate(lldp):
def apply(lldp):
if lldp:
# start/restart lldp service
- os.system('sudo systemctl restart lldpd.service')
+ call('sudo systemctl restart lldpd.service')
else:
# LLDP service has been terminated
- os.system('sudo systemctl stop lldpd.service')
+ call('sudo systemctl stop lldpd.service')
os.unlink(config_file)
os.unlink(vyos_config_file)
@@ -278,5 +256,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..9230aaf61 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,23 +13,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
-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
+from vyos.util import call
-config_file = r'/etc/default/mdns-repeater'
-config_tmpl = """
-### Autogenerated by mdns_repeater.py ###
-DAEMON_ARGS="{{ interfaces | join(' ') }}"
-"""
+config_file = r'/etc/default/mdns-repeater'
default_config_data = {
'disabled': False,
@@ -37,21 +35,22 @@ default_config_data = {
}
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 +68,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 +82,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)
@@ -92,11 +96,11 @@ def generate(mdns):
def apply(mdns):
if (mdns is None) or mdns['disabled']:
- os.system('sudo systemctl stop mdns-repeater')
+ call('sudo systemctl stop mdns-repeater')
if os.path.exists(config_file):
os.unlink(config_file)
else:
- os.system('sudo systemctl restart mdns-repeater')
+ call('sudo systemctl restart mdns-repeater')
return None
@@ -108,4 +112,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 8f32e6e81..75328dfd7 100755
--- a/src/conf_mode/ntp.py
+++ b/src/conf_mode/ntp.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,64 +13,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
-import ipaddress
-import copy
+from copy import deepcopy
+from ipaddress import ip_network
+from jinja2 import FileSystemLoader, Environment
+from sys import exit
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
from vyos import ConfigError
+from vyos.util import call
-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
-
-# Do not listen on any interface address by default
-interface ignore wildcard
-#
-# 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
-{% for a in listen_address -%}
-interface listen {{ a }}
-{% endfor -%}
-{% endif %}
-
-"""
+config_file = r'/etc/ntp.conf'
default_config_data = {
'servers': [],
@@ -79,7 +36,7 @@ default_config_data = {
}
def get_config():
- ntp = copy.deepcopy(default_config_data)
+ ntp = deepcopy(default_config_data)
conf = Config()
if not conf.exists('system ntp'):
return None
@@ -89,7 +46,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,
@@ -131,7 +88,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']))
@@ -143,7 +100,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)
@@ -152,10 +114,10 @@ def generate(ntp):
def apply(ntp):
if ntp is not None:
- os.system('sudo systemctl restart ntp.service')
+ call('sudo systemctl restart ntp.service')
else:
# NTP support is removed in the commit
- os.system('sudo systemctl stop ntp.service')
+ call('sudo systemctl stop ntp.service')
os.unlink(config_file)
return None
@@ -168,4 +130,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..cf4db5f54 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,38 +13,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 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
+from vyos.util import call
-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 -%}
-!
-"""
+config_file = r'/tmp/bfd.frr'
default_config_data = {
'new_peers': [],
@@ -132,7 +115,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 +147,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 +191,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)
+ call("vtysh -d bfdd -f " + config_file)
if os.path.exists(config_file):
os.remove(config_file)
@@ -233,4 +221,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
new file mode 100755
index 000000000..141b1950d
--- /dev/null
+++ b/src/conf_mode/protocols_igmp.py
@@ -0,0 +1,121 @@
+#!/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/>.
+
+import os
+
+from ipaddress import IPv4Address
+from jinja2 import FileSystemLoader, Environment
+from sys import exit
+
+from vyos import ConfigError
+from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
+from vyos.util import call
+
+
+config_file = r'/tmp/igmp.frr'
+
+def get_config():
+ conf = Config()
+ igmp_conf = {
+ 'igmp_conf' : False,
+ 'old_ifaces' : {},
+ 'ifaces' : {}
+ }
+ if not (conf.exists('protocols igmp') or conf.exists_effective('protocols igmp')):
+ return None
+
+ if conf.exists('protocols igmp'):
+ igmp_conf['igmp_conf'] = True
+
+ conf.set_level('protocols igmp')
+
+ # # Get interfaces
+ for iface in conf.list_effective_nodes('interface'):
+ igmp_conf['old_ifaces'].update({
+ 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)),
+ '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)),
+ '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
+
+def verify(igmp):
+ if igmp is None:
+ return None
+
+ if igmp['igmp_conf']:
+ # 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
+
+ # 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)
+
+ return None
+
+def apply(igmp):
+ if igmp is None:
+ return None
+
+ if os.path.exists(config_file):
+ call("sudo vtysh -d pimd -f " + config_file)
+ os.remove(config_file)
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py
new file mode 100755
index 000000000..b5753aea8
--- /dev/null
+++ b/src/conf_mode/protocols_mpls.py
@@ -0,0 +1,178 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from jinja2 import FileSystemLoader, Environment
+
+from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
+from vyos import ConfigError
+from vyos.util import call
+
+
+config_file = r'/tmp/ldpd.frr'
+
+def sysctl(name, value):
+ call('sysctl -wq {}={}'.format(name, value))
+
+def get_config():
+ conf = Config()
+ mpls_conf = {
+ 'router_id' : None,
+ 'mpls_ldp' : False,
+ 'old_ldp' : {
+ 'interfaces' : [],
+ 'neighbors' : {},
+ 'd_transp_ipv4' : None,
+ 'd_transp_ipv6' : None
+ },
+ 'ldp' : {
+ 'interfaces' : [],
+ 'neighbors' : {},
+ 'd_transp_ipv4' : None,
+ 'd_transp_ipv6' : None
+ }
+ }
+ if not (conf.exists('protocols mpls') or conf.exists_effective('protocols mpls')):
+ return None
+
+ if conf.exists('protocols mpls ldp'):
+ mpls_conf['mpls_ldp'] = True
+
+ conf.set_level('protocols mpls ldp')
+
+ # Get router-id
+ if conf.exists_effective('router-id'):
+ mpls_conf['old_router_id'] = conf.return_effective_value('router-id')
+ if conf.exists('router-id'):
+ mpls_conf['router_id'] = conf.return_value('router-id')
+
+ # Get discovery transport-ipv4-address
+ if conf.exists_effective('discovery transport-ipv4-address'):
+ mpls_conf['old_ldp']['d_transp_ipv4'] = conf.return_effective_value('discovery transport-ipv4-address')
+
+ if conf.exists('discovery transport-ipv4-address'):
+ mpls_conf['ldp']['d_transp_ipv4'] = conf.return_value('discovery transport-ipv4-address')
+
+ # Get discovery transport-ipv6-address
+ if conf.exists_effective('discovery transport-ipv6-address'):
+ mpls_conf['old_ldp']['d_transp_ipv6'] = conf.return_effective_value('discovery transport-ipv6-address')
+
+ if conf.exists('discovery transport-ipv6-address'):
+ mpls_conf['ldp']['d_transp_ipv6'] = conf.return_value('discovery transport-ipv6-address')
+
+ # Get interfaces
+ if conf.exists_effective('interface'):
+ mpls_conf['old_ldp']['interfaces'] = conf.return_effective_values('interface')
+
+ if conf.exists('interface'):
+ mpls_conf['ldp']['interfaces'] = conf.return_values('interface')
+
+ # Get neighbors
+ for neighbor in conf.list_effective_nodes('neighbor'):
+ mpls_conf['old_ldp']['neighbors'].update({
+ neighbor : {
+ 'password' : conf.return_effective_value('neighbor {0} password'.format(neighbor))
+ }
+ })
+
+ for neighbor in conf.list_nodes('neighbor'):
+ mpls_conf['ldp']['neighbors'].update({
+ neighbor : {
+ 'password' : conf.return_value('neighbor {0} password'.format(neighbor))
+ }
+ })
+
+ return mpls_conf
+
+def operate_mpls_on_intfc(interfaces, action):
+ rp_filter = 0
+ if action == 1:
+ rp_filter = 2
+ for iface in interfaces:
+ sysctl('net.mpls.conf.{0}.input'.format(iface), action)
+ # Operate rp filter
+ sysctl('net.ipv4.conf.{0}.rp_filter'.format(iface), rp_filter)
+
+def verify(mpls):
+ if mpls is None:
+ return None
+
+ if mpls['mpls_ldp']:
+ # Requre router-id
+ if not mpls['router_id']:
+ raise ConfigError(f"MPLS ldp router-id is mandatory!")
+
+ # Requre discovery transport-address
+ if not mpls['ldp']['d_transp_ipv4'] and not mpls['ldp']['d_transp_ipv6']:
+ raise ConfigError(f"MPLS ldp discovery transport address is mandatory!")
+
+ # Requre interface
+ if not mpls['ldp']['interfaces']:
+ raise ConfigError(f"MPLS ldp interface is mandatory!")
+
+def generate(mpls):
+ if mpls is None:
+ return None
+
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'mpls')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+
+ tmpl = env.get_template('ldpd.frr.tmpl')
+ config_text = tmpl.render(mpls)
+ with open(config_file, 'w') as f:
+ f.write(config_text)
+
+ return None
+
+def apply(mpls):
+ if mpls is None:
+ return None
+
+ # Set number of entries in the platform label table
+ if mpls['mpls_ldp']:
+ sysctl('net.mpls.platform_labels', '1048575')
+ else:
+ sysctl('net.mpls.platform_labels', '0')
+
+ # Do not copy IP TTL to MPLS header
+ sysctl('net.mpls.ip_ttl_propagate', '0')
+
+ # Allow mpls on interfaces
+ operate_mpls_on_intfc(mpls['ldp']['interfaces'], 1)
+
+ # Disable mpls on deleted interfaces
+ diactive_ifaces = set(mpls['old_ldp']['interfaces']).difference(mpls['ldp']['interfaces'])
+ operate_mpls_on_intfc(diactive_ifaces, 0)
+
+ if os.path.exists(config_file):
+ call("sudo vtysh -d ldpd -f " + config_file)
+ os.remove(config_file)
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/protocols_pim.py b/src/conf_mode/protocols_pim.py
new file mode 100755
index 000000000..44fc9293b
--- /dev/null
+++ b/src/conf_mode/protocols_pim.py
@@ -0,0 +1,148 @@
+#!/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/>.
+
+import os
+
+from ipaddress import IPv4Address
+from jinja2 import FileSystemLoader, Environment
+from sys import exit
+
+from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
+from vyos import ConfigError
+from vyos.util import call
+
+
+config_file = r'/tmp/pimd.frr'
+
+def get_config():
+ conf = Config()
+ pim_conf = {
+ 'pim_conf' : False,
+ 'old_pim' : {
+ 'ifaces' : {},
+ 'rp' : {}
+ },
+ 'pim' : {
+ 'ifaces' : {},
+ 'rp' : {}
+ }
+ }
+ if not (conf.exists('protocols pim') or conf.exists_effective('protocols pim')):
+ return None
+
+ if conf.exists('protocols pim'):
+ pim_conf['pim_conf'] = True
+
+ conf.set_level('protocols pim')
+
+ # Get interfaces
+ 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)),
+ '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)),
+ 'dr_prio' : conf.return_value('interface {0} dr-priority'.format(iface)),
+ }
+ })
+
+ conf.set_level('protocols pim rp')
+
+ # Get RPs addresses
+ for rp_addr in conf.list_effective_nodes('address'):
+ pim_conf['old_pim']['rp'][rp_addr] = conf.return_effective_values('address {0} group'.format(rp_addr))
+
+ for rp_addr in conf.list_nodes('address'):
+ pim_conf['pim']['rp'][rp_addr] = conf.return_values('address {0} group'.format(rp_addr))
+
+ # Get RP keep-alive-timer
+ if conf.exists_effective('rp keep-alive-timer'):
+ pim_conf['old_pim']['rp_keep_alive'] = conf.return_effective_value('rp keep-alive-timer')
+ if conf.exists('rp keep-alive-timer'):
+ pim_conf['pim']['rp_keep_alive'] = conf.return_value('rp keep-alive-timer')
+
+ return pim_conf
+
+def verify(pim):
+ if pim is None:
+ return None
+
+ if pim['pim_conf']:
+ # Check interfaces
+ if not pim['pim']['ifaces']:
+ raise ConfigError(f"PIM require defined interfaces!")
+
+ if not pim['pim']['rp']:
+ raise ConfigError(f"RP address required")
+
+ # Check unique multicast groups
+ uniq_groups = []
+ for rp_addr in pim['pim']['rp']:
+ if not pim['pim']['rp'][rp_addr]:
+ raise ConfigError(f"Group should be specified for RP " + rp_addr)
+ for group in pim['pim']['rp'][rp_addr]:
+ if (group in uniq_groups):
+ raise ConfigError(f"Group range " + group + " specified cannot exact match another")
+
+ # Check, is this multicast group
+ gr_addr = group.split('/')
+ if IPv4Address(gr_addr[0]) < IPv4Address('224.0.0.0'):
+ raise ConfigError(group + " not a multicast group")
+
+ uniq_groups.extend(pim['pim']['rp'][rp_addr])
+
+def generate(pim):
+ if pim is None:
+ return None
+
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'pim')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+
+ tmpl = env.get_template('pimd.frr.tmpl')
+ config_text = tmpl.render(pim)
+ with open(config_file, 'w') as f:
+ f.write(config_text)
+
+ return None
+
+def apply(pim):
+ if pim is None:
+ return None
+
+ if os.path.exists(config_file):
+ call("vtysh -d pimd -f " + config_file)
+ os.remove(config_file)
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/salt-minion.py b/src/conf_mode/salt-minion.py
new file mode 100755
index 000000000..bfc3a707e
--- /dev/null
+++ b/src/conf_mode/salt-minion.py
@@ -0,0 +1,144 @@
+#!/usr/bin/env python3
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from copy import deepcopy
+from jinja2 import FileSystemLoader, Environment
+from pwd import getpwnam
+from socket import gethostname
+from sys import exit
+from urllib3 import PoolManager
+
+from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
+from vyos import ConfigError
+from vyos.util import call
+
+
+config_file = r'/etc/salt/minion'
+
+default_config_data = {
+ 'hash_type': 'sha256',
+ 'log_file': '/var/log/salt/minion',
+ 'log_level': 'warning',
+ 'master' : 'salt',
+ 'user': 'minion',
+ 'salt_id': gethostname(),
+ 'mine_interval': '60',
+ 'verify_master_pubkey_sign': 'false'
+}
+
+def get_config():
+ salt = deepcopy(default_config_data)
+ conf = Config()
+ if not conf.exists('service salt-minion'):
+ return None
+ else:
+ conf.set_level('service salt-minion')
+
+ if conf.exists('hash_type'):
+ salt['hash_type'] = conf.return_value('hash_type')
+
+ if conf.exists('log_file'):
+ salt['log_file'] = conf.return_value('log_file')
+
+ if conf.exists('log_level'):
+ salt['log_level'] = conf.return_value('log_level')
+
+ if conf.exists('master'):
+ master = conf.return_values('master')
+ salt['master'] = master
+
+ if conf.exists('id'):
+ salt['salt_id'] = conf.return_value('id')
+
+ if conf.exists('user'):
+ salt['user'] = conf.return_value('user')
+
+ if conf.exists('mine_interval'):
+ salt['mine_interval'] = conf.return_value('mine_interval')
+
+ salt['master-key'] = None
+ if conf.exists('master-key'):
+ salt['master-key'] = conf.return_value('master-key')
+ salt['verify_master_pubkey_sign'] = 'true'
+
+ return salt
+
+def generate(salt):
+ paths = ['/etc/salt/','/var/run/salt','/opt/vyatta/etc/config/salt/']
+ directory = '/opt/vyatta/etc/config/salt/pki/minion'
+ 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 = env.get_template('minion.tmpl')
+ config_text = tmpl.render(salt)
+ with open(config_file, 'w') as f:
+ f.write(config_text)
+
+ path = "/etc/salt/"
+ for path in paths:
+ 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)
+
+ 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
+
+def apply(salt):
+ if salt is not None:
+ call("sudo systemctl restart salt-minion")
+ else:
+ # Salt access is removed in the commit
+ call("sudo systemctl stop salt-minion")
+ os.unlink(config_file)
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/service-ipoe.py b/src/conf_mode/service-ipoe.py
index bd7a898d0..5bd4aea2e 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,20 +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 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
+from vyos.util import run
ipoe_cnf_dir = r'/etc/accel-ppp/ipoe'
ipoe_cnf = ipoe_cnf_dir + r'/ipoe.config'
@@ -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,40 +51,21 @@ 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")
break
-def _accel_cmd(cmd=''):
- if not cmd:
- return None
- try:
- ret = subprocess.check_output(
- ['/usr/bin/accel-cmd', '-p', cmd_port, cmd]).decode().strip()
- return ret
- 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')
+def _accel_cmd(command):
+ return run('/usr/bin/accel-cmd -p {cmd_port} {command}')
##### Inline functions end ####
@@ -388,14 +219,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
@@ -457,15 +299,13 @@ def apply(c):
return None
if not os.path.exists(pidfile):
- ret = subprocess.call(
- ['/usr/sbin/accel-pppd', '-c', ipoe_cnf, '-p', pidfile, '-d'])
+ ret = run(f'/usr/sbin/accel-pppd -c {ipoe_cnf} -p {pidfile} -d')
_chk_con()
if ret != 0 and os.path.exists(pidfile):
os.remove(pidfile)
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 +316,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..d3fc82406 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,20 +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 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
+from vyos.util import run
pidfile = r'/var/run/accel_pppoe.pid'
pppoe_cnf_dir = r'/etc/accel-ppp/pppoe'
@@ -38,291 +37,28 @@ 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
- try:
- ret = subprocess.check_output(
- ['/usr/bin/accel-cmd', cmd]).decode().strip()
- return ret
- except:
- return 1
+def _accel_cmd(command):
+ return run(f'/usr/bin/accel-cmd {command}')
def get_config():
@@ -640,6 +376,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 +394,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
@@ -672,15 +419,13 @@ def apply(c):
return None
if not os.path.exists(pidfile):
- ret = subprocess.call(
- ['/usr/sbin/accel-pppd', '-c', pppoe_conf, '-p', pidfile, '-d'])
+ ret = run(f'/usr/sbin/accel-pppd -c {pppoe_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:
_accel_cmd('restart')
- sl.syslog(sl.LOG_NOTICE, "reloading config via daemon restart")
if __name__ == '__main__':
@@ -691,4 +436,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
new file mode 100755
index 000000000..75a324260
--- /dev/null
+++ b/src/conf_mode/service-router-advert.py
@@ -0,0 +1,178 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from jinja2 import FileSystemLoader, Environment
+from stat import S_IRUSR, S_IWUSR, S_IRGRP
+from sys import exit
+
+from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
+from vyos import ConfigError
+from vyos.util import call
+
+
+config_file = r'/etc/radvd.conf'
+
+default_config_data = {
+ 'interfaces': []
+}
+
+def get_config():
+ rtradv = default_config_data
+ conf = Config()
+ base_level = ['service', 'router-advert']
+
+ if not conf.exists(base_level):
+ return rtradv
+
+ for interface in conf.list_nodes(base_level + ['interface']):
+ intf = {
+ 'name': interface,
+ 'hop_limit' : '64',
+ 'default_lifetime': '',
+ 'default_preference': 'medium',
+ 'dnssl': [],
+ 'link_mtu': '',
+ 'managed_flag': 'off',
+ 'interval_max': '600',
+ 'interval_min': '',
+ 'name_server': [],
+ 'other_config_flag': 'off',
+ 'prefixes' : [],
+ 'reachable_time': '0',
+ 'retrans_timer': '0',
+ 'send_advert': 'on'
+ }
+
+ # set config level first to reduce boilerplate code
+ conf.set_level(base_level + ['interface', interface])
+
+ if conf.exists(['hop-limit']):
+ intf['hop_limit'] = conf.return_value(['hop-limit'])
+
+ if conf.exists(['default-lifetim']):
+ intf['default_lifetime'] = conf.return_value(['default-lifetim'])
+
+ if conf.exists(['default-preference']):
+ intf['default_preference'] = conf.return_value(['default-preference'])
+
+ if conf.exists(['dnssl']):
+ intf['dnssl'] = conf.return_values(['dnssl'])
+
+ if conf.exists(['link-mtu']):
+ intf['link_mtu'] = conf.return_value(['link-mtu'])
+
+ if conf.exists(['managed-flag']):
+ intf['managed_flag'] = 'on'
+
+ if conf.exists(['interval', 'max']):
+ intf['interval_max'] = conf.return_value(['interval', 'max'])
+
+ if conf.exists(['interval', 'min']):
+ intf['interval_min'] = conf.return_value(['interval', 'min'])
+
+ if conf.exists(['name-server']):
+ intf['name_server'] = conf.return_values(['name-server'])
+
+ if conf.exists(['other-config-flag']):
+ intf['other_config_flag'] = 'on'
+
+ if conf.exists(['reachable-time']):
+ intf['reachable_time'] = conf.return_value(['reachable-time'])
+
+ if conf.exists(['retrans-timer']):
+ intf['retrans_timer'] = conf.return_value(['retrans-timer'])
+
+ if conf.exists(['no-send-advert']):
+ intf['send_advert'] = 'off'
+
+ for prefix in conf.list_nodes(['prefix']):
+ tmp = {
+ 'prefix' : prefix,
+ 'autonomous_flag' : 'on',
+ 'on_link' : 'on',
+ 'preferred_lifetime': '14400',
+ 'valid_lifetime' : '2592000'
+
+ }
+
+ # set config level first to reduce boilerplate code
+ conf.set_level(base_level + ['interface', interface, 'prefix', prefix])
+
+ if conf.exists(['no-autonomous-flag']):
+ tmp['autonomous_flag'] = 'off'
+
+ if conf.exists(['no-on-link-flag']):
+ tmp['on_link'] = 'off'
+
+ if conf.exists(['preferred-lifetime']):
+ tmp['preferred_lifetime'] = conf.return_value(['preferred-lifetime'])
+
+ if conf.exists(['valid-lifetime']):
+ tmp['valid_lifetime'] = conf.return_value(['valid-lifetime'])
+
+ intf['prefixes'].append(tmp)
+
+ rtradv['interfaces'].append(intf)
+
+ return rtradv
+
+def verify(rtradv):
+ return None
+
+def generate(rtradv):
+ if not rtradv['interfaces']:
+ return None
+
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'router-advert')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader, trim_blocks=True)
+
+ tmpl = env.get_template('radvd.conf.tmpl')
+ config_text = tmpl.render(rtradv)
+ with open(config_file, 'w') as f:
+ f.write(config_text)
+
+ # adjust file permissions of new configuration file
+ if os.path.exists(config_file):
+ os.chmod(config_file, S_IRUSR | S_IWUSR | S_IRGRP)
+
+ return None
+
+def apply(rtradv):
+ if not rtradv['interfaces']:
+ # bail out early - looks like removal from running config
+ call('systemctl stop radvd.service')
+ if os.path.exists(config_file):
+ os.unlink(config_file)
+
+ return None
+
+ call('systemctl restart radvd.service')
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py
index ac94afb1a..4a69e8742 100755
--- a/src/conf_mode/snmp.py
+++ b/src/conf_mode/snmp.py
@@ -15,18 +15,20 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
-import jinja2
-
-import vyos.version
-import vyos.validate
from binascii import hexlify
from time import sleep
from stat import S_IRWXU, S_IXGRP, S_IXOTH, S_IROTH, S_IRGRP
from sys import exit
+from jinja2 import FileSystemLoader, Environment
from vyos.config import Config
+from vyos.defaults import directories as vyos_data_dir
+from vyos.validate import is_ipv4, is_addr_assigned
+from vyos.version import get_version_data
from vyos import ConfigError
+from vyos.util import call
+
config_file_client = r'/etc/snmp/snmp.conf'
config_file_daemon = r'/etc/snmp/snmpd.conf'
@@ -43,171 +45,10 @@ 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,udp6:161{% 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': [],
+ 'ipv6_enabled': 'True',
'communities': [],
'smux_peers': [],
'location' : '',
@@ -237,9 +78,12 @@ def get_config():
if not conf.exists('service snmp'):
return None
else:
+ if conf.exists('system ipv6 disable'):
+ snmp['ipv6_enabled'] = False
+
conf.set_level('service snmp')
- version_data = vyos.version.get_version_data()
+ version_data = get_version_data()
snmp['version'] = version_data['version']
# create an internal snmpv3 user of the form 'vyosxxxxxxxxxxxxxxxx'
@@ -263,7 +107,7 @@ def get_config():
# Subnet of SNMP client(s) allowed to contact system
if conf.exists('community {0} network'.format(name)):
for addr in conf.return_values('community {0} network'.format(name)):
- if vyos.validate.is_ipv4(addr):
+ if is_ipv4(addr):
community['network_v4'].append(addr)
else:
community['network_v6'].append(addr)
@@ -271,7 +115,7 @@ def get_config():
# IP address of SNMP client allowed to contact system
if conf.exists('community {0} client'.format(name)):
for addr in conf.return_values('community {0} client'.format(name)):
- if vyos.validate.is_ipv4(addr):
+ if is_ipv4(addr):
community['network_v4'].append(addr)
else:
community['network_v6'].append(addr)
@@ -554,16 +398,16 @@ def verify(snmp):
addr = listen[0]
port = listen[1]
- if vyos.validate.is_ipv4(addr):
+ if is_ipv4(addr):
# example: udp:127.0.0.1:161
listen = 'udp:' + addr + ':' + port
- else:
+ elif snmp['ipv6_enabled']:
# example: udp6:[::1]:161
listen = 'udp6:' + '[' + addr + ']' + ':' + port
# We only wan't to configure addresses that exist on the system.
# Hint the user if they don't exist
- if vyos.validate.is_addr_assigned(addr):
+ if is_addr_assigned(addr):
snmp['listen_on'].append(listen)
else:
print('WARNING: SNMP listen address {0} not configured!'.format(addr))
@@ -665,35 +509,40 @@ 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)
+ call('systemctl stop snmpd.service')
+ 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)
@@ -705,7 +554,7 @@ def apply(snmp):
return None
# start SNMP daemon
- os.system("systemctl restart snmpd.service")
+ call("systemctl restart snmpd.service")
# Passwords are not available immediately in the configuration file,
# after daemon startup - we wait until they have been processed by
@@ -746,15 +595,15 @@ def apply(snmp):
# Now update the running configuration
#
- # Currently when executing os.system() the environment does not
+ # Currently when executing call() the environment does not
# have the vyos_libexec_dir variable set, see Phabricator T685.
- os.system('/opt/vyatta/sbin/my_set service snmp v3 user "{0}" auth encrypted-key "{1}" > /dev/null'.format(cfg['user'], cfg['auth_pw']))
- os.system('/opt/vyatta/sbin/my_set service snmp v3 user "{0}" privacy encrypted-key "{1}" > /dev/null'.format(cfg['user'], cfg['priv_pw']))
- os.system('/opt/vyatta/sbin/my_delete service snmp v3 user "{0}" auth plaintext-key > /dev/null'.format(cfg['user']))
- os.system('/opt/vyatta/sbin/my_delete service snmp v3 user "{0}" privacy plaintext-key > /dev/null'.format(cfg['user']))
+ call('/opt/vyatta/sbin/my_set service snmp v3 user "{0}" auth encrypted-key "{1}" > /dev/null'.format(cfg['user'], cfg['auth_pw']))
+ call('/opt/vyatta/sbin/my_set service snmp v3 user "{0}" privacy encrypted-key "{1}" > /dev/null'.format(cfg['user'], cfg['priv_pw']))
+ call('/opt/vyatta/sbin/my_delete service snmp v3 user "{0}" auth plaintext-key > /dev/null'.format(cfg['user']))
+ call('/opt/vyatta/sbin/my_delete service snmp v3 user "{0}" privacy plaintext-key > /dev/null'.format(cfg['user']))
# Enable AgentX in FRR
- os.system('vtysh -c "configure terminal" -c "agentx" >/dev/null')
+ call('vtysh -c "configure terminal" -c "agentx" >/dev/null')
return None
diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py
index 9fe22bfee..a6cdb7ccc 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,148 +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
+from vyos.util import call
-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 %}
-"""
+config_file = r'/etc/ssh/sshd_config'
default_config_data = {
'port' : '22',
@@ -250,7 +120,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)
@@ -258,10 +133,10 @@ def generate(ssh):
def apply(ssh):
if ssh is not None and 'port' in ssh.keys():
- os.system("sudo systemctl restart ssh.service")
+ call("sudo systemctl restart ssh.service")
else:
# SSH access is removed in the commit
- os.system("sudo systemctl stop ssh.service")
+ call("sudo systemctl stop ssh.service")
if os.path.isfile(config_file):
os.unlink(config_file)
@@ -275,4 +150,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/system-ip.py b/src/conf_mode/system-ip.py
index 335507411..8a1ac8411 100755
--- a/src/conf_mode/system-ip.py
+++ b/src/conf_mode/system-ip.py
@@ -20,6 +20,8 @@ from sys import exit
from copy import deepcopy
from vyos.config import Config
from vyos import ConfigError
+from vyos.util import call
+
default_config_data = {
'arp_table': 8192,
@@ -29,7 +31,7 @@ default_config_data = {
}
def sysctl(name, value):
- os.system('sysctl -wq {}={}'.format(name, value))
+ call('sysctl -wq {}={}'.format(name, value))
def get_config():
ip_opt = deepcopy(default_config_data)
diff --git a/src/conf_mode/system-ipv6.py b/src/conf_mode/system-ipv6.py
index bd28ec357..04a063564 100755
--- a/src/conf_mode/system-ipv6.py
+++ b/src/conf_mode/system-ipv6.py
@@ -21,6 +21,8 @@ from sys import exit
from copy import deepcopy
from vyos.config import Config
from vyos import ConfigError
+from vyos.util import call
+
ipv6_disable_file = '/etc/modprobe.d/vyos_disable_ipv6.conf'
@@ -35,7 +37,7 @@ default_config_data = {
}
def sysctl(name, value):
- os.system('sysctl -wq {}={}'.format(name, value))
+ call('sysctl -wq {}={}'.format(name, value))
def get_config():
ip_opt = deepcopy(default_config_data)
diff --git a/src/conf_mode/system-login-banner.py b/src/conf_mode/system-login-banner.py
index e66d409bb..5a34a0b06 100755
--- a/src/conf_mode/system-login-banner.py
+++ b/src/conf_mode/system-login-banner.py
@@ -16,6 +16,7 @@
from sys import exit
from vyos.config import Config
+from vyos import ConfigError
motd="""
The programs included with the Debian GNU/Linux system are free software;
@@ -24,6 +25,7 @@ individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
+
"""
PRELOGIN_FILE = r'/etc/issue'
@@ -31,8 +33,8 @@ PRELOGIN_NET_FILE = r'/etc/issue.net'
POSTLOGIN_FILE = r'/etc/motd'
default_config_data = {
- 'issue': 'Welcome to VyOS - \n \l',
- 'issue_net': 'Welcome to VyOS',
+ 'issue': 'Welcome to VyOS - \n \l\n',
+ 'issue_net': 'Welcome to VyOS\n',
'motd': motd
}
@@ -49,15 +51,29 @@ def get_config():
# Post-Login banner
if conf.exists(['post-login']):
tmp = conf.return_value(['post-login'])
- tmp = tmp.replace('\\n','\n')
- tmp = tmp.replace('\\t','\t')
+ # post-login banner can be empty as well
+ if tmp:
+ tmp = tmp.replace('\\n','\n')
+ tmp = tmp.replace('\\t','\t')
+ # always add newline character
+ tmp += '\n'
+ else:
+ tmp = ''
+
banner['motd'] = tmp
# Pre-Login banner
if conf.exists(['pre-login']):
tmp = conf.return_value(['pre-login'])
- tmp = tmp.replace('\\n','\n')
- tmp = tmp.replace('\\t','\t')
+ # pre-login banner can be empty as well
+ if tmp:
+ tmp = tmp.replace('\\n','\n')
+ tmp = tmp.replace('\\t','\t')
+ # always add newline character
+ tmp += '\n'
+ else:
+ tmp = ''
+
banner['issue'] = banner['issue_net'] = tmp
return banner
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index 23152fee0..43732cfae 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.py
@@ -14,36 +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 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
+from vyos.util import cmd
+from vyos.util import call
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,
@@ -67,10 +53,7 @@ def get_local_users():
def get_crypt_pw(password):
- command = '/usr/bin/mkpasswd --method=sha-512 {}'.format(password)
- p = Popen(command, stdout=PIPE, stderr=STDOUT, shell=True)
- tmp = p.communicate()[0].strip()
- return tmp.decode()
+ return cmd(f'/usr/bin/mkpasswd --method=sha-512 {password}')
def get_config():
@@ -196,6 +179,14 @@ def verify(login):
if cur_user in login['del_users']:
raise ConfigError('Attempting to delete current user: {}'.format(cur_user))
+ for user in login['add_users']:
+ for key in user['public_keys']:
+ if not key['type']:
+ raise ConfigError('SSH public key type missing for "{}"!'.format(key['name']))
+
+ if not key['key']:
+ raise ConfigError('SSH public key for id "{}" missing!'.format(key['name']))
+
# At lease one RADIUS server must not be disabled
if len(login['radius_server']) > 0:
fail = True
@@ -221,7 +212,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)
@@ -240,40 +236,44 @@ def apply(login):
for user in login['add_users']:
# make new user using vyatta shell and make home directory (-m),
# default group of 100 (users)
- cmd = "useradd -m -N"
+ command = "useradd -m -N"
# check if user already exists:
if user['name'] in get_local_users():
# update existing account
- cmd = "usermod"
+ command = "usermod"
# we need to use '' quotes when passing formatted data to the shell
# else it will not work as some data parts are lost in translation
- cmd += " -p '{}'".format(user['password_encrypted'])
- cmd += " -s /bin/vbash"
+ command += " -p '{}'".format(user['password_encrypted'])
+ command += " -s /bin/vbash"
if user['full_name']:
- cmd += " -c '{}'".format(user['full_name'])
+ command += " -c '{}'".format(user['full_name'])
if user['home_dir']:
- cmd += " -d '{}'".format(user['home_dir'])
+ command += " -d '{}'".format(user['home_dir'])
- cmd += " -G frrvty,vyattacfg,sudo,adm,dip,disk"
- cmd += " {}".format(user['name'])
+ command += " -G frrvty,vyattacfg,sudo,adm,dip,disk"
+ command += " {}".format(user['name'])
try:
- os.system(cmd)
+ call(command)
uid = getpwnam(user['name']).pw_uid
gid = getpwnam(user['name']).pw_gid
+ # we should not rely on the home dir value stored in user['home_dir']
+ # as if a crazy user will choose username root or any other system
+ # user this will fail. should be deny using root at all?
+ home_dir = getpwnam(user['name']).pw_dir
# install ssh keys
- key_dir = '{}/.ssh'.format(user['home_dir'])
- if not os.path.isdir(key_dir):
- os.mkdir(key_dir)
- os.chown(key_dir, uid, gid)
- os.chmod(key_dir, S_IRWXU | S_IRGRP | S_IXGRP)
-
- key_file = key_dir + '/authorized_keys';
- with open(key_file, 'w') as f:
+ ssh_key_dir = home_dir + '/.ssh'
+ if not os.path.isdir(ssh_key_dir):
+ os.mkdir(ssh_key_dir)
+ os.chown(ssh_key_dir, uid, gid)
+ os.chmod(ssh_key_dir, S_IRWXU | S_IRGRP | S_IXGRP)
+
+ ssh_key_file = ssh_key_dir + '/authorized_keys';
+ with open(ssh_key_file, 'w') as f:
f.write("# Automatically generated by VyOS\n")
f.write("# Do not edit, all changes will be lost\n")
@@ -285,8 +285,8 @@ def apply(login):
line += '{} {} {}\n'.format(id['type'], id['key'], id['name'])
f.write(line)
- os.chown(key_file, uid, gid)
- os.chmod(key_file, S_IRUSR | S_IWUSR)
+ os.chown(ssh_key_file, uid, gid)
+ os.chmod(ssh_key_file, S_IRUSR | S_IWUSR)
except Exception as e:
raise ConfigError('Adding user "{}" raised an exception: {}'.format(user['name'], e))
@@ -296,10 +296,10 @@ def apply(login):
# Logout user if he is logged in
if user in list(set([tmp[0] for tmp in users()])):
print('{} is logged in, forcing logout'.format(user))
- os.system('pkill -HUP -u {}'.format(user))
+ call('pkill -HUP -u {}'.format(user))
# Remove user account but leave home directory to be safe
- os.system('userdel -r {} 2>/dev/null'.format(user))
+ call('userdel -r {} 2>/dev/null'.format(user))
except Exception as e:
raise ConfigError('Deleting user "{}" raised an exception: {}'.format(user, e))
@@ -313,7 +313,7 @@ def apply(login):
os.system("DEBIAN_FRONTEND=noninteractive pam-auth-update --package --enable radius")
# Make NSS system aware of RADIUS, too
- cmd = "sed -i -e \'/\smapname/b\' \
+ command = "sed -i -e \'/\smapname/b\' \
-e \'/^passwd:/s/\s\s*/&mapuid /\' \
-e \'/^passwd:.*#/s/#.*/mapname &/\' \
-e \'/^passwd:[^#]*$/s/$/ mapname &/\' \
@@ -321,7 +321,7 @@ def apply(login):
-e \'/^group:[^#]*$/s/: */&mapname /\' \
/etc/nsswitch.conf"
- os.system(cmd)
+ call(command)
except Exception as e:
raise ConfigError('RADIUS configuration failed: {}'.format(e))
@@ -331,13 +331,13 @@ def apply(login):
# Disable RADIUS in PAM
os.system("DEBIAN_FRONTEND=noninteractive pam-auth-update --package --remove radius")
- cmd = "sed -i -e \'/^passwd:.*mapuid[ \t]/s/mapuid[ \t]//\' \
+ command = "sed -i -e \'/^passwd:.*mapuid[ \t]/s/mapuid[ \t]//\' \
-e \'/^passwd:.*[ \t]mapname/s/[ \t]mapname//\' \
-e \'/^group:.*[ \t]mapname/s/[ \t]mapname//\' \
-e \'s/[ \t]*$//\' \
/etc/nsswitch.conf"
- os.system(cmd)
+ call(command)
except Exception as e:
raise ConfigError('Removing RADIUS configuration failed'.format(e))
@@ -352,4 +352,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/system-options.py b/src/conf_mode/system-options.py
index a893e98b3..b3dbc82fb 100755
--- a/src/conf_mode/system-options.py
+++ b/src/conf_mode/system-options.py
@@ -20,6 +20,7 @@ from sys import exit
from copy import deepcopy
from vyos.config import Config
from vyos import ConfigError
+from vyos.util import run
systemd_ctrl_alt_del = '/lib/systemd/system/ctrl-alt-del.target'
@@ -51,9 +52,9 @@ def generate(opt):
def apply(opt):
# Beep action
if opt['beep_if_fully_booted']:
- os.system('systemctl enable vyos-beep.service >/dev/null 2>&1')
+ run('systemctl enable vyos-beep.service')
else:
- os.system('systemctl disable vyos-beep.service >/dev/null 2>&1')
+ run('systemctl disable vyos-beep.service')
# Ctrl-Alt-Delete action
if opt['ctrl_alt_del'] == 'ignore':
diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py
index 2d47cc061..25b9b5bed 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,84 +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 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 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
-
+from vyos.util import run
def get_config():
c = Config()
@@ -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,8 @@ def verify(c):
def apply(c):
if not c:
- subprocess.call(['sudo', 'systemctl', 'stop', 'syslog'])
- return 0
-
- subprocess.call(['sudo', 'systemctl', 'restart', 'syslog'])
+ return run('systemctl stop syslog')
+ return run('systemctl restart syslog')
if __name__ == '__main__':
try:
@@ -328,4 +264,4 @@ if __name__ == '__main__':
apply(c)
except ConfigError as e:
print(e)
- sys.exit(1)
+ exit(1)
diff --git a/src/conf_mode/system-timezone.py b/src/conf_mode/system-timezone.py
index d715bd27e..25b949a79 100755
--- a/src/conf_mode/system-timezone.py
+++ b/src/conf_mode/system-timezone.py
@@ -20,6 +20,8 @@ import os
from copy import deepcopy
from vyos.config import Config
from vyos import ConfigError
+from vyos.util import call
+
default_config_data = {
'name': 'UTC'
@@ -40,9 +42,7 @@ def generate(tz):
pass
def apply(tz):
- cmd = '/usr/bin/timedatectl set-timezone {}'.format(tz['name'])
- os.system(cmd)
- pass
+ call('/usr/bin/timedatectl set-timezone {}'.format(tz['name']))
if __name__ == '__main__':
try:
diff --git a/src/conf_mode/system-wifi-regdom.py b/src/conf_mode/system-wifi-regdom.py
index 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..7a7246783 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,31 +13,24 @@
#
# 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
+from vyos.util import call
-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 }}"
-
-"""
+config_file = r'/etc/default/tftpd'
default_config_data = {
'directory': '',
@@ -47,23 +40,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 +75,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 +83,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 +115,7 @@ def generate(tftpd):
def apply(tftpd):
# stop all services first - then we will decide
- os.system('sudo systemctl stop tftpd@{0..20}')
+ call('systemctl stop tftpd@{0..20}')
# bail out early - e.g. service deletion
if tftpd is None:
@@ -140,7 +140,7 @@ def apply(tftpd):
idx = 0
for listen in tftpd['listen']:
- os.system('sudo systemctl restart tftpd@{0}.service'.format(idx))
+ call('systemctl restart tftpd@{0}.service'.format(idx))
idx = idx + 1
return None
@@ -153,4 +153,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..45b2c4b40 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,20 +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 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
+from vyos.util import run
pidfile = r'/var/run/accel_pptp.pid'
pptp_cnf_dir = r'/etc/accel-ppp/pptp'
@@ -36,143 +35,24 @@ 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")
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')
-
-def _accel_cmd(cmd=''):
- if not cmd:
- return None
- try:
- ret = subprocess.check_output(
- ['/usr/bin/accel-cmd', '-p', '2003', cmd]).decode().strip()
- return ret
- except:
- return 1
+def _accel_cmd(command):
+ return run('/usr/bin/accel-cmd -p 2003 {command}')
###
# inline helper functions end
@@ -326,6 +206,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 +223,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
@@ -357,8 +248,7 @@ def apply(c):
return None
if not os.path.exists(pidfile):
- ret = subprocess.call(
- ['/usr/sbin/accel-pppd', '-c', pptp_conf, '-p', pidfile, '-d'])
+ ret = run(f'/usr/sbin/accel-pppd -c {pptp_conf} -p {pidfile} -d')
_chk_con()
if ret != 0 and os.path.exists(pidfile):
os.remove(pidfile)
@@ -366,8 +256,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 +265,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
new file mode 100755
index 000000000..ca0844c50
--- /dev/null
+++ b/src/conf_mode/vpn_sstp.py
@@ -0,0 +1,402 @@
+#!/usr/bin/env python3
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from time import sleep
+from sys import exit
+from socket import socket, AF_INET, SOCK_STREAM
+from copy import deepcopy
+from stat import S_IRUSR, S_IWUSR, S_IRGRP
+from jinja2 import FileSystemLoader, Environment
+
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.defaults import directories as vyos_data_dir
+from vyos.util import process_running
+from vyos.util import process_running, cmd, run
+
+pidfile = r'/var/run/accel_sstp.pid'
+sstp_cnf_dir = r'/etc/accel-ppp/sstp'
+chap_secrets = sstp_cnf_dir + '/chap-secrets'
+sstp_conf = sstp_cnf_dir + '/sstp.config'
+
+# config path creation
+if not os.path.exists(sstp_cnf_dir):
+ os.makedirs(sstp_cnf_dir)
+
+def chk_con():
+ cnt = 0
+ s = socket(AF_INET, SOCK_STREAM)
+ while True:
+ try:
+ s.connect(("127.0.0.1", 2005))
+ s.close()
+ break
+ except ConnectionRefusedError:
+ sleep(0.5)
+ cnt += 1
+ if cnt == 100:
+ raise("failed to start sstp server")
+ break
+
+
+def _accel_cmd(command):
+ return run(f'/usr/bin/accel-cmd -p 2005 {command}')
+
+default_config_data = {
+ 'local_users' : [],
+ 'auth_mode' : 'local',
+ 'auth_proto' : [],
+ 'radius_server' : [],
+ 'radius_acct_tmo' : '3',
+ 'radius_max_try' : '3',
+ 'radius_timeout' : '3',
+ 'radius_nas_id' : '',
+ 'radius_nas_ip' : '',
+ 'radius_source_address' : '',
+ 'radius_shaper_attr' : '',
+ 'radius_shaper_vendor': '',
+ 'radius_dynamic_author' : '',
+ 'ssl_ca' : '',
+ 'ssl_cert' : '',
+ 'ssl_key' : '',
+ 'client_ip_pool' : [],
+ 'dnsv4' : [],
+ 'mtu' : '',
+ 'ppp_mppe' : '',
+ 'ppp_echo_failure' : '',
+ 'ppp_echo_interval' : '',
+ 'ppp_echo_timeout' : '',
+ 'thread_cnt' : ''
+}
+
+def get_config():
+ sstp = deepcopy(default_config_data)
+ base_path = ['vpn', 'sstp']
+ conf = Config()
+ if not conf.exists(base_path):
+ return None
+
+ conf.set_level(base_path)
+
+ cpu = int(os.cpu_count()/2)
+ if cpu < 1:
+ cpu = 1
+ sstp['thread_cnt'] = cpu
+
+ if conf.exists(['authentication', 'mode']):
+ sstp['auth_mode'] = conf.return_value(['authentication', 'mode'])
+
+ #
+ # local auth
+ if conf.exists(['authentication', 'local-users']):
+ for username in conf.list_nodes(['authentication', 'local-users', 'username']):
+ user = {
+ 'name' : username,
+ 'password' : '',
+ 'state' : 'enabled',
+ 'ip' : '*',
+ 'upload' : None,
+ 'download' : None
+ }
+
+ conf.set_level(base_path + ['authentication', 'local-users', 'username', username])
+
+ if conf.exists(['password']):
+ user['password'] = conf.return_value(['password'])
+
+ if conf.exists(['disable']):
+ user['state'] = 'disable'
+
+ if conf.exists(['static-ip']):
+ user['ip'] = conf.return_value(['static-ip'])
+
+ if conf.exists(['rate-limit', 'download']):
+ user['download'] = conf.return_value(['rate-limit', 'download'])
+
+ if conf.exists(['rate-limit', 'upload']):
+ user['upload'] = conf.return_value(['rate-limit', 'upload'])
+
+ sstp['local_users'].append(user)
+
+ #
+ # RADIUS auth and settings
+ conf.set_level(base_path + ['authentication', 'radius'])
+ if conf.exists(['server']):
+ for server in conf.list_nodes(['server']):
+ radius = {
+ 'server' : server,
+ 'key' : '',
+ 'fail_time' : 0,
+ 'port' : '1812'
+ }
+
+ conf.set_level(base_path + ['authentication', 'radius', 'server', server])
+
+ if conf.exists(['fail-time']):
+ radius['fail-time'] = conf.return_value(['fail-time'])
+
+ if conf.exists(['port']):
+ radius['port'] = conf.return_value(['port'])
+
+ if conf.exists(['key']):
+ radius['key'] = conf.return_value(['key'])
+
+ if not conf.exists(['disable']):
+ sstp['radius_server'].append(radius)
+
+ #
+ # advanced radius-setting
+ conf.set_level(base_path + ['authentication', 'radius'])
+
+ if conf.exists(['acct-timeout']):
+ sstp['radius_acct_tmo'] = conf.return_value(['acct-timeout'])
+
+ if conf.exists(['max-try']):
+ sstp['radius_max_try'] = conf.return_value(['max-try'])
+
+ if conf.exists(['timeout']):
+ sstp['radius_timeout'] = conf.return_value(['timeout'])
+
+ if conf.exists(['nas-identifier']):
+ sstp['radius_nas_id'] = conf.return_value(['nas-identifier'])
+
+ if conf.exists(['nas-ip-address']):
+ sstp['radius_nas_ip'] = conf.return_value(['nas-ip-address'])
+
+ if conf.exists(['source-address']):
+ sstp['radius_source_address'] = conf.return_value(['source-address'])
+
+ # Dynamic Authorization Extensions (DOA)/Change Of Authentication (COA)
+ if conf.exists(['dynamic-author']):
+ dae = {
+ 'port' : '',
+ 'server' : '',
+ 'key' : ''
+ }
+
+ if conf.exists(['dynamic-author', 'server']):
+ dae['server'] = conf.return_value(['dynamic-author', 'server'])
+
+ if conf.exists(['dynamic-author', 'port']):
+ dae['port'] = conf.return_value(['dynamic-author', 'port'])
+
+ if conf.exists(['dynamic-author', 'key']):
+ dae['key'] = conf.return_value(['dynamic-author', 'key'])
+
+ sstp['radius_dynamic_author'] = dae
+
+ if conf.exists(['rate-limit', 'enable']):
+ sstp['radius_shaper_attr'] = 'Filter-Id'
+ c_attr = ['rate-limit', 'enable', 'attribute']
+ if conf.exists(c_attr):
+ sstp['radius_shaper_attr'] = conf.return_value(c_attr)
+
+ c_vendor = ['rate-limit', 'enable', 'vendor']
+ if conf.exists(c_vendor):
+ sstp['radius_shaper_vendor'] = conf.return_value(c_vendor)
+
+ #
+ # authentication protocols
+ conf.set_level(base_path + ['authentication'])
+ if conf.exists(['protocols']):
+ auth_mods = {
+ 'pap': 'auth_pap',
+ 'chap': 'auth_chap_md5',
+ 'mschap': 'auth_mschap_v1',
+ 'mschap-v2': 'auth_mschap_v2'
+ }
+
+ for proto in conf.return_values(['protocols']):
+ sstp['auth_proto'].append(auth_mods[proto])
+
+ else:
+ sstp['auth_proto'] = ['auth_mschap_v2']
+
+ #
+ # read in SSL certs
+ conf.set_level(base_path + ['ssl'])
+ if conf.exists(['ca-cert-file']):
+ sstp['ssl_ca'] = conf.return_value(['ca-cert-file'])
+
+ if conf.exists(['cert-file']):
+ sstp['ssl_cert'] = conf.return_value(['cert-file'])
+
+ if conf.exists(['key-file']):
+ sstp['ssl_key'] = conf.return_value(['key-file'])
+
+
+ #
+ # read in client ip pool settings
+ conf.set_level(base_path + ['network-settings', 'client-ip-settings'])
+ if conf.exists(['subnet']):
+ sstp['client_ip_pool'] = conf.return_values(['subnet'])
+
+ if conf.exists(['gateway-address']):
+ sstp['client_gateway'] = conf.return_value(['gateway-address'])
+
+ #
+ # read in network settings
+ conf.set_level(base_path + ['network-settings'])
+ if conf.exists(['name-server']):
+ sstp['dnsv4'] = conf.return_values(['name-server'])
+
+ if conf.exists(['mtu']):
+ sstp['mtu'] = conf.return_value(['mtu'])
+
+ #
+ # read in PPP stuff
+ conf.set_level(base_path + ['ppp-settings'])
+ if conf.exists('mppe'):
+ sstp['ppp_mppe'] = conf.return_value('ppp-settings mppe')
+
+ if conf.exists(['lcp-echo-failure']):
+ sstp['ppp_echo_failure'] = conf.return_value(['lcp-echo-failure'])
+
+ if conf.exists(['lcp-echo-interval']):
+ sstp['ppp_echo_interval'] = conf.return_value(['lcp-echo-interval'])
+
+ if conf.exists(['lcp-echo-timeout']):
+ sstp['ppp_echo_timeout'] = conf.return_value(['lcp-echo-timeout'])
+
+ return sstp
+
+
+def verify(sstp):
+ if sstp is None:
+ return None
+
+ # vertify auth settings
+ if sstp['auth_mode'] == 'local':
+ if not sstp['local_users']:
+ raise ConfigError('sstp-server authentication local-users required')
+
+ for user in sstp['local_users']:
+ if not user['password']:
+ raise ConfigError(f"Password required for user {user['name']}")
+
+ # if up/download is set, check that both have a value
+ if user['upload'] and not user['download']:
+ raise ConfigError(f"Download speed value required for user {user['name']}")
+
+ if user['download'] and not user['upload']:
+ raise ConfigError(f"Upload speed value required for user {user['name']}")
+
+ if not sstp['client_ip_pool']:
+ raise ConfigError("Client IP subnet required")
+
+ if not sstp['client_gateway']:
+ raise ConfigError("Client gateway IP address required")
+
+ if len(sstp['dnsv4']) > 2:
+ raise ConfigError("Only 2 DNS name-servers can be configured")
+
+ if not sstp['ssl_ca'] or not sstp['ssl_cert'] or not sstp['ssl_key']:
+ raise ConfigError('One or more SSL certificates missing')
+
+ if not os.path.exists(sstp['ssl_ca']):
+ raise ConfigError(f"CA cert file {sstp['ssl_ca']} does not exist")
+
+ if not os.path.exists(sstp['ssl_cert']):
+ raise ConfigError(f"SSL cert file {sstp['ssl_cert']} does not exist")
+
+ if not os.path.exists(sstp['ssl_key']):
+ raise ConfigError(f"SSL key file {sstp['ssl_key']} does not exist")
+
+ if sstp['auth_mode'] == 'radius':
+ if len(sstp['radius_server']) == 0:
+ raise ConfigError("RADIUS authentication requires at least one server")
+
+ for radius in sstp['radius_server']:
+ if not radius['key']:
+ raise ConfigError(f"Missing RADIUS secret for server {{ radius['key'] }}")
+
+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 = 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 = env.get_template('chap-secrets.tmpl')
+ config_text = tmpl.render(sstp)
+ with open(chap_secrets, 'w') as f:
+ f.write(config_text)
+
+ os.chmod(chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP)
+ else:
+ if os.path.exists(chap_secrets):
+ os.unlink(chap_secrets)
+
+ return sstp
+
+def apply(sstp):
+ if sstp is None:
+ if process_running(pidfile):
+ command = 'start-stop-daemon'
+ command += ' --stop '
+ command += ' --quiet'
+ command += ' --oknodo'
+ command += ' --pidfile ' + pidfile
+ cmd(command)
+
+ if os.path.exists(pidfile):
+ os.remove(pidfile)
+
+ return None
+
+ if not process_running(pidfile):
+ if os.path.exists(pidfile):
+ os.remove(pidfile)
+
+ command = 'start-stop-daemon'
+ command += ' --start '
+ command += ' --quiet'
+ command += ' --oknodo'
+ command += ' --pidfile ' + pidfile
+ command += ' --exec /usr/sbin/accel-pppd'
+ # now pass arguments to accel-pppd binary
+ command += ' --'
+ command += ' -c ' + sstp_conf
+ command += ' -p ' + pidfile
+ command += ' -d'
+ cmd(command)
+
+ chk_con()
+
+ else:
+ _accel_cmd('restart')
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py
new file mode 100755
index 000000000..586424c09
--- /dev/null
+++ b/src/conf_mode/vrf.py
@@ -0,0 +1,276 @@
+#!/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/>.
+
+import os
+
+from sys import exit
+from copy import deepcopy
+from jinja2 import FileSystemLoader, Environment
+from json import loads
+
+from vyos.config import Config
+from vyos.configdict import list_diff
+from vyos.defaults import directories as vyos_data_dir
+from vyos.ifconfig import Interface
+from vyos.util import read_file, cmd
+from vyos import ConfigError
+
+config_file = r'/etc/iproute2/rt_tables.d/vyos-vrf.conf'
+
+default_config_data = {
+ 'bind_to_all': '0',
+ 'deleted': False,
+ 'vrf_add': [],
+ 'vrf_existing': [],
+ 'vrf_remove': []
+}
+
+def _cmd(command):
+ cmd(command, raising=ConfigError, message='Error changing VRF')
+
+def list_rules():
+ command = 'ip -j -4 rule show'
+ answer = loads(cmd(command))
+ return [_ for _ in answer if _]
+
+def vrf_interfaces(c, match):
+ matched = []
+ old_level = c.get_level()
+ c.set_level(['interfaces'])
+ section = c.get_config_dict([])
+ for type in section:
+ interfaces = section[type]
+ for name in interfaces:
+ interface = interfaces[name]
+ if 'vrf' in interface:
+ v = interface.get('vrf', '')
+ if v == match:
+ matched.append(name)
+
+ c.set_level(old_level)
+ return matched
+
+def vrf_routing(c, match):
+ matched = []
+ old_level = c.get_level()
+ c.set_level(['protocols', 'vrf'])
+ if match in c.list_nodes([]):
+ matched.append(match)
+
+ c.set_level(old_level)
+ return matched
+
+
+def get_config():
+ conf = Config()
+ vrf_config = deepcopy(default_config_data)
+
+ cfg_base = ['vrf']
+ if not conf.exists(cfg_base):
+ # get all currently effetive VRFs and mark them for deletion
+ vrf_config['vrf_remove'] = conf.list_effective_nodes(cfg_base + ['name'])
+ else:
+ # set configuration level base
+ conf.set_level(cfg_base)
+
+ # Should services be allowed to bind to all VRFs?
+ if conf.exists(['bind-to-all']):
+ 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
+ eff_vrf = conf.list_effective_nodes(['name'])
+ act_vrf = conf.list_nodes(['name'])
+ vrf_config['vrf_remove'] = list_diff(eff_vrf, act_vrf)
+
+ # read in individual VRF definition and build up
+ # configuration
+ for name in conf.list_nodes(['name']):
+ vrf_inst = {
+ 'description' : '',
+ 'members': [],
+ 'name' : name,
+ 'table' : '',
+ 'table_mod': False
+ }
+ conf.set_level(cfg_base + ['name', name])
+
+ if conf.exists(['table']):
+ # VRF table can't be changed on demand, thus we need to read in the
+ # current and the effective routing table number
+ act_table = conf.return_value(['table'])
+ eff_table = conf.return_effective_value(['table'])
+ vrf_inst['table'] = act_table
+ if eff_table and eff_table != act_table:
+ vrf_inst['table_mod'] = True
+
+ if conf.exists(['description']):
+ vrf_inst['description'] = conf.return_value(['description'])
+
+ # append individual VRF configuration to global configuration list
+ vrf_config['vrf_add'].append(vrf_inst)
+
+ # set configuration level base
+ conf.set_level(cfg_base)
+
+ # check VRFs which need to be removed as they are not allowed to have
+ # interfaces attached
+ tmp = []
+ for name in vrf_config['vrf_remove']:
+ vrf_inst = {
+ 'interfaces': [],
+ 'name': name,
+ 'routes': []
+ }
+
+ # find member interfaces of this particulat VRF
+ vrf_inst['interfaces'] = vrf_interfaces(conf, name)
+
+ # find routing protocols used by this VRF
+ vrf_inst['routes'] = vrf_routing(conf, name)
+
+ # append individual VRF configuration to temporary configuration list
+ tmp.append(vrf_inst)
+
+ # replace values in vrf_remove with list of dictionaries
+ # as we need it in verify() - we can't delete a VRF with members attached
+ vrf_config['vrf_remove'] = tmp
+ return vrf_config
+
+def verify(vrf_config):
+ # ensure VRF is not assigned to any interface
+ for vrf in vrf_config['vrf_remove']:
+ if len(vrf['interfaces']) > 0:
+ raise ConfigError(f"VRF {vrf['name']} can not be deleted. It has active member interfaces!")
+
+ if len(vrf['routes']) > 0:
+ raise ConfigError(f"VRF {vrf['name']} can not be deleted. It has active routing protocols!")
+
+ table_ids = []
+ for vrf in vrf_config['vrf_add']:
+ # table id is mandatory
+ if not vrf['table']:
+ raise ConfigError(f"VRF {vrf['name']} table id is mandatory!")
+
+ # routing table id can't be changed - OS restriction
+ if vrf['table_mod']:
+ raise ConfigError(f"VRF {vrf['name']} table id modification is not possible!")
+
+ # VRf routing table ID must be unique on the system
+ if vrf['table'] in table_ids:
+ raise ConfigError(f"VRF {vrf['name']} table id {vrf['table']} is not unique!")
+
+ table_ids.append(vrf['table'])
+
+ return None
+
+def generate(vrf_config):
+ # Prepare Jinja2 template loader from files
+ tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'vrf')
+ fs_loader = FileSystemLoader(tmpl_path)
+ env = Environment(loader=fs_loader)
+
+ tmpl = env.get_template('vrf.conf.tmpl')
+ config_text = tmpl.render(vrf_config)
+ with open(config_file, 'w') as f:
+ f.write(config_text)
+
+ return None
+
+def apply(vrf_config):
+ # Documentation
+ #
+ # - https://github.com/torvalds/linux/blob/master/Documentation/networking/vrf.txt
+ # - https://github.com/Mellanox/mlxsw/wiki/Virtual-Routing-and-Forwarding-(VRF)
+ # - https://github.com/Mellanox/mlxsw/wiki/L3-Tunneling
+ # - https://netdevconf.info/1.1/proceedings/slides/ahern-vrf-tutorial.pdf
+ # - https://netdevconf.info/1.2/slides/oct6/02_ahern_what_is_l3mdev_slides.pdf
+
+ # set the default VRF global behaviour
+ bind_all = vrf_config['bind_to_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']:
+ name = vrf['name']
+ table = vrf['table']
+
+ if not os.path.isdir(f'/sys/class/net/{name}'):
+ # For each VRF apart from your default context create a VRF
+ # interface with a separate routing table
+ _cmd(f'ip link add {name} type vrf table {table}')
+ # Start VRf
+ _cmd(f'ip link set dev {name} up')
+ # The kernel Documentation/networking/vrf.txt also recommends
+ # adding unreachable routes to the VRF routing tables so that routes
+ # afterwards are taken.
+ _cmd(f'ip -4 route add vrf {name} unreachable default metric 4278198272')
+ _cmd(f'ip -6 route add vrf {name} unreachable default metric 4278198272')
+
+ # set VRF description for e.g. SNMP monitoring
+ Interface(name).set_alias(vrf['description'])
+
+ # Linux routing uses rules to find tables - routing targets are then
+ # looked up in those tables. If the lookup got a matching route, the
+ # process ends.
+ #
+ # TL;DR; first table with a matching entry wins!
+ #
+ # You can see your routing table lookup rules using "ip rule", sadly the
+ # local lookup is hit before any VRF lookup. Pinging an addresses from the
+ # VRF will usually find a hit in the local table, and never reach the VRF
+ # routing table - this is usually not what you want. Thus we will
+ # re-arrange the tables and move the local lookup furhter down once VRFs
+ # are enabled.
+
+ # get current preference on local table
+ local_pref = [r.get('priority') for r in list_rules() if r.get('table') == 'local'][0]
+
+ # change preference when VRFs are enabled and local lookup table is default
+ if not local_pref and vrf_config['vrf_add']:
+ for af in ['-4', '-6']:
+ _cmd(f'ip {af} rule add pref 32765 table local')
+ _cmd(f'ip {af} rule del pref 0')
+
+ # return to default lookup preference when no VRF is configured
+ if not vrf_config['vrf_add']:
+ for af in ['-4', '-6']:
+ _cmd(f'ip {af} rule add pref 0 table local')
+ _cmd(f'ip {af} rule del pref 32765')
+
+ # clean out l3mdev-table rule if present
+ if 1000 in [r.get('priority') for r in list_rules() if r.get('priority') == 1000]:
+ _cmd(f'ip {af} rule del pref 1000')
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py
index a09e55a2f..3f1b73385 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,145 +13,25 @@
#
# 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 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 vyos.util import call
daemon_file = "/etc/default/keepalived"
config_file = "/etc/keepalived/keepalived.conf"
-
-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
-}
-
-{% 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 -%}
-
- {% if group.master_script -%}
- notify_master "/usr/libexec/vyos/system/vrrp-script-wrapper.py --state master --group {{ group.name }} --interface {{ group.interface }} {{ group.master_script }}"
- {% endif -%}
-
- {% if group.backup_script -%}
- notify_backup "/usr/libexec/vyos/system/vrrp-script-wrapper.py --state backup --group {{ group.name }} --interface {{ group.interface }} {{ group.backup_script }}"
- {% endif -%}
-
- {% if group.fault_script -%}
- notify_fault "/usr/libexec/vyos/system/vrrp-script-wrapper.py --state fault --group {{ group.name }} --interface {{ group.interface }} {{ group.fault_script }}"
- {% endif -%}
-
- {% if group.stop_script -%}
- notify_stop "/usr/libexec/vyos/system/vrrp-script-wrapper.py --state stop --group {{ group.name }} --interface {{ group.interface }} {{ group.stop_script }}"
- {% 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"
-"""
+config_dict_path = "/run/keepalived_config.dict"
def get_config():
vrrp_groups = []
@@ -220,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
@@ -238,10 +118,21 @@ 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(dumps({'vrrp_groups': vrrp_groups, 'sync_groups': sync_groups}))
+
return (vrrp_groups, sync_groups)
+
def verify(data):
vrrp_groups, sync_groups = data
@@ -262,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
@@ -308,7 +199,13 @@ def verify(data):
if not (m in vrrp_group_names):
raise ConfigError("VRRP sync-group {0} refers to VRRP group {1}, but group {1} does not exist".format(sync_group["name"], m))
+
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
@@ -318,34 +215,44 @@ def generate(data):
if g["disable"]:
print("Warning: ignoring disabled VRRP group {0} in sync-group {1}".format(g["name"], sync_group["name"]))
# Filter out disabled groups
- vrrp_groups = list(filter(lambda x: x["disable"] != True, vrrp_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
+
def apply(data):
vrrp_groups, sync_groups = data
if vrrp_groups:
+ # safely rename a temporary file with configuration dict
+ try:
+ dict_file = Path("{}.temp".format(config_dict_path))
+ dict_file.rename(Path(config_dict_path))
+ except Exception as err:
+ print("Unable to rename the file with keepalived config for FIFO pipe: {}".format(err))
+
if not vyos.keepalived.vrrp_running():
print("Starting the VRRP process")
- ret = subprocess.call("sudo systemctl restart keepalived.service", shell=True)
+ ret = call("sudo systemctl restart keepalived.service")
else:
print("Reloading the VRRP process")
- ret = subprocess.call("sudo systemctl reload keepalived.service", shell=True)
+ ret = call("sudo systemctl reload keepalived.service")
if ret != 0:
raise ConfigError("keepalived failed to start")
else:
# VRRP is removed in the commit
print("Stopping the VRRP process")
- subprocess.call("sudo systemctl stop keepalived.service", shell=True)
+ call("sudo systemctl stop keepalived.service")
os.unlink(config_file)
return None
@@ -359,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/conf_mode/vyos_cert.py b/src/conf_mode/vyos_cert.py
index 4a44573ca..8b8953cb7 100755
--- a/src/conf_mode/vyos_cert.py
+++ b/src/conf_mode/vyos_cert.py
@@ -18,7 +18,6 @@
import sys
import os
-import subprocess
import tempfile
import pathlib
import ssl
@@ -26,6 +25,7 @@ import ssl
import vyos.defaults
from vyos.config import Config
from vyos import ConfigError
+from vyos.util import cmd
vyos_conf_scripts_dir = vyos.defaults.directories['conf_mode']
@@ -49,16 +49,16 @@ def status_self_signed(cert_data):
# check if certificate is 1/2 past lifetime, with openssl -checkend
end_days = int(cert_data['lifetime'])
end_seconds = int(0.5*60*60*24*end_days)
- checkend_cmd = ('openssl x509 -checkend {end} -noout -in {crt}'
- ''.format(end=end_seconds, **cert_data))
+ checkend_cmd = 'openssl x509 -checkend {end} -noout -in {crt}'.format(end=end_seconds, **cert_data)
try:
- subprocess.check_call(checkend_cmd, shell=True)
+ cmd(checkend_cmd, message='Called process error')
return True
- except subprocess.CalledProcessError as err:
- if err.returncode == 1:
+ except OSError as err:
+ if err.errno == 1:
return False
- else:
- print("Called process error: {}.".format(err))
+ print(err)
+ # XXX: This seems wrong to continue on failure
+ # implicitely returning None
def generate_self_signed(cert_data):
san_config = None
@@ -86,9 +86,10 @@ def generate_self_signed(cert_data):
''.format(**cert_data))
try:
- subprocess.check_call(openssl_req_cmd, shell=True)
- except subprocess.CalledProcessError as err:
- print("Called process error: {}.".format(err))
+ cmd(openssl_req_cmd, message='Called process error')
+ except OSError as err:
+ print(err)
+ # XXX: seems wrong to ignore the failure
os.chmod('{key}'.format(**cert_data), 0o400)
@@ -126,11 +127,8 @@ def generate(vyos_cert):
def apply(vyos_cert):
for dep in dependencies:
- cmd = '{0}/{1}'.format(vyos_conf_scripts_dir, dep)
- try:
- subprocess.check_call(cmd, shell=True)
- except subprocess.CalledProcessError as err:
- raise ConfigError("{}.".format(err))
+ command = '{0}/{1}'.format(vyos_conf_scripts_dir, dep)
+ cmd(command, raising=ConfigError)
if __name__ == '__main__':
try: