diff options
157 files changed, 3147 insertions, 2909 deletions
diff --git a/data/templates/dhcp-client/ipv4.tmpl b/data/templates/dhcp-client/ipv4.tmpl new file mode 100644 index 000000000..43f273077 --- /dev/null +++ b/data/templates/dhcp-client/ipv4.tmpl @@ -0,0 +1,17 @@ +# generated by ifconfig.py +option rfc3442-classless-static-routes code 121 = array of unsigned integer 8; +timeout 60; +retry 300; + +interface "{{ ifname }}" { + send host-name "{{ hostname }}"; + {% if client_id -%} + send dhcp-client-identifier "{{ client_id }}"; + {% endif -%} + {% if vendor_class_id -%} + send vendor-class-identifier "{{ vendor_class_id }}"; + {% endif -%} + request subnet-mask, broadcast-address, routers, domain-name-servers, + rfc3442-classless-static-routes, domain-name, interface-mtu; + require subnet-mask; +} diff --git a/data/templates/dhcp-client/ipv6.tmpl b/data/templates/dhcp-client/ipv6.tmpl new file mode 100644 index 000000000..83db40c5f --- /dev/null +++ b/data/templates/dhcp-client/ipv6.tmpl @@ -0,0 +1,4 @@ +# generated by ifconfig.py +interface "{{ ifname }}" { + request routers, domain-name-servers, domain-name; +} diff --git a/data/templates/dhcp-relay/config.tmpl b/data/templates/dhcp-relay/config.tmpl index 7203ae9fb..b223807cf 100644 --- a/data/templates/dhcp-relay/config.tmpl +++ b/data/templates/dhcp-relay/config.tmpl @@ -1,17 +1,4 @@ ### 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(' ') }}" +# Defaults for isc-dhcp-relay6.service +OPTIONS="{{ options | join(' ') }} -i {{ interface | join(' -i ') }} {{ server | join(' ') }}" diff --git a/data/templates/dhcp-server/daemon.tmpl b/data/templates/dhcp-server/daemon.tmpl deleted file mode 100644 index f88032d38..000000000 --- a/data/templates/dhcp-server/daemon.tmpl +++ /dev/null @@ -1,8 +0,0 @@ -### 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="" diff --git a/data/templates/dhcpv6-relay/config.tmpl b/data/templates/dhcpv6-relay/config.tmpl index 28f7a1a58..55035ae6c 100644 --- a/data/templates/dhcpv6-relay/config.tmpl +++ b/data/templates/dhcpv6-relay/config.tmpl @@ -1,4 +1,4 @@ ### 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(' ') }}" +# Defaults for isc-dhcp-relay6.service +OPTIONS="-l {{ listen_addr | join(' -l ') }} -u {{ upstream_addr | join(' -u ') }} {{ options | join(' ') }}" diff --git a/data/templates/dhcpv6-server/daemon.tmpl b/data/templates/dhcpv6-server/daemon.tmpl deleted file mode 100644 index a4967e7c3..000000000 --- a/data/templates/dhcpv6-server/daemon.tmpl +++ /dev/null @@ -1,8 +0,0 @@ -### 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="" diff --git a/data/templates/dynamic-dns/ddclient.conf.tmpl b/data/templates/dynamic-dns/ddclient.conf.tmpl index 22cb38f4e..9c7219230 100644 --- a/data/templates/dynamic-dns/ddclient.conf.tmpl +++ b/data/templates/dynamic-dns/ddclient.conf.tmpl @@ -1,10 +1,7 @@ - ### Autogenerated by dynamic_dns.py ### daemon=1m syslog=yes ssl=yes -pid={{ pid_file }} -cache={{ cache_file }} {% for interface in interfaces -%} diff --git a/data/templates/https/nginx.default.tmpl b/data/templates/https/nginx.default.tmpl index 33f7b2820..f4f2c1848 100644 --- a/data/templates/https/nginx.default.tmpl +++ b/data/templates/https/nginx.default.tmpl @@ -43,6 +43,7 @@ server { location ~ /(retrieve|configure|config-file|image|generate|show) { {% if server.api %} proxy_pass http://localhost:{{ server.api.port }}; + proxy_read_timeout 600; proxy_buffering off; {% else %} return 503; diff --git a/data/templates/l2tp/chap-secrets.tmpl b/data/templates/l2tp/chap-secrets.tmpl index 0db295fdc..dd00d7bd0 100644 --- a/data/templates/l2tp/chap-secrets.tmpl +++ b/data/templates/l2tp/chap-secrets.tmpl @@ -1,10 +1,10 @@ -# 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'] %} -{{ "%-12s" | format(user) }} * {{ "%-16s" | format(authentication['local-users'][user]['passwd']) }} {{ "%-16s" | format(authentication['local-users'][user]['ip']) }} {{ authentication['local-users'][user]['download'] }} / {{ authentication['local-users'][user]['upload'] }} +# username server password acceptable local IP addresses shaper +{% for user in local_users %} +{% if user.state == 'enabled' %} +{% if user.upload and user.download %} +{{ "%-12s" | format(user.name) }} * {{ "%-16s" | format(user.password) }} {{ "%-16s" | format(user.ip) }} {{ user.download }} / {{ user.upload }} {% else %} -{{ "%-12s" | format(user) }} * {{ "%-16s" | format(authentication['local-users'][user]['passwd']) }} {{ "%-16s" | format(authentication['local-users'][user]['ip']) }} +{{ "%-12s" | format(user.name) }} * {{ "%-16s" | format(user.password) }} {{ "%-16s" | format(user.ip) }} {% endif %} {% endif %} {% endfor %} diff --git a/data/templates/l2tp/l2tp.config.tmpl b/data/templates/l2tp/l2tp.config.tmpl index b8637e256..ba78cadcd 100644 --- a/data/templates/l2tp/l2tp.config.tmpl +++ b/data/templates/l2tp/l2tp.config.tmpl @@ -3,12 +3,14 @@ log_syslog l2tp chap-secrets -{% for proto in authentication['auth_proto']: %} +{% for proto in auth_proto: %} {{proto}} {% endfor%} -{% if authentication['mode'] == 'radius' %} + +{% if auth_mode == 'radius' %} radius {% endif -%} + ippool shaper ipv6pool @@ -23,52 +25,46 @@ syslog=accel-l2tp,daemon copy=1 level=5 -{% if dns %} +{% if dnsv4 %} [dns] -{% if dns[0] %} -dns1={{dns[0]}} -{% endif %} -{% if dns[1] %} -dns2={{dns[1]}} +{% for dns in dnsv4 -%} +dns{{ loop.index }}={{ dns }} +{% endfor -%} {% endif %} -{% endif -%} {% if dnsv6 %} [ipv6-dns] -{% for srv in dnsv6: %} -{{srv}} -{% endfor %} +{% for dns in dnsv6 -%} +{{ dns }} +{% endfor -%} {% endif %} {% if wins %} [wins] -{% if wins[0] %} -wins1={{wins[0]}} -{% endif %} -{% if wins[1] %} -wins2={{wins[1]}} +{% for server in wins -%} +wins{{ loop.index }}={{ server }} +{% endfor -%} {% endif %} -{% endif -%} [l2tp] verbose=1 ifname=l2tp%d -ppp-max-mtu={{mtu}} -mppe={{authentication['mppe']}} +ppp-max-mtu={{ mtu }} +mppe={{ ppp_mppe }} {% if outside_addr %} -bind={{outside_addr}} +bind={{ outside_addr }} {% endif %} {% if lns_shared_secret %} -secret={{lns_shared_secret}} +secret={{ lns_shared_secret }} {% endif %} [client-ip-range] 0.0.0.0/0 -{% if (client_ip_pool) or (client_ip_subnets) %} +{% if client_ip_pool or client_ip_subnets %} [ip-pool] {% if client_ip_pool %} -{{client_ip_pool}} +{{ client_ip_pool }} {% endif -%} {% if client_ip_subnets %} {% for sn in client_ip_subnets %} @@ -77,34 +73,41 @@ secret={{lns_shared_secret}} {% endif %} {% endif %} {% if gateway_address %} -gw-ip-address={{gateway_address}} +gw-ip-address={{ gateway_address }} {% endif %} -{% if authentication['mode'] == 'local' %} +{% if auth_mode == 'local' %} [chap-secrets] -chap-secrets=/etc/accel-ppp/l2tp/chap-secrets -{% if gateway_address %} -gw-ip-address={{gateway_address}} -{% endif %} +chap-secrets={{ chap_secrets_file }} +{% elif auth_mode == 'radius' %} +[radius] +verbose=1 +{% for r in radius_server %} +server={{ r.server }},{{ r.key }},auth-port={{ r.port }},req-limit=0,fail-time={{ r.fail_time }} +{% endfor -%} {% endif %} +acct-timeout={{ radius_acct_tmo }} +timeout={{ radius_timeout }} +max-try={{ radius_max_try }} + +{% if radius_nas_id %} +nas-identifier={{ radius_nas_id }} +{% endif -%} +{% if radius_nas_ip %} +nas-ip-address={{ radius_nas_ip }} +{% endif -%} +{% if radius_source_address %} +bind={{ radius_source_address }} +{% endif -%} + [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 %} +lcp-echo-timeout={{ ppp_echo_timeout }} +lcp-echo-interval={{ ppp_echo_interval }} +lcp-echo-failure={{ ppp_echo_failure }} {% if ccp_disable %} ccp=0 {% endif %} @@ -112,62 +115,33 @@ ccp=0 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}} +{% for p in client_ipv6_pool %} +{{ p.prefix }},{{ p.mask }} {% endfor %} -{% for prfx in client_ipv6_pool.delegate_prefix: %} -delegate={{prfx}} +{% for p in client_ipv6_delegate_prefix %} +delegate={{ p.prefix }},{{ p.mask }} {% endfor %} + {% endif %} -{% if client_ipv6_pool['delegate_prefix'] %} +{% if client_ipv6_delegate_prefix %} [ipv6-dhcp] verbose=1 {% endif %} -{% if authentication['radiusopt']['shaper'] %} +{% if radius_shaper_attr %} [shaper] verbose=1 -attr={{authentication['radiusopt']['shaper']['attr']}} -{% if authentication['radiusopt']['shaper']['vendor'] %} -vendor={{authentication['radiusopt']['shaper']['vendor']}} +attr={{ radius_shaper_attr }} +{% if radius_shaper_vendor %} +vendor={{ radius_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 +sessions-columns=ifname,username,calling-sid,ip,{{ ip6_column | join(',') }}{{ ',' if ip6_column }}rate-limit,type,comp,state,rx-bytes,tx-bytes,uptime diff --git a/data/templates/openvpn/server.conf.tmpl b/data/templates/openvpn/server.conf.tmpl index e7715dfb5..a9dacd36e 100644 --- a/data/templates/openvpn/server.conf.tmpl +++ b/data/templates/openvpn/server.conf.tmpl @@ -3,18 +3,18 @@ # See https://community.openvpn.net/openvpn/wiki/Openvpn24ManPage # for individual keyword definition -{% if description %} +{% if description -%} # {{ description }} -{% endif %} + +{% 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 }} + +dev-type {{ type }} +dev {{ intf }} persist-key iproute /usr/libexec/vyos/system/unpriv-ip @@ -22,187 +22,197 @@ proto {% if 'tcp-active' in protocol -%}tcp-client{% elif 'tcp-passive' in proto {%- if local_host %} local {{ local_host }} -{% endif %} +{%- endif %} {%- if mode == 'server' and protocol == 'udp' and not local_host %} multihome -{% endif %} +{%- endif %} {%- if local_port %} lport {{ local_port }} -{% endif %} +{%- endif %} -{%- if remote_port %} +{% if remote_port -%} rport {{ remote_port }} {% endif %} {%- if remote_host %} -{% for remote in remote_host -%} +{%- for remote in remote_host -%} remote {{ remote }} {% endfor -%} -{% endif %} +{% endif -%} -{%- if shared_secret_file %} +{% if shared_secret_file %} secret {{ shared_secret_file }} -{% endif %} +{%- endif %} {%- if persistent_tunnel %} persist-tun -{% endif %} +{%- endif %} + +{%- if redirect_gateway %} +push "redirect-gateway {{ redirect_gateway }}" +{%- endif %} -{%- if mode %} -{%- if 'client' in mode %} +{%- if compress_lzo %} +compress lzo +{%- endif %} + +{% if 'client' in mode -%} # # OpenVPN Client mode # client nobind -{%- elif 'server' in mode %} + +{% 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 -%} +topology {% if server_topology == 'point-to-point' %}p2p{% else %}{{ server_topology }}{% endif %} +{%- endif %} -{% for route in server_push_route -%} -push "route {{ route }}" -{% endfor -%} +{%- if bridge_member %} +mode server +tls-server +{%- else %} +server {{ server_subnet }}{% if server_pool_start %} nopool{% endif %} +{%- endif %} -{%- if server_domain %} -push "dhcp-option DOMAIN {{ server_domain }}" -{% endif %} +{%- if server_pool_start %} +ifconfig-pool {{ server_pool_start }} {{ server_pool_stop }}{% if server_pool_netmask %} {{ server_pool_netmask }}{% endif %} +{%- endif %} {%- if server_max_conn %} max-clients {{ server_max_conn }} -{% endif %} +{%- endif %} -{%- if bridge_member %} -server-bridge nogw -{%- else %} -server {{ server_subnet }} -{% endif %} +{%- if client %} +client-config-dir /run/openvpn/ccd/{{ intf }} +{%- endif %} {%- if server_reject_unconfigured %} ccd-exclusive +{%- endif %} + +keepalive {{ ping_interval }} {{ ping_restart }} +management /tmp/openvpn-mgmt-intf unix + +{% for route in server_push_route -%} +push "route {{ route }}" +{% endfor -%} + +{% for ns in server_dns_nameserver -%} +push "dhcp-option DNS {{ ns }}" +{% endfor -%} + +{%- if server_domain -%} +push "dhcp-option DOMAIN {{ server_domain }}" {% endif %} -{%- else %} +{% else -%} # # OpenVPN site-2-site mode # ping {{ ping_interval }} ping-restart {{ ping_restart }} -{%- if local_address_subnet %} +{% if local_address_subnet -%} ifconfig {{ local_address }} {{ local_address_subnet }} -{% elif remote_address %} +{%- elif remote_address -%} ifconfig {{ local_address }} {{ remote_address }} -{% endif %} +{%- endif %} -{% endif %} -{% endif %} +{% endif -%} +{% if tls -%} +# TLS options {%- if tls_ca_cert %} ca {{ tls_ca_cert }} -{% endif %} +{%- endif %} {%- if tls_cert %} cert {{ tls_cert }} -{% endif %} +{%- endif %} {%- if tls_key %} key {{ tls_key }} -{% endif %} +{%- endif %} {%- if tls_crypt %} tls-crypt {{ tls_crypt }} -{% endif %} +{%- endif %} {%- if tls_crl %} crl-verify {{ tls_crl }} -{% endif %} +{%- endif %} {%- if tls_version_min %} tls-version-min {{tls_version_min}} -{% endif %} +{%- endif %} {%- if tls_dh %} dh {{ tls_dh }} -{% endif %} +{%- endif %} {%- if tls_auth %} tls-auth {{tls_auth}} -{% endif %} +{%- endif %} +{%- if tls_role %} {%- if 'active' in tls_role %} tls-client {%- elif 'passive' in tls_role %} tls-server -{% endif %} +{%- endif %} +{%- endif %} -{%- if redirect_gateway %} -push "redirect-gateway {{ redirect_gateway }}" -{% endif %} - -{%- if compress_lzo %} -compress lzo -{% endif %} - -{%- if hash %} -auth {{ hash }} -{% endif %} +{%- endif %} +# Encryption options {%- if encryption %} -{%- if 'des' in encryption %} +{% if encryption == 'des' -%} cipher des-cbc -{%- elif '3des' in encryption %} +{%- elif encryption == '3des' -%} cipher des-ede3-cbc -{%- elif 'bf128' in encryption %} +{%- elif encryption == 'bf128' -%} cipher bf-cbc keysize 128 -{%- elif 'bf256' in encryption %} +{%- elif encryption == 'bf256' -%} cipher bf-cbc keysize 25 -{%- elif 'aes128gcm' in encryption %} +{%- elif encryption == 'aes128gcm' -%} cipher aes-128-gcm -{%- elif 'aes128' in encryption %} +{%- elif encryption == 'aes128' -%} cipher aes-128-cbc -{%- elif 'aes192gcm' in encryption %} +{%- elif encryption == 'aes192gcm' -%} cipher aes-192-gcm -{%- elif 'aes192' in encryption %} +{%- elif encryption == 'aes192' -%} cipher aes-192-cbc -{%- elif 'aes256gcm' in encryption %} +{%- elif encryption == 'aes256gcm' -%} cipher aes-256-gcm -{%- elif 'aes256' in encryption %} +{%- elif encryption == 'aes256' -%} cipher aes-256-cbc -{% endif %} -{% endif %} +{%- endif -%} +{%- endif %} {%- if ncp_ciphers %} ncp-ciphers {{ncp_ciphers}} -{% endif %} +{%- endif %} {%- if disable_ncp %} ncp-disable -{% endif %} +{%- endif %} + +{% if hash -%} +auth {{ hash }} +{%- 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 %} +{%- 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: @@ -218,6 +228,12 @@ client-config-dir /opt/vyatta/etc/openvpn/ccd/{{ intf }} # See https://phabricator.vyos.net/T1512 compat-names +{% if options -%} +# +# Custom options added by user (not validated) +# + {% for option in options -%} {{ option }} {% endfor -%} +{%- endif %} diff --git a/data/templates/pppoe/ip-down.script.tmpl b/data/templates/pppoe/ip-down.script.tmpl index e76875f12..a68fc099c 100644 --- a/data/templates/pppoe/ip-down.script.tmpl +++ b/data/templates/pppoe/ip-down.script.tmpl @@ -10,8 +10,9 @@ fi DIALER_PID=$(cat /var/run/{{ intf }}.pid) logger -t pppd[$DIALER_PID] "executing $0" -# Determine if we are enslaved to a VRF, this is needed to properly insert -# the default route +{% if not on_demand %} +# See https://phabricator.vyos.net/T2248. Determine if we are enslaved to a +# VRF, this is needed to properly insert the default route. VRF_NAME="" if [ -d /sys/class/net/{{ intf }}/upper_* ]; then # Determine upper (VRF) interface @@ -24,3 +25,4 @@ fi # Always delete default route when interface goes down vtysh -c "conf t" ${VRF_NAME} -c "no ip route 0.0.0.0/0 {{ intf }} ${VRF_NAME}" +{% endif %} diff --git a/data/templates/pppoe/ip-up.script.tmpl b/data/templates/pppoe/ip-up.script.tmpl index 4cc779914..697ebcc20 100644 --- a/data/templates/pppoe/ip-up.script.tmpl +++ b/data/templates/pppoe/ip-up.script.tmpl @@ -6,13 +6,15 @@ if [ "$6" != "{{ intf }}" ]; then exit fi -set -x +{% if not on_demand %} +# See https://phabricator.vyos.net/T2248 & T2220. Determine if we are enslaved +# to a VRF, this is needed to properly insert the default route. # add some info to syslog DIALER_PID=$(cat /var/run/{{ intf }}.pid) logger -t pppd[$DIALER_PID] "executing $0" -SED_OPT="ip route" +SED_OPT="^ip route" VRF_NAME="" if [ -d /sys/class/net/{{ intf }}/upper_* ]; then # Determine upper (VRF) interface @@ -25,10 +27,8 @@ if [ -d /sys/class/net/{{ intf }}/upper_* ]; then VRF_NAME="vrf ${VRF}" fi -# Debian PPP version has no support for replacing an existing default route -# thus we emulate this ba an ip-up script https://phabricator.vyos.net/T2220. {% if 'auto' in default_route -%} -# only insert a new default route if there is no default route configured +# Only insert a new default route if there is no default route configured routes=$(vtysh -c "show running-config" | sed -n "/${SED_OPT}/,/!/p" | grep 0.0.0.0/0 | wc -l) if [ "$routes" -ne 0 ]; then exit 1 @@ -44,4 +44,4 @@ done # Add default route to default or VRF routing table vtysh -c "conf t" ${VTY_OPT} -c "ip route 0.0.0.0/0 {{ intf }} ${VRF_NAME}" logger -t pppd[$DIALER_PID] "added default route via {{ intf }} ${VRF_NAME}" - +{% endif %} diff --git a/data/templates/pppoe/peer.tmpl b/data/templates/pppoe/peer.tmpl index 8651f12a5..36d108cee 100644 --- a/data/templates/pppoe/peer.tmpl +++ b/data/templates/pppoe/peer.tmpl @@ -60,4 +60,13 @@ rp_pppoe_service "{{ service_name }}" {% endif %} {% if on_demand %} demand +# See T2249. PPP default route options should only be set when in on-demand +# mode. As soon as we are not in on-demand mode the default-route handling is +# passed to the ip-up.d/ip-down.s scripts which is required for VRF support. +{% if 'auto' in default_route -%} +defaultroute +{% elif 'force' in default_route -%} +defaultroute +replacedefaultroute +{% endif %} {% endif %} diff --git a/data/templates/sstp/sstp.config.tmpl b/data/templates/sstp/sstp.config.tmpl index 19805358e..acdb6c76b 100644 --- a/data/templates/sstp/sstp.config.tmpl +++ b/data/templates/sstp/sstp.config.tmpl @@ -30,6 +30,7 @@ disable [sstp] verbose=1 +ifname=sstp%d accept=ssl ssl-ca-file={{ ssl_ca }} ssl-pemfile={{ ssl_cert }} @@ -52,7 +53,7 @@ dns{{ loop.index }}={{ dns }} {% if auth_mode == 'local' %} [chap-secrets] -chap-secrets=/etc/accel-ppp/sstp/chap-secrets +chap-secrets={{ chap_secrets_file }} {% elif auth_mode == 'radius' %} [radius] verbose=1 diff --git a/data/templates/wifi/hostapd.conf.tmpl b/data/templates/wifi/hostapd.conf.tmpl index 031fb6c90..e2fb9ca8f 100644 --- a/data/templates/wifi/hostapd.conf.tmpl +++ b/data/templates/wifi/hostapd.conf.tmpl @@ -73,7 +73,6 @@ channel={{ channel }} # offloaded ACS is used. {% if 'n' in mode -%} hw_mode=g -ieee80211n=1 {% elif 'ac' in mode -%} hw_mode=a ieee80211h=1 @@ -421,6 +420,12 @@ vht_capab= ieee80211n=0 # Require stations to support VHT PHY (reject association if they do not) require_vht=1 +{% else -%} +{% if 'n' in mode or 'ac' in mode -%} +ieee80211n=1 +{% else -%} +ieee80211n=0 +{%- endif %} {% endif %} {% if cap_vht_center_freq_1 -%} diff --git a/debian/control b/debian/control index bccfc02d4..7b95b2c75 100644 --- a/debian/control +++ b/debian/control @@ -32,6 +32,7 @@ Depends: python3, python3-netaddr, python3-zmq, cron, + easy-rsa, ipaddrcheck, tcpdump, tshark, diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install index 5004d111f..dd8eebc0b 100644 --- a/debian/vyos-1x.install +++ b/debian/vyos-1x.install @@ -1,5 +1,4 @@ etc/dhcp -etc/init.d etc/ppp etc/rsyslog.d etc/systemd diff --git a/interface-definitions/include/interface-hw-id.xml.i b/interface-definitions/include/interface-hw-id.xml.i new file mode 100644 index 000000000..cefc9f0a0 --- /dev/null +++ b/interface-definitions/include/interface-hw-id.xml.i @@ -0,0 +1,12 @@ +<leafNode name="mac"> + <properties> + <help>Associate Ethernet Interface with given Media Access Control (MAC) address</help> + <valueHelp> + <format>h:h:h:h:h:h</format> + <description>Hardware Media Access Control (MAC) address</description> + </valueHelp> + <constraint> + <validator name="mac-address"/> + </constraint> + </properties> +</leafNode> diff --git a/interface-definitions/include/port-number.xml.i b/interface-definitions/include/port-number.xml.i new file mode 100644 index 000000000..78eb4b7af --- /dev/null +++ b/interface-definitions/include/port-number.xml.i @@ -0,0 +1,12 @@ +<leafNode name="port">
+ <properties>
+ <help>Port number used to establish connection</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/radius-server.xml.i b/interface-definitions/include/radius-server.xml.i index d1068b0e4..047728233 100644 --- a/interface-definitions/include/radius-server.xml.i +++ b/interface-definitions/include/radius-server.xml.i @@ -8,7 +8,7 @@ <help>RADIUS client source address</help> <valueHelp> <format>ipv4</format> - <description>TFTP IPv4 listen address</description> + <description>IPv4 source-address of RADIUS queries</description> </valueHelp> <constraint> <validator name="ipv4-address"/> diff --git a/interface-definitions/include/vif-s.xml.i b/interface-definitions/include/vif-s.xml.i index 2120aa32d..ab2dcd955 100644 --- a/interface-definitions/include/vif-s.xml.i +++ b/interface-definitions/include/vif-s.xml.i @@ -58,6 +58,7 @@ #include <include/interface-disable.xml.i> #include <include/interface-mac.xml.i> #include <include/interface-mtu-68-9000.xml.i> + #include <include/interface-vrf.xml.i> </children> </tagNode> </children> diff --git a/interface-definitions/interfaces-ethernet.xml.in b/interface-definitions/interfaces-ethernet.xml.in index f8ec26d04..89669f966 100644 --- a/interface-definitions/interfaces-ethernet.xml.in +++ b/interface-definitions/interfaces-ethernet.xml.in @@ -56,18 +56,7 @@ <constraintErrorMessage>duplex must be auto, half or full</constraintErrorMessage> </properties> </leafNode> - <leafNode name="hw-id"> - <properties> - <help>Media Access Control (MAC) address</help> - <valueHelp> - <format>h:h:h:h:h:h</format> - <description>Hardware (MAC) address</description> - </valueHelp> - <constraint> - <validator name="mac-address"/> - </constraint> - </properties> - </leafNode> + #include <include/interface-hw-id.xml.i> <node name="ip"> <children> #include <include/interface-arp-cache-timeout.xml.i> diff --git a/interface-definitions/interfaces-openvpn.xml.in b/interface-definitions/interfaces-openvpn.xml.in index 92bac3fab..d926876f7 100644 --- a/interface-definitions/interfaces-openvpn.xml.in +++ b/interface-definitions/interfaces-openvpn.xml.in @@ -444,6 +444,52 @@ </leafNode> </children> </tagNode> + <node name="client-ip-pool"> + <properties> + <help>Pool of client IP addresses</help> + </properties> + <children> + <leafNode name="start"> + <properties> + <help>First IP address in the pool</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="stop"> + <properties> + <help>Last IP address in the pool</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="subnet-mask"> + <properties> + <help>Subnet mask pushed to dynamic clients. + If not set the server subnet mask will be used. + Only used with topology subnet or device type tap. + Not used with bridged interfaces.</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <valueHelp> + <format>ipv4</format> + <description>IPv4 subnet mask</description> + </valueHelp> + </properties> + </leafNode> + </children> + </node> <leafNode name="domain-name"> <properties> <help>DNS suffix to be pushed to all clients</help> @@ -501,7 +547,7 @@ <help>Server-mode subnet (from which client IPs are allocated)</help> <valueHelp> <format>ipv4net</format> - <description>IPv4 address and prefix length</description> + <description>IPv4 network and prefix length</description> </valueHelp> <constraint> <validator name="ipv4-prefix"/> @@ -512,9 +558,13 @@ <properties> <help>Topology for clients</help> <completionHelp> - <list>point-to-point subnet</list> + <list>net30 point-to-point subnet</list> </completionHelp> <valueHelp> + <format>net30</format> + <description>net30 topology</description> + </valueHelp> + <valueHelp> <format>point-to-point</format> <description>Point-to-point topology</description> </valueHelp> diff --git a/interface-definitions/interfaces-pseudo-ethernet.xml.in b/interface-definitions/interfaces-pseudo-ethernet.xml.in index 0c56e4e4b..c6e61d19a 100644 --- a/interface-definitions/interfaces-pseudo-ethernet.xml.in +++ b/interface-definitions/interfaces-pseudo-ethernet.xml.in @@ -40,9 +40,9 @@ #include <include/ipv6-dup-addr-detect-transmits.xml.i> </children> </node> - <leafNode name="link"> + <leafNode name="source-interface"> <properties> - <help>Lower link device</help> + <help>Physical Interface used for this device</help> <valueHelp> <format>interface</format> <description>Interface used for VXLAN underlay</description> diff --git a/interface-definitions/interfaces-tunnel.xml.in b/interface-definitions/interfaces-tunnel.xml.in index 3ba82067f..e1ac60319 100644 --- a/interface-definitions/interfaces-tunnel.xml.in +++ b/interface-definitions/interfaces-tunnel.xml.in @@ -107,7 +107,7 @@ </leafNode> <leafNode name="encapsulation"> <properties> - <help>Ignore link state changes</help> + <help>Encapsulation of this tunnel interface</help> <completionHelp> <list>gre gre-bridge ipip sit ipip6 ip6ip6 ip6gre</list> </completionHelp> diff --git a/interface-definitions/interfaces-vxlan.xml.in b/interface-definitions/interfaces-vxlan.xml.in index 3108817b3..fdde57525 100644 --- a/interface-definitions/interfaces-vxlan.xml.in +++ b/interface-definitions/interfaces-vxlan.xml.in @@ -52,9 +52,21 @@ #include <include/ipv6-dup-addr-detect-transmits.xml.i> </children> </node> - <leafNode name="link"> + <leafNode name="source-address"> <properties> - <help>Underlay device of VXLAN interface</help> + <help>VXLAN source address</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 source-address of VXLAN tunnel</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="source-interface"> + <properties> + <help>Physical Interface used for this connection</help> <valueHelp> <format>interface</format> <description>Interface used for VXLAN underlay</description> diff --git a/interface-definitions/interfaces-wireguard.xml.in b/interface-definitions/interfaces-wireguard.xml.in index d3f084774..9db608afb 100644 --- a/interface-definitions/interfaces-wireguard.xml.in +++ b/interface-definitions/interfaces-wireguard.xml.in @@ -19,26 +19,9 @@ #include <include/address-ipv4-ipv6.xml.i> #include <include/interface-description.xml.i> #include <include/interface-disable.xml.i> - <leafNode name="port"> - <properties> - <help>Local port to listen for incoming connections</help> - <valueHelp> - <format>1-65535</format> - <description>Numeric IP port</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-65535"/> - </constraint> - </properties> - </leafNode> - <leafNode name="mtu"> - <properties> - <help>interface mtu size(default: 1420)</help> - <constraint> - <validator name="numeric" argument="--range 68-9000"/> - </constraint> - </properties> - </leafNode> + #include <include/interface-vrf.xml.i> + #include <include/port-number.xml.i> + #include <include/interface-mtu-68-9000.xml.i> <leafNode name="fwmark"> <properties> <help>A 32-bit fwmark value set on all outgoing packets</help> @@ -113,18 +96,7 @@ </constraint> </properties> </leafNode> - <leafNode name="port"> - <properties> - <help>Port number on tunnel remote end</help> - <valueHelp> - <format>1-65535</format> - <description>Numeric IP port</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-65535"/> - </constraint> - </properties> - </leafNode> + #include <include/port-number.xml.i> <leafNode name="persistent-keepalive"> <properties> <help>how often send keep alives in seconds</help> diff --git a/interface-definitions/interfaces-wireless.xml.in b/interface-definitions/interfaces-wireless.xml.in index 194669f77..a5c6315fa 100644 --- a/interface-definitions/interfaces-wireless.xml.in +++ b/interface-definitions/interfaces-wireless.xml.in @@ -476,18 +476,7 @@ #include <include/ipv6-dup-addr-detect-transmits.xml.i> </children> </node> - <leafNode name="hw-id"> - <properties> - <help>Media Access Control (MAC) address</help> - <valueHelp> - <format>h:h:h:h:h:h</format> - <description>Hardware (MAC) address</description> - </valueHelp> - <constraint> - <validator name="mac-address"/> - </constraint> - </properties> - </leafNode> + #include <include/interface-hw-id.xml.i> <leafNode name="isolate-stations"> <properties> <help>Isolate stations on the AP so they cannot see each other</help> diff --git a/interface-definitions/l2tp-server.xml.in b/interface-definitions/vpn-l2tp.xml.in index 7fc844054..d4286a810 100644 --- a/interface-definitions/l2tp-server.xml.in +++ b/interface-definitions/vpn-l2tp.xml.in @@ -2,7 +2,7 @@ <interfaceDefinition> <node name="vpn"> <children> - <node name="l2tp" owner="${vyos_conf_scripts_dir}/accel_l2tp.py"> + <node name="l2tp" owner="${vyos_conf_scripts_dir}/vpn_l2tp.py"> <properties> <help>L2TP Virtual Private Network (VPN)</help> </properties> @@ -36,48 +36,22 @@ </constraint> </properties> </leafNode> - <node name="dns-servers"> + <leafNode name="name-server"> <properties> - <help>IPv4 Domain Name Service (DNS) server</help> - </properties> - <children> - <leafNode name="server-1"> - <properties> - <help>Primary DNS server</help> - <valueHelp> - <format>ipv4</format> - <description>IPv4 address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> - <leafNode name="server-2"> - <properties> - <help>Secondary DNS server</help> - <valueHelp> - <format>ipv4</format> - <description>IPv4 address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> - </children> - </node> - <leafNode name="dnsv6-servers"> - <properties> - <help>IPv6 Domain Name Service (DNS) server</help> + <help>Domain Name Server (DNS) propagated to client</help> <valueHelp> - <format>ipv6</format> - <description>IPv6 DNS address</description> + <format>ipv4</format> + <description>Domain Name Server (DNS) IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Domain Name Server (DNS) IPv6 address</description> </valueHelp> <constraint> + <validator name="ipv4-address"/> <validator name="ipv6-address"/> </constraint> - <multi /> + <multi/> </properties> </leafNode> <node name="lns"> @@ -208,29 +182,19 @@ </leafNode> </children> </node> - <node name="wins-servers"> + <leafNode name="wins-server"> <properties> - <help>Windows Internet Name Service (WINS) server settings</help> + <help>Windows Internet Name Service (WINS) servers propagated to client</help> + <valueHelp> + <format>ipv4</format> + <description>Domain Name Server (DNS) IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> </properties> - <children> - <leafNode name="server-1"> - <properties> - <help>Primary WINS server</help> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> - <leafNode name="server-2"> - <properties> - <help>Secondary WINS server</help> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> - </children> - </node> + </leafNode> <node name="client-ip-pool"> <properties> <help>Pool of client IP addresses (must be within a /24)</help> @@ -273,26 +237,58 @@ <help>Pool of client IPv6 addresses</help> </properties> <children> - <leafNode name="prefix"> + <tagNode name="prefix"> <properties> - <help>IPV6 prefix delegation</help> + <help>Pool of addresses used to assign to clients</help> <valueHelp> - <format>ipv6prefix/mask,prefix_len</format> - <description>e.g.: fc00:0:1::/48,64 - divides prefix into /64 subnets for clients</description> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> </valueHelp> - <multi /> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> </properties> - </leafNode> - <leafNode name="delegate-prefix"> + <children> + <leafNode name="mask"> + <properties> + <help>Prefix length used for individual client</help> + <valueHelp> + <format><48-128></format> + <description>Client prefix length (default: 64)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 48-128"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + <tagNode name="delegate"> <properties> - <help>DHCPv6 prefix delegation - rfc3633</help> + <help>Subnet used to delegate prefix through DHCPv6-PD (RFC3633)</help> <valueHelp> - <format>ipv6prefix/mask,prefix_len</format> - <description>Delegate to clients through DHCPv6 prefix delegation - rfc3633</description> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> </valueHelp> - <multi /> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> </properties> - </leafNode> + <children> + <leafNode name="delegation-prefix"> + <properties> + <help>Prefix length delegated to client</help> + <valueHelp> + <format><32-64></format> + <description>Delegated prefix length</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 32-64"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> </children> </node> <leafNode name="description"> @@ -445,46 +441,26 @@ </tagNode> </children> </node> + #include <include/radius-server.xml.i> <node name="radius"> - <properties> - <help>RADIUS specific configuration</help> - </properties> <children> <tagNode name="server"> - <properties> - <help>IP address of RADIUS server</help> - <valueHelp> - <format>ipv4</format> - <description>IPv4 address of RADIUS server</description> - </valueHelp> - </properties> <children> - <leafNode name="key"> - <properties> - <help>Key for accessing the specified server</help> - </properties> - </leafNode> - <leafNode name="req-limit"> - <properties> - <help>Maximum number of simultaneous requests to server (default: unlimited)</help> - </properties> - </leafNode> <leafNode name="fail-time"> <properties> - <help>If server doesn not responds mark it unavailable for this time (seconds)</help> + <help>Mark server unavailable for <n> seconds on failure</help> + <valueHelp> + <format>0-600</format> + <description>Fail time penalty</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-600"/> + </constraint> + <constraintErrorMessage>Fail time must be between 0 and 600 seconds</constraintErrorMessage> </properties> </leafNode> </children> </tagNode> - <leafNode name="source-address"> - <properties> - <help>Local RADIUS client address from which packets are sent.</help> - <valueHelp> - <format><x.x.x.x></format> - <description>Local RADIUS client address from which packets are sent</description> - </valueHelp> - </properties> - </leafNode> <leafNode name="timeout"> <properties> <help>Timeout to wait response from server (seconds)</help> diff --git a/op-mode-definitions/ipoe-server.xml b/op-mode-definitions/ipoe-server.xml index 369ceebea..c05e2d2c1 100644 --- a/op-mode-definitions/ipoe-server.xml +++ b/op-mode-definitions/ipoe-server.xml @@ -1,5 +1,41 @@ <?xml version="1.0"?> <interfaceDefinition> + <node name="reset"> + <children> + <node name="ipoe-server"> + <properties> + <help>Clear ipoe-server sessions or process</help> + </properties> + <children> + <node name="session"> + <properties> + <help>Clear ipoe-server session</help> + </properties> + <children> + <leafNode name="username"> + <properties> + <help>Clear ipoe-server session by username</help> + <completionHelp> + <script>/usr/bin/accel-cmd -p 2002 show sessions username | sed -e 's/ \r//g' | tail -n +3</script> + </completionHelp> + </properties> + <command>/usr/bin/accel-cmd -p 2002 terminate username $5</command> + </leafNode> + <leafNode name="sid"> + <properties> + <help>Clear ipoe-server session by sid</help> + <completionHelp> + <script>/usr/bin/accel-cmd -p 2002 show sessions sid | sed -e 's/ \r//g' | tail -n +3</script> + </completionHelp> + </properties> + <command>/usr/bin/accel-cmd -p 2002 terminate sid $5</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> <node name="show"> <children> <node name="ipoe-server"> @@ -11,7 +47,7 @@ <properties> <help>Show active IPoE server sessions</help> </properties> - <command>/usr/bin/accel-cmd -p 2002 show sessions ifname,called-sid,calling-sid,ip,ip6,ip6-dp,rate-limit,state,uptime,sid</command> + <command>/usr/bin/accel-cmd -p 2002 show sessions ifname,username,called-sid,calling-sid,ip,ip6,ip6-dp,rate-limit,state,uptime,sid</command> </leafNode> <leafNode name="statistics"> <properties> diff --git a/op-mode-definitions/reset-vpn.xml b/op-mode-definitions/reset-vpn.xml index c0b0ddeb1..ae553c272 100644 --- a/op-mode-definitions/reset-vpn.xml +++ b/op-mode-definitions/reset-vpn.xml @@ -37,6 +37,12 @@ </properties> <command>sudo ${vyos_op_scripts_dir}/reset_vpn.py --username="all_users" --protocol="pptp"</command> </leafNode> + <leafNode name="sstp"> + <properties> + <help>Terminate all user's current remote access VPN session(s) with SSTP protocol</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/reset_vpn.py --username="all_users" --protocol="sstp"</command> + </leafNode> </children> </node> </children> @@ -62,13 +68,19 @@ <properties> <help>Terminate all user's current remote access VPN session(s) with L2TP protocol</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/reset_vpn.py --username="all_users" --protocol="l2tp"</command> + <command>sudo ${vyos_op_scripts_dir}/reset_vpn.py --username="$5" --protocol="l2tp"</command> </leafNode> <leafNode name="pptp"> <properties> <help>Terminate all user's current remote access VPN session(s) with PPTP protocol</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/reset_vpn.py --username="all_users" --protocol="pptp"</command> + <command>sudo ${vyos_op_scripts_dir}/reset_vpn.py --username="$5" --protocol="pptp"</command> + </leafNode> + <leafNode name="sstp"> + <properties> + <help>Terminate all user's current remote access VPN session(s) with SSTP protocol</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/reset_vpn.py --username="$5" --protocol="sstp"</command> </leafNode> </children> </node> diff --git a/op-mode-definitions/show-raid.xml b/op-mode-definitions/show-raid.xml index 000fd4610..8bf394552 100644 --- a/op-mode-definitions/show-raid.xml +++ b/op-mode-definitions/show-raid.xml @@ -4,7 +4,7 @@ <children> <tagNode name="raid"> <properties> - <help>Show statis of RAID set</help> + <help>Show status of RAID set</help> <completionHelp> <script>${vyos_completion_dir}/list_raidset.sh</script> </completionHelp> diff --git a/python/vyos/__init__.py b/python/vyos/__init__.py index 9b5ed21c9..e3e14fdd8 100644 --- a/python/vyos/__init__.py +++ b/python/vyos/__init__.py @@ -1 +1 @@ -from .base import * +from .base import ConfigError diff --git a/python/vyos/airbag.py b/python/vyos/airbag.py new file mode 100644 index 000000000..b0565192d --- /dev/null +++ b/python/vyos/airbag.py @@ -0,0 +1,168 @@ +# Copyright 2019-2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys +import logging +import logging.handlers +from datetime import datetime + +from vyos import debug +from vyos.config import Config +from vyos.version import get_version +from vyos.util import run + + +# we allow to disable the extra logging +DISABLE = False + + +# emulate a file object +class _IO(object): + def __init__(self, std, log): + self.std = std + self.log = log + + def write(self, message): + self.std.write(message) + if DISABLE: + return + for line in message.split('\n'): + s = line.rstrip() + if s: + self.log(s) + + def flush(self): + self.std.flush() + + def close(self): + pass + + +# The function which will be used to report information +# to users when an exception is unhandled +def bug_report(dtype, value, trace): + from traceback import format_exception + + sys.stdout.flush() + sys.stderr.flush() + + information = { + 'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'version': get_version(), + 'trace': format_exception(dtype, value, trace), + 'instructions': COMMUNITY if 'rolling' in get_version() else SUPPORTED, + } + + sys.stdout.write(INTRO.format(**information)) + sys.stdout.flush() + + sys.stderr.write(FAULT.format(**information)) + sys.stderr.flush() + + +# define an exception handler to be run when an exception +# reach the end of __main__ and was not intercepted +def intercepter(dtype, value, trace): + bug_report(dtype, value, trace) + if debug.enabled('developer'): + import pdb + pdb.pm() + + +def InterceptingLogger(address, _singleton=[False]): + skip = _singleton.pop() + _singleton.append(True) + if skip: + return + + logger = logging.getLogger('VyOS') + logger.setLevel(logging.DEBUG) + handler = logging.handlers.SysLogHandler(address='/dev/log', facility='syslog') + logger.addHandler(handler) + + # log to syslog any message sent to stderr + sys.stderr = _IO(sys.stderr, logger.critical) + + +# lists as default arguments in function is normally dangerous +# as they will keep any modification performed, unless this is +# what you want to do (in that case to only run the code once) +def InterceptingException(excepthook,_singleton=[False]): + skip = _singleton.pop() + _singleton.append(True) + if skip: + return + + # install the handler to replace the default behaviour + # which just prints the exception trace on screen + sys.excepthook = excepthook + + +# Do not attempt the extra logging for operational commands +try: + # This fails during boot + insession = Config().in_session() +except: + # we save info on boot to help debugging + insession = True + + +# Installing the interception, it currently does not work when +# running testing so we are checking that we are on the router +# as otherwise it prevents dpkg-buildpackage to work +if get_version() and insession: + InterceptingLogger('/run/systemd/journal/dev-log') + InterceptingException(intercepter) + + +# Messages to print + +FAULT = """\ +Date: {date} +VyOS image: {version} + +{trace} +""" + +INTRO = """\ +VyOS had an issue completing a command. + +We are sorry that you encountered a problem with VyOS. +There are a few things you can do to help us (and yourself): +{instructions} + +PLEASE, when reporting, do include as much information as you can: +- do not obfuscate any data (feel free to send us a private communication with + the extra information if your business policy is strict on information sharing) +- and include all the information presented below + +""" + +COMMUNITY = """\ +- Make sure you are running the latest version of the code available at + https://downloads.vyos.io/rolling/current/amd64/vyos-rolling-latest.iso +- Consult the forum to see how to handle this issue + https://forum.vyos.io +- Join our community on slack where our users exchange help and advice + https://vyos.slack.com +""".strip() + +SUPPORTED = """\ +- Make sure you are running the latest stable version of VyOS + the code is available at https://downloads.vyos.io/?dir=release/current +- Contact us on our online help desk + https://support.vyos.io/ +""".strip() diff --git a/python/vyos/authutils.py b/python/vyos/authutils.py index 90a46ffb4..66b5f4a74 100644 --- a/python/vyos/authutils.py +++ b/python/vyos/authutils.py @@ -22,7 +22,7 @@ def make_password_hash(password): """ Makes a password hash for /etc/shadow using mkpasswd """ mkpassword = 'mkpasswd --method=sha-512 --stdin' - return cmd(mkpassword, input=password.encode(), timeout=5) + return cmd(mkpassword, input=password, timeout=5) def split_ssh_public_key(key_string, defaultname=""): """ Splits an SSH public key into its components """ diff --git a/python/vyos/debug.py b/python/vyos/debug.py new file mode 100644 index 000000000..20090fb85 --- /dev/null +++ b/python/vyos/debug.py @@ -0,0 +1,182 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys + + +def message(message, flag='', destination=sys.stdout): + """ + print a debug message line on stdout if debugging is enabled for the flag + also log it to a file if the flag 'log' is enabled + + message: the message to print + flag: which flag must be set for it to print + destination: which file like object to write to (default: sys.stdout) + + returns if any message was logged or not + """ + enable = enabled(flag) + if enable: + destination.write(_format(flag,message)) + + # the log flag is special as it logs all the commands + # executed to a log + logfile = _logfile('log', '/tmp/developer-log') + if not logfile: + return enable + + try: + # at boot the file is created as root:vyattacfg + # at runtime the file is created as user:vyattacfg + # the default permission are 644 + mask = os.umask(0o113) + + with open(logfile, 'a') as f: + f.write(_format('log', message)) + finally: + os.umask(mask) + + return enable + + +def enabled(flag): + """ + a flag can be set by touching the file in /tmp or /config + + The current flags are: + - developer: the code will drop into PBD on un-handled exception + - log: the code will log all command to a file + - ifconfig: when modifying an interface, + prints command with result and sysfs access on stdout for interface + - command: print command run with result + + Having the flag setup on the filesystem is required to have + debuging at boot time, however, setting the flag via environment + does not require a seek to the filesystem and is more efficient + it can be done on the shell on via .bashrc for the user + + The function returns an empty string if the flag was not set otherwise + the function returns either the file or environment name used to set it up + """ + + # this is to force all new flags to be registered here to be + # documented both here and a reminder to update readthedocs :-) + if flag not in ['developer', 'log', 'ifconfig', 'command']: + return '' + + return _fromenv(flag) or _fromfile(flag) + + +def _format(flag, message): + """ + format a log message + """ + return f'DEBUG/{flag.upper():<7} {message}\n' + + +def _fromenv(flag): + """ + check if debugging is set for this flag via environment + + For a given debug flag named "test" + The presence of the environment VYOS_TEST_DEBUG (uppercase) enables it + + return empty string if not + return content of env value it is + """ + + flagname = f'VYOS_{flag.upper()}_DEBUG' + flagenv = os.environ.get(flagname, None) + + if flagenv is None: + return '' + return flagenv + + +def _fromfile(flag): + """ + Check if debug exist for a given debug flag name + + Check is a debug flag was set by the user. the flag can be set either: + - in /tmp for a non-persistent presence between reboot + - in /config for always on (an existence at boot time) + + For a given debug flag named "test" + The presence of the file vyos.test.debug (all lowercase) enables it + + The function returns an empty string if the flag was not set otherwise + the function returns the full flagname + """ + + for folder in ('/tmp', '/config'): + flagfile = f'{folder}/vyos.{flag}.debug' + if os.path.isfile(flagfile): + return flagfile + + return '' + + +def _contentenv(flag): + return os.environ.get(f'VYOS_{flag.upper()}_DEBUG', '').strip() + + +def _contentfile(flag): + """ + Check if debug exist for a given debug flag name + + Check is a debug flag was set by the user. the flag can be set either: + - in /tmp for a non-persistent presence between reboot + - in /config for always on (an existence at boot time) + + For a given debug flag named "test" + The presence of the file vyos.test.debug (all lowercase) enables it + + The function returns an empty string if the flag was not set otherwise + the function returns the full flagname + """ + + for folder in ('/tmp', '/config'): + flagfile = f'{folder}/vyos.{flag}.debug' + if not os.path.isfile(flagfile): + continue + with open(flagfile) as f: + return f.readline().strip() + + return '' + + +def _logfile(flag, default): + """ + return the name of the file to use for logging when the flag 'log' is set + if it could not be established or the location is invalid it returns + an empty string + """ + + # For log we return the location of the log file + log_location = _contentenv(flag) or _contentfile(flag) + + # it was not set + if not log_location: + return '' + + # Make sure that the logs can only be in /tmp, /var/log, or /tmp + if not log_location.startswith('/tmp/') and \ + not log_location.startswith('/config/') and \ + not log_location.startswith('/var/log/'): + return default + if '..' in log_location: + return default + return log_location diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index a2ad142bc..88894674f 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -21,6 +21,7 @@ directories = { "current": "/opt/vyatta/etc/config-migrate/current", "migrate": "/opt/vyatta/etc/config-migrate/migrate", "log": "/var/log/vyatta", + "templates": "/usr/share/vyos/templates/" } cfg_group = 'vyattacfg' diff --git a/python/vyos/dicts.py b/python/vyos/dicts.py new file mode 100644 index 000000000..79cab4a08 --- /dev/null +++ b/python/vyos/dicts.py @@ -0,0 +1,50 @@ +#!/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/>. + + +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) diff --git a/python/vyos/ifconfig/__init__.py b/python/vyos/ifconfig/__init__.py index 1f9956af0..cd1696ca1 100644 --- a/python/vyos/ifconfig/__init__.py +++ b/python/vyos/ifconfig/__init__.py @@ -14,6 +14,7 @@ # License along with this library. If not, see <http://www.gnu.org/licenses/>. +from vyos.ifconfig.section import Section from vyos.ifconfig.interface import Interface from vyos.ifconfig.bond import BondIf diff --git a/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py index e2ff71490..47dd4ff34 100644 --- a/python/vyos/ifconfig/bond.py +++ b/python/vyos/ifconfig/bond.py @@ -18,7 +18,8 @@ import os from vyos.ifconfig.interface import Interface from vyos.ifconfig.vlan import VLAN -from vyos.validate import * +from vyos.validate import assert_list +from vyos.validate import assert_positive @Interface.register diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py index 94b0075d8..44b92c1db 100644 --- a/python/vyos/ifconfig/bridge.py +++ b/python/vyos/ifconfig/bridge.py @@ -16,7 +16,8 @@ from vyos.ifconfig.interface import Interface -from vyos.validate import * +from vyos.validate import assert_boolean +from vyos.validate import assert_positive @Interface.register diff --git a/python/vyos/ifconfig/control.py b/python/vyos/ifconfig/control.py index c7a2fa2d6..7bb63beed 100644 --- a/python/vyos/ifconfig/control.py +++ b/python/vyos/ifconfig/control.py @@ -16,12 +16,13 @@ import os -from vyos.util import debug, debug_msg -from vyos.util import popen, cmd -from vyos.ifconfig.register import Register +from vyos import debug +from vyos.util import popen +from vyos.util import cmd +from vyos.ifconfig.section import Section -class Control(Register): +class Control(Section): _command_get = {} _command_set = {} @@ -35,10 +36,10 @@ class Control(Register): # if debug is not explicitely disabled the the config, enable it self.debug = '' if kargs.get('debug', True): - self.debug = debug('ifconfig') + self.debug = debug.enabled('ifconfig') def _debug_msg (self, message): - return debug_msg(message, self.debug) + return debug.message(message, self.debug) def _popen(self, command): return popen(command, self.debug) diff --git a/python/vyos/ifconfig/dhcp.py b/python/vyos/ifconfig/dhcp.py index 8ec8263b5..d4ff9c2cd 100644 --- a/python/vyos/ifconfig/dhcp.py +++ b/python/vyos/ifconfig/dhcp.py @@ -14,105 +14,37 @@ # License along with this library. If not, see <http://www.gnu.org/licenses/>. import os -import jinja2 +from vyos.dicts import FixedDict from vyos.ifconfig.control import Control +from vyos.template import render -template_v4 = """ -# generated by ifconfig.py -option rfc3442-classless-static-routes code 121 = array of unsigned integer 8; -timeout 60; -retry 300; - -interface "{{ intf }}" { - send host-name "{{ hostname }}"; - {% if client_id -%} - send dhcp-client-identifier "{{ client_id }}"; - {% endif -%} - {% if vendor_class_id -%} - send vendor-class-identifier "{{ vendor_class_id }}"; - {% endif -%} - request subnet-mask, broadcast-address, routers, domain-name-servers, - rfc3442-classless-static-routes, domain-name, interface-mtu; - require subnet-mask; -} - -""" - -template_v6 = """ -# generated by ifconfig.py -interface "{{ intf }}" { - request routers, domain-name-servers, domain-name; -} - -""" - -class DHCP (Control): + +class _DHCP (Control): client_base = r'/var/lib/dhcp/dhclient_' - def __init__ (self, ifname, **kargs): + def __init__(self, ifname, version, **kargs): super().__init__(**kargs) - - # per interface DHCP config files - self._dhcp = { - 4: { - 'ifname': ifname, - 'conf': self.client_base + ifname + '.conf', - 'pid': self.client_base + ifname + '.pid', - 'lease': self.client_base + ifname + '.leases', - 'options': { - 'intf': ifname, - 'hostname': '', - 'client_id': '', - 'vendor_class_id': '' - }, - }, - 6: { - 'ifname': ifname, - 'conf': self.client_base + ifname + '.v6conf', - 'pid': self.client_base + ifname + '.v6pid', - 'lease': self.client_base + ifname + '.v6leases', - 'accept_ra': f'/proc/sys/net/ipv6/conf/{ifname}/accept_ra', - 'options': { - 'intf': ifname, - 'dhcpv6_prm_only': False, - 'dhcpv6_temporary': False - }, - }, + self.version = version + self.file = { + 'ifname': ifname, + 'conf': self.client_base + ifname + '.' + version + 'conf', + 'pid': self.client_base + ifname + '.' + version + 'pid', + 'lease': self.client_base + ifname + '.' + version + 'leases', } - def get_dhcp_options(self): - """ - Return dictionary with supported DHCP options. - - Dictionary should be altered and send back via set_dhcp_options() - so those options are applied when DHCP is run. - """ - return self._dhcp[4]['options'] - - def set_dhcp_options(self, options): - """ - Store new DHCP options used by next run of DHCP client. - """ - self._dhcp[4]['options'] = options - - def get_dhcpv6_options(self): - """ - Return dictionary with supported DHCPv6 options. - - Dictionary should be altered and send back via set_dhcp_options() - so those options are applied when DHCP is run. - """ - return self._dhcp[6]['options'] - - def set_dhcpv6_options(self, options): - """ - Store new DHCP options used by next run of DHCP client. - """ - self._dhcp[6]['options'] = options +class _DHCPv4 (_DHCP): + def __init__(self, ifname): + super().__init__(ifname, '') + self.options = FixedDict(**{ + 'ifname': ifname, + 'hostname': '', + 'client_id': '', + 'vendor_class_id': '' + }) # replace dhcpv4/v6 with systemd.networkd? - def _set_dhcp(self): + def set(self): """ Configure interface as DHCP client. The dhclient binary is automatically started in background! @@ -121,21 +53,16 @@ class DHCP (Control): >>> from vyos.ifconfig import Interface >>> j = Interface('eth0') - >>> j.set_dhcp() + >>> j.dhcp.v4.set() """ - dhcp = self.get_dhcp_options() - if not dhcp['hostname']: + if not self.options['hostname']: # read configured system hostname. # maybe change to vyos hostd client ??? with open('/etc/hostname', 'r') as f: - dhcp['hostname'] = f.read().rstrip('\n') + self.options['hostname'] = f.read().rstrip('\n') - # render DHCP configuration - tmpl = jinja2.Template(template_v4) - dhcp_text = tmpl.render(dhcp) - with open(self._dhcp[4]['conf'], 'w') as f: - f.write(dhcp_text) + render(self.file['conf'], 'dhcp-client/ipv4.tmpl' ,self.options) cmd = 'start-stop-daemon' cmd += ' --start' @@ -146,9 +73,9 @@ class DHCP (Control): cmd += ' --' # now pass arguments to dhclient binary cmd += ' -4 -nw -cf {conf} -pf {pid} -lf {lease} {ifname}' - return self._cmd(cmd.format(**self._dhcp[4])) + return self._cmd(cmd.format(**self.file)) - def _del_dhcp(self): + def delete(self): """ De-configure interface as DHCP clinet. All auto generated files like pid, config and lease will be removed. @@ -157,14 +84,14 @@ class DHCP (Control): >>> from vyos.ifconfig import Interface >>> j = Interface('eth0') - >>> j.del_dhcp() + >>> j.dhcp.v4.delete() """ - if not os.path.isfile(self._dhcp[4]['pid']): + if not os.path.isfile(self.file['pid']): self._debug_msg('No DHCP client PID found') return None - # with open(self._dhcp[4]['pid'], 'r') as f: - # pid = int(f.read()) + # with open(self.file['pid'], 'r') as f: + # pid = int(f.read()) # stop dhclient, we need to call dhclient and tell it should release the # aquired IP address. tcpdump tells me: @@ -178,14 +105,27 @@ class DHCP (Control): # Hostname Option 12, length 10: "vyos" # cmd = '/sbin/dhclient -cf {conf} -pf {pid} -lf {lease} -r {ifname}' - self._cmd(cmd.format(**self._dhcp[4])) + self._cmd(cmd.format(**self.file)) # cleanup old config files for name in ('conf', 'pid', 'lease'): - if os.path.isfile(self._dhcp[4][name]): - os.remove(self._dhcp[4][name]) + if os.path.isfile(self.file[name]): + os.remove(self.file[name]) + - def _set_dhcpv6(self): +class _DHCPv6 (_DHCP): + def __init__(self, ifname): + super().__init__(ifname, 'v6') + self.options = FixedDict(**{ + 'ifname': ifname, + 'dhcpv6_prm_only': False, + 'dhcpv6_temporary': False, + }) + self.file.update({ + 'accept_ra': f'/proc/sys/net/ipv6/conf/{ifname}/accept_ra', + }) + + def set(self): """ Configure interface as DHCPv6 client. The dhclient binary is automatically started in background! @@ -196,22 +136,17 @@ class DHCP (Control): >>> j = Interface('eth0') >>> j.set_dhcpv6() """ - dhcpv6 = self.get_dhcpv6_options() # better save then sorry .. should be checked in interface script # but if you missed it we are safe! - if dhcpv6['dhcpv6_prm_only'] and dhcpv6['dhcpv6_temporary']: + if self.options['dhcpv6_prm_only'] and self.options['dhcpv6_temporary']: raise Exception( 'DHCPv6 temporary and parameters-only options are mutually exclusive!') - # render DHCP configuration - tmpl = jinja2.Template(template_v6) - dhcpv6_text = tmpl.render(dhcpv6) - with open(self._dhcp[6]['conf'], 'w') as f: - f.write(dhcpv6_text) + render(self.file['conf'], 'dhcp-client/ipv6.tmpl', self.options) # no longer accept router announcements on this interface - self._write_sysfs(self._dhcp[6]['accept_ra'], 0) + self._write_sysfs(self.file['accept_ra'], 0) # assemble command-line to start DHCPv6 client (dhclient) cmd = 'start-stop-daemon' @@ -224,15 +159,15 @@ class DHCP (Control): # now pass arguments to dhclient binary cmd += ' -6 -nw -cf {conf} -pf {pid} -lf {lease}' # add optional arguments - if dhcpv6['dhcpv6_prm_only']: + if self.options['dhcpv6_prm_only']: cmd += ' -S' - if dhcpv6['dhcpv6_temporary']: + if self.options['dhcpv6_temporary']: cmd += ' -T' cmd += ' {ifname}' - return self._cmd(cmd.format(**self._dhcp[6])) + return self._cmd(cmd.format(**self.file)) - def _del_dhcpv6(self): + def delete(self): """ De-configure interface as DHCPv6 clinet. All auto generated files like pid, config and lease will be removed. @@ -243,12 +178,12 @@ class DHCP (Control): >>> j = Interface('eth0') >>> j.del_dhcpv6() """ - if not os.path.isfile(self._dhcp[6]['pid']): + if not os.path.isfile(self.file['pid']): self._debug_msg('No DHCPv6 client PID found') return None - # with open(self._dhcp[6]['pid'], 'r') as f: - # pid = int(f.read()) + # with open(self.file['pid'], 'r') as f: + # pid = int(f.read()) # stop dhclient cmd = 'start-stop-daemon' @@ -256,13 +191,18 @@ class DHCP (Control): cmd += ' --oknodo' cmd += ' --quiet' cmd += ' --pidfile {pid}' - self._cmd(cmd.format(**self._dhcp[6])) + self._cmd(cmd.format(**self.file)) # accept router announcements on this interface - self._write_sysfs(self._dhcp[6]['accept_ra'], 1) + self._write_sysfs(self.options['accept_ra'], 1) # cleanup old config files for name in ('conf', 'pid', 'lease'): - if os.path.isfile(self._dhcp[6][name]): - os.remove(self._dhcp[6][name]) + if os.path.isfile(self.file[name]): + os.remove(self.file[name]) + +class DHCP (object): + def __init__(self, ifname): + self.v4 = _DHCPv4(ifname) + self.v6 = _DHCPv6(ifname) diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py index 291b326bf..542de4f59 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -18,8 +18,8 @@ import re from vyos.ifconfig.interface import Interface from vyos.ifconfig.vlan import VLAN +from vyos.validate import assert_list from vyos.util import run -from vyos.validate import * @Interface.register diff --git a/python/vyos/ifconfig/input.py b/python/vyos/ifconfig/input.py new file mode 100644 index 000000000..bfab36335 --- /dev/null +++ b/python/vyos/ifconfig/input.py @@ -0,0 +1,31 @@ +# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +from vyos.ifconfig.interface import Interface + + +@Interface.register +class InputIf(Interface): + default = { + 'type': '', + } + definition = { + **Interface.definition, + **{ + 'section': 'input', + 'prefixes': ['ifb', ], + }, + } diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 96057a943..43f823eca 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -18,23 +18,33 @@ import re import json import glob import time +from time import sleep +from os.path import isfile from copy import deepcopy +from datetime import timedelta -from vyos.validate import * # should not * include -from vyos.util import mac2eui64 -from vyos import ConfigError - +from hurry.filesize import size, alternative from ipaddress import IPv4Network, IPv6Address, IPv6Network from netifaces import ifaddresses, AF_INET, AF_INET6 -from time import sleep -from os.path import isfile from tabulate import tabulate -from hurry.filesize import size,alternative -from datetime import timedelta +from vyos.util import mac2eui64 +from vyos import ConfigError from vyos.ifconfig.dhcp import DHCP +from vyos.validate import is_ipv4 +from vyos.validate import is_ipv6 +from vyos.validate import is_intf_addr_assigned +from vyos.validate import assert_boolean +from vyos.validate import assert_list +from vyos.validate import assert_mac +from vyos.validate import assert_mtu +from vyos.validate import assert_positive +from vyos.validate import assert_range + +from vyos.ifconfig.control import Control + -class Interface(DHCP): +class Interface(Control): options = [] required = [] default = { @@ -173,7 +183,8 @@ class Interface(DHCP): self.config['ifname'] = ifname # we must have updated config before initialising the Interface - super().__init__(ifname, **kargs) + super().__init__(**kargs) + self.dhcp = DHCP(ifname) if not os.path.exists('/sys/class/net/{}'.format(self.config['ifname'])): # Any instance of Interface, such as Interface('eth0') @@ -216,8 +227,8 @@ class Interface(DHCP): >>> i.remove() """ # stop DHCP(v6) if running - self._del_dhcp() - self._del_dhcpv6() + self.dhcp.v4.delete() + self.dhcp.v6.delete() # remove all assigned IP addresses from interface - this is a bit redundant # as the kernel will remove all addresses on interface deletion, but we @@ -660,12 +671,13 @@ class Interface(DHCP): # do not change below 'if' ordering esle you will get an exception as: # ValueError: 'dhcp' does not appear to be an IPv4 or IPv6 address if addr != 'dhcp' and is_ipv4(addr): - raise ConfigError("Can't configure both static IPv4 and DHCP address on the same interface") + raise ConfigError( + "Can't configure both static IPv4 and DHCP address on the same interface") if addr == 'dhcp': - self._set_dhcp() + self.dhcp.v4.set() elif addr == 'dhcpv6': - self._set_dhcpv6() + self.dhcp.v6.set() else: if not is_intf_addr_assigned(self.config['ifname'], addr): cmd = 'ip addr add "{}" dev "{}"'.format(addr, self.config['ifname']) @@ -694,9 +706,9 @@ class Interface(DHCP): ['2001:db8::ffff/64'] """ if addr == 'dhcp': - self._del_dhcp() + self.dhcp.v4.delete() elif addr == 'dhcpv6': - self._del_dhcpv6() + self.dhcp.v6.delete() else: if is_intf_addr_assigned(self.config['ifname'], addr): cmd = 'ip addr del "{}" dev "{}"'.format(addr, self.config['ifname']) diff --git a/python/vyos/ifconfig/macvlan.py b/python/vyos/ifconfig/macvlan.py index 4e4b563a1..55b1a3e91 100644 --- a/python/vyos/ifconfig/macvlan.py +++ b/python/vyos/ifconfig/macvlan.py @@ -35,10 +35,10 @@ class MACVLANIf(Interface): 'prefixes': ['peth', ], }, } - options = Interface.options + ['link', 'mode'] + options = Interface.options + ['source_interface', 'mode'] def _create(self): - cmd = 'ip link add {ifname} link {link} type macvlan mode {mode}'.format( + cmd = 'ip link add {ifname} link {source_interface} type macvlan mode {mode}'.format( **self.config) self._cmd(cmd) @@ -54,7 +54,7 @@ class MACVLANIf(Interface): """ config = { 'address': '', - 'link': 0, + 'source_interface': '', 'mode': '' } return config @@ -62,7 +62,6 @@ class MACVLANIf(Interface): def set_mode(self, mode): """ """ - - cmd = 'ip link set dev {} type macvlan mode {}'.format( - self.config['ifname'], mode) + ifname = self.config['ifname'] + cmd = f'ip link set dev {ifname} type macvlan mode {mode}' return self._cmd(cmd) diff --git a/python/vyos/ifconfig/register.py b/python/vyos/ifconfig/section.py index c90782b70..ab340d247 100644 --- a/python/vyos/ifconfig/register.py +++ b/python/vyos/ifconfig/section.py @@ -16,9 +16,10 @@ import netifaces -class Register: +class Section: # the known interface prefixes _prefixes = {} + _classes = [] # class need to define: definition['prefixes'] # the interface prefixes declared by a class used to name interface with @@ -26,9 +27,16 @@ class Register: @classmethod def register(cls, klass): + """ + A function to use as decorator the interfaces classes + It register the prefix for the interface (eth, dum, vxlan, ...) + with the class which can handle it (EthernetIf, DummyIf,VXLANIf, ...) + """ if not klass.definition.get('prefixes',[]): raise RuntimeError(f'valid interface prefixes not defined for {klass.__name__}') + cls._classes.append(klass) + for ifprefix in klass.definition['prefixes']: if ifprefix in cls._prefixes: raise RuntimeError(f'only one class can be registered for prefix "{ifprefix}" type') @@ -38,7 +46,11 @@ class Register: @classmethod def _basename (cls, name, vlan): - # remove number from interface name + """ + remove the number at the end of interface name + name: name of the interface + vlan: if vlan is True, do not stop at the vlan number + """ name = name.rstrip('0123456789') name = name.rstrip('.') if vlan: @@ -47,15 +59,13 @@ class Register: @classmethod def section(cls, name, vlan=True): - # return the name of a section an interface should be under + """ + return the name of a section an interface should be under + name: name of the interface (eth0, dum1, ...) + vlan: should we try try to remove the VLAN from the number + """ name = cls._basename(name, vlan) - # XXX: To leave as long as vti and input are not moved to vyos - if name == 'vti': - return 'vti' - if name == 'ifb': - return 'input' - if name in cls._prefixes: return cls._prefixes[name].definition['section'] return '' @@ -68,15 +78,13 @@ class Register: raise ValueError(f'No type found for interface name: {name}') @classmethod - def _listing (cls,section=''): + def _intf_under_section (cls,section=''): + """ + return a generator with the name of the interface which are under a section + """ interfaces = netifaces.interfaces() for ifname in interfaces: - # XXX: Temporary hack as vti and input are not yet moved from vyatta to vyos - if ifname.startswith('vti') or ifname.startswith('input'): - yield ifname - continue - ifsection = cls.section(ifname) if not ifsection: continue @@ -87,9 +95,37 @@ class Register: yield ifname @classmethod - def listing(cls, section=''): - return list(cls._listing(section)) + def interfaces(cls, section=''): + """ + return a list of the name of the interface which are under a section + if no section is provided, then it returns all configured interfaces + """ + return list(cls._intf_under_section(section)) + @classmethod + def _intf_with_feature(cls, feature=''): + """ + return a generator with the name of the interface which have + a particular feature set in their definition such as: + bondable, broadcast, bridgeable, ... + """ + for klass in cls._classes: + if klass.definition[feature]: + yield klass.definition['section'] -# XXX: TODO - limit name for VRF interfaces + @classmethod + def feature(cls, feature=''): + """ + return list with the name of the interface which have + a particular feature set in their definition such as: + bondable, broadcast, bridgeable, ... + """ + return list(cls._intf_with_feature(feature)) + @classmethod + def reserved(cls): + """ + return list with the interface name prefixes + eth, lo, vxlan, dum, ... + """ + return list(cls._prefixes.keys()) diff --git a/python/vyos/ifconfig/stp.py b/python/vyos/ifconfig/stp.py index 97a3c1ff3..5e83206c2 100644 --- a/python/vyos/ifconfig/stp.py +++ b/python/vyos/ifconfig/stp.py @@ -16,7 +16,7 @@ from vyos.ifconfig.interface import Interface -from vyos.validate import * +from vyos.validate import assert_positive class STP: diff --git a/python/vyos/ifconfig/tunnel.py b/python/vyos/ifconfig/tunnel.py index 1bbb9eb6a..009a53a82 100644 --- a/python/vyos/ifconfig/tunnel.py +++ b/python/vyos/ifconfig/tunnel.py @@ -31,7 +31,7 @@ def enable_to_on(value): raise ValueError(f'expect enable or disable but got "{value}"') - +@Interface.register class _Tunnel(Interface): """ _Tunnel: private base class for tunnels @@ -143,7 +143,7 @@ class GREIf(_Tunnel): options = ['local', 'remote', 'ttl', 'tos', 'key'] updates = ['local', 'remote', 'ttl', 'tos', - 'multicast', 'allmulticast'] + 'mtu', 'multicast', 'allmulticast'] create = 'ip tunnel add {ifname} mode {type}' change = 'ip tunnel cha {ifname}' @@ -167,7 +167,7 @@ class GRETapIf(_Tunnel): required = ['local', ] options = ['local', 'remote', ] - updates = [] + updates = ['mtu', ] create = 'ip link add {ifname} type {type}' change = '' @@ -193,7 +193,7 @@ class IP6GREIf(_Tunnel): 'hoplimit', 'tclass', 'flowlabel'] updates = ['local', 'remote', 'encaplimit', 'hoplimit', 'tclass', 'flowlabel', - 'multicast', 'allmulticast'] + 'mtu', 'multicast', 'allmulticast'] create = 'ip tunnel add {ifname} mode {type}' change = 'ip tunnel cha {ifname} mode {type}' @@ -227,7 +227,7 @@ class IPIPIf(_Tunnel): options = ['local', 'remote', 'ttl', 'tos', 'key'] updates = ['local', 'remote', 'ttl', 'tos', - 'multicast', 'allmulticast'] + 'mtu', 'multicast', 'allmulticast'] create = 'ip tunnel add {ifname} mode {type}' change = 'ip tunnel cha {ifname}' @@ -252,7 +252,7 @@ class IPIP6If(_Tunnel): 'hoplimit', 'tclass', 'flowlabel'] updates = ['local', 'remote', 'encaplimit', 'hoplimit', 'tclass', 'flowlabel', - 'multicast', 'allmulticast'] + 'mtu', 'multicast', 'allmulticast'] create = 'ip -6 tunnel add {ifname} mode {type}' change = 'ip -6 tunnel cha {ifname}' @@ -288,7 +288,7 @@ class SitIf(_Tunnel): options = ['local', 'remote', 'ttl', 'tos', 'key'] updates = ['local', 'remote', 'ttl', 'tos', - 'multicast', 'allmulticast'] + 'mtu', 'multicast', 'allmulticast'] create = 'ip tunnel add {ifname} mode {type}' change = 'ip tunnel cha {ifname}' @@ -309,7 +309,7 @@ class Sit6RDIf(SitIf): # TODO: check if key can really be used with 6RD options = ['remote', 'ttl', 'tos', 'key', '6rd-prefix', '6rd-relay-prefix'] updates = ['remote', 'ttl', 'tos', - 'multicast', 'allmulticast'] + 'mtu', 'multicast', 'allmulticast'] def _create(self): # do not call _Tunnel.create, building fully here diff --git a/python/vyos/ifconfig/vti.py b/python/vyos/ifconfig/vti.py new file mode 100644 index 000000000..56ebe01d1 --- /dev/null +++ b/python/vyos/ifconfig/vti.py @@ -0,0 +1,31 @@ +# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +from vyos.ifconfig.interface import Interface + + +@Interface.register +class VTIIf(Interface): + default = { + 'type': 'vti', + } + definition = { + **Interface.definition, + **{ + 'section': 'vti', + 'prefixes': ['vti', ], + }, + } diff --git a/python/vyos/ifconfig/vxlan.py b/python/vyos/ifconfig/vxlan.py index 5678ad62e..f47ae17cc 100644 --- a/python/vyos/ifconfig/vxlan.py +++ b/python/vyos/ifconfig/vxlan.py @@ -43,12 +43,13 @@ class VXLANIf(Interface): default = { 'type': 'vxlan', - 'vni': 0, - 'dev': '', 'group': '', - 'remote': '', 'port': 8472, # The Linux implementation of VXLAN pre-dates # the IANA's selection of a standard destination port + 'remote': '', + 'src_address': '', + 'src_interface': '', + 'vni': 0 } definition = { **Interface.definition, @@ -58,24 +59,30 @@ class VXLANIf(Interface): 'bridgeable': True, } } - options = ['group', 'remote', 'dev', 'port', 'vni'] + options = ['group', 'remote', 'src_interface', 'port', 'vni', 'src_address'] mapping = { 'ifname': 'add', 'vni': 'id', 'port': 'dstport', + 'src_address': 'nolearning local', } def _create(self): cmdline = set() if self.config['remote']: - cmdline = ('ifname', 'type', 'remote', 'dev', 'vni', 'port') - elif self.config['group'] and self.config['dev']: - cmdline = ('ifname', 'type', 'group', 'dev', 'vni', 'port') + cmdline = ('ifname', 'type', 'remote', 'src_interface', 'vni', 'port') + + elif self.config['src_address']: + cmdline = ('ifname', 'type', 'src_address', 'vni', 'port') + + elif self.config['group'] and self.config['src_interface']: + cmdline = ('ifname', 'type', 'group', 'src_interface', 'vni', 'port') + else: - intf = self.config['intf'] + ifname = self.config['ifname'] raise ConfigError( - f'VXLAN "{intf}" is missing mandatory underlay interface for a multicast network.') + f'VXLAN "{ifname}" is missing mandatory underlay interface for a multicast network.') cmd = 'ip link' for key in cmdline: diff --git a/python/vyos/ifconfig_vlan.py b/python/vyos/ifconfig_vlan.py index ed22646c1..899fd17da 100644 --- a/python/vyos/ifconfig_vlan.py +++ b/python/vyos/ifconfig_vlan.py @@ -25,32 +25,20 @@ def apply_vlan_config(vlan, config): if not vlan.definition['vlan']: raise TypeError() - # get DHCP config dictionary and update values - opt = vlan.get_dhcp_options() - if config['dhcp_client_id']: - opt['client_id'] = config['dhcp_client_id'] + vlan.dhcp.v4.options['client_id'] = config['dhcp_client_id'] if config['dhcp_hostname']: - opt['hostname'] = config['dhcp_hostname'] + vlan.dhcp.v4.options['hostname'] = config['dhcp_hostname'] if config['dhcp_vendor_class_id']: - opt['vendor_class_id'] = config['dhcp_vendor_class_id'] - - # store DHCP config dictionary - used later on when addresses are aquired - vlan.set_dhcp_options(opt) - - # get DHCPv6 config dictionary and update values - opt = vlan.get_dhcpv6_options() + vlan.dhcp.v4.options['vendor_class_id'] = config['dhcp_vendor_class_id'] if config['dhcpv6_prm_only']: - opt['dhcpv6_prm_only'] = True + vlan.dhcp.v6.options['dhcpv6_prm_only'] = True if config['dhcpv6_temporary']: - opt['dhcpv6_temporary'] = True - - # store DHCPv6 config dictionary - used later on when addresses are aquired - vlan.set_dhcpv6_options(opt) + vlan.dhcp.v6.options['dhcpv6_temporary'] = True # update interface description used e.g. within SNMP vlan.set_alias(config['description']) diff --git a/python/vyos/ioctl.py b/python/vyos/ioctl.py index e57d261e4..cfa75aac6 100644 --- a/python/vyos/ioctl.py +++ b/python/vyos/ioctl.py @@ -13,9 +13,11 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see <http://www.gnu.org/licenses/>. +import sys import os -import fcntl, struct, sys -from socket import * +import socket +import fcntl +import struct SIOCGIFFLAGS = 0x8913 @@ -28,7 +30,7 @@ def get_terminal_size(): def get_interface_flags(intf): """ Pull the SIOCGIFFLAGS """ nullif = '\0'*256 - sock = socket(AF_INET, SOCK_DGRAM) + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) raw = fcntl.ioctl(sock.fileno(), SIOCGIFFLAGS, intf + nullif) flags, = struct.unpack('H', raw[16:18]) return flags diff --git a/python/vyos/remote.py b/python/vyos/remote.py index f0bf41cd4..1b4d3876e 100644 --- a/python/vyos/remote.py +++ b/python/vyos/remote.py @@ -18,7 +18,8 @@ import os import re import fileinput -from vyos.util import cmd, DEVNULL +from vyos.util import cmd +from vyos.util import DEVNULL def check_and_add_host_key(host_name): @@ -31,10 +32,10 @@ def check_and_add_host_key(host_name): mode = 0o600 os.mknod(known_hosts, 0o600) - keyscan_cmd = 'ssh-keyscan -t rsa {} 2>/dev/null'.format(host_name) + keyscan_cmd = 'ssh-keyscan -t rsa {}'.format(host_name) try: - host_key = cmd(keyscan_cmd, stderr=DEVNULL, universal_newlines=True) + host_key = cmd(keyscan_cmd, stderr=DEVNULL) except OSError: sys.exit("Can not get RSA host key") @@ -61,9 +62,9 @@ def check_and_add_host_key(host_name): print("Host key has changed!") print("If you trust the host key fingerprint below, continue.") - fingerprint_cmd = 'ssh-keygen -lf /dev/stdin <<< "{}"'.format(host_key) + fingerprint_cmd = 'ssh-keygen -lf /dev/stdin' try: - fingerprint = cmd(fingerprint_cmd, stderr=DEVNULL, universal_newlines=True) + fingerprint = cmd(fingerprint_cmd, stderr=DEVNULL, input=host_key) except OSError: sys.exit("Can not get RSA host key fingerprint.") @@ -125,7 +126,7 @@ def get_remote_config(remote_file): # Try header first, and look for 'OK' or 'Moved' codes: curl_cmd = 'curl {0} -q -I {1}'.format(redirect_opt, remote_file) try: - curl_output = cmd(curl_cmd, shell=True, universal_newlines=True) + curl_output = cmd(curl_cmd) except OSError: sys.exit(1) @@ -142,6 +143,6 @@ def get_remote_config(remote_file): curl_cmd = 'curl {0} -# {1}'.format(redirect_opt, remote_file) try: - return cmd(curl_cmd, universal_newlines=True) + return cmd(curl_cmd, stderr=None) except OSError: return None diff --git a/python/vyos/template.py b/python/vyos/template.py new file mode 100644 index 000000000..6c73ce753 --- /dev/null +++ b/python/vyos/template.py @@ -0,0 +1,65 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import os + +from jinja2 import Environment +from jinja2 import FileSystemLoader + +from vyos.defaults import directories + + +# reuse the same Environment to improve performance +_templates_env = { + False: Environment(loader=FileSystemLoader(directories['templates'])), + True: Environment(loader=FileSystemLoader(directories['templates']), trim_blocks=True), +} +_templates_mem = { + False: {}, + True: {}, +} + + +def render(destination, template, content, trim_blocks=False, formater=None): + """ + render a template from the template directory, it will raise on any errors + destination: the file where the rendered template must be saved + template: the path to the template relative to the template folder + content: the dictionary to use to render the template + + This classes cache the renderer, so rendering the same file multiple time + does not cause as too much overhead. If use everywhere, it could be changed + and load the template from python environement variables from an import + python module generated when the debian package is build + (recovering the load time and overhead caused by having the file out of the code) + """ + + # Setup a renderer for the given template + # This is cached and re-used for performance + if template not in _templates_mem[trim_blocks]: + _templates_mem[trim_blocks][template] = _templates_env[trim_blocks].get_template(template) + template = _templates_mem[trim_blocks][template] + + # As we are opening the file with 'w', we are performing the rendering + # before calling open() to not accidentally erase the file if the + # templating fails + content = template.render(content) + + if formater: + content = formater(content) + + # Write client config file + with open(destination, 'w') as f: + f.write(content) diff --git a/python/vyos/util.py b/python/vyos/util.py index 16cfae92d..49c47cd85 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -16,61 +16,121 @@ import os import re import sys -from subprocess import Popen, PIPE, STDOUT, DEVNULL +from subprocess import Popen +from subprocess import PIPE +from subprocess import STDOUT +from subprocess import DEVNULL -def debug(flag): - return flag if os.path.isfile(f'/tmp/vyos.{flag}.debug') else '' +from vyos import debug +# There is many (too many) ways to run command with python +# os.system, subprocess.Popen, subproces.{run,call,check_output} +# which all have slighty different behaviour -def debug_msg(message, section=''): - if section: - print(f'DEBUG/{section:<6} {message}') +def popen(command, flag='', shell=None, input=None, timeout=None, env=None, + stdout=PIPE, stderr=None, decode=None): + """ + popen is a wrapper helper aound subprocess.Popen + with it default setting it will return a tuple (out, err) + out: the output of the program run + err: the error code returned by the program + + it can be affected by the following flags: + shell: do not try to auto-detect if a shell is required + for example if a pipe (|) or redirection (>, >>) is used + input: data to sent to the child process via STDIN + the data should be bytes but string will be converted + timeout: time after which the command will be considered to have failed + env: mapping that defines the environment variables for the new process + stdout: define how the output of the program should be handled + - PIPE (default), sends stdout to the output + - DEVNULL, discard the output + stderr: define how the output of the program should be handled + - None (default), send/merge the data to/with stderr + - PIPE, popen will append it to output + - STDOUT, send the data to be merged with stdout + - DEVNULL, discard the output + decode: specify the expected text encoding (utf-8, ascii, ...) + + usage: + to get both stdout, and stderr: popen('command', stdout=PIPE, stderr=STDOUT) + to discard stdout and get stderr: popen('command', stdout=DEVNUL, stderr=PIPE) + """ + + # log if the flag is set, otherwise log if command is set + if not debug.enabled(flag): + flag = 'command' + + cmd_msg = f"cmd '{command}'" + debug.message(cmd_msg, flag) -def popen(command, section='', shell=None, input=None, timeout=None, env=None, - universal_newlines=None, stdout=PIPE, stderr=STDOUT, decode=None): - """ popen does not raise, returns the output and error code of command """ use_shell = shell + stdin = None if shell is None: - use_shell = True if ' ' in command else False + use_shell = False + if ' ' in command: + use_shell = True + if env: + use_shell = True + if input: + stdin = PIPE + input = input.encode() if type(input) is str else input p = Popen( command, - stdout=stdout, stderr=stderr, + stdin=stdin, stdout=stdout, stderr=stderr, env=env, shell=use_shell, - universal_newlines=universal_newlines, ) - tmp = p.communicate(input, timeout)[0].strip() - debug_msg(f"cmd '{command}'", section) - decoded = tmp.decode(decode) if decode else tmp.decode() + tmp = p.communicate(input, timeout) + out1 = b'' + out2 = b'' + if stdout == PIPE: + out1 = tmp[0] + if stderr == PIPE: + out2 += tmp[1] + decoded1 = out1.decode(decode) if decode else out1.decode() + decoded2 = out2.decode(decode) if decode else out2.decode() + decoded1 = decoded1.replace('\r\n', '\n').strip() + decoded2 = decoded2.replace('\r\n', '\n').strip() + nl = '\n' if decoded1 and decoded2 else '' + decoded = decoded1 + nl + decoded2 if decoded: - debug_msg(f"returned:\n{decoded}", section) + ret_msg = f"returned:\n{decoded}" + debug.message(ret_msg, flag) return decoded, p.returncode -def run(command, section='', shell=None, input=None, timeout=None, env=None, - universal_newlines=None, stdout=PIPE, stderr=STDOUT, decode=None): - """ does not raise exception on error, returns error code """ +def run(command, flag='', shell=None, input=None, timeout=None, env=None, + stdout=DEVNULL, stderr=None, decode=None): + """ + A wrapper around vyos.util.popen, which discard the stdout and + will return the error code of a command + """ _, code = popen( - command, section, + command, flag, stdout=stdout, stderr=stderr, input=input, timeout=timeout, env=env, shell=shell, - universal_newlines=universal_newlines, decode=decode, ) return code -def cmd(command, section='', shell=None, input=None, timeout=None, env=None, - universal_newlines=None, stdout=PIPE, stderr=STDOUT, decode=None, +def cmd(command, flag='', shell=None, input=None, timeout=None, env=None, + stdout=PIPE, stderr=None, decode=None, raising=None, message=''): - """ does raise exception, returns output of command """ + """ + A wrapper around vyos.util.popen, which returns the stdout and + will raise the error code of a command + + raising: specify which call should be used when raising (default is OSError) + the class should only require a string as parameter + """ decoded, code = popen( - command, section, + command, flag, stdout=stdout, stderr=stderr, input=input, timeout=timeout, env=env, shell=shell, - universal_newlines=universal_newlines, decode=decode, ) if code != 0: @@ -86,6 +146,23 @@ def cmd(command, section='', shell=None, input=None, timeout=None, env=None, return decoded +def call(command, flag='', shell=None, input=None, timeout=None, env=None, + stdout=PIPE, stderr=None, decode=None): + """ + A wrapper around vyos.util.popen, which print the stdout and + will return the error code of a command + """ + out, code = popen( + command, flag, + stdout=stdout, stderr=stderr, + input=input, timeout=timeout, + env=env, shell=shell, + decode=decode, + ) + print(out) + return code + + def read_file(path): """ Read a file to string """ with open(path, 'r') as f: @@ -93,18 +170,37 @@ def read_file(path): return data -def chown_file(path, user, group): - """ change file owner """ +def chown(path, user, group): + """ change file/directory owner """ from pwd import getpwnam from grp import getgrnam - if os.path.isfile(path): + if os.path.exists(path): uid = getpwnam(user).pw_uid gid = getgrnam(group).gr_gid os.chown(path, uid, gid) -def chmod_x(path): - """ make file executable """ + +def chmod_600(path): + """ make file only read/writable by owner """ + from stat import S_IRUSR, S_IWUSR + + if os.path.exists(path): + bitmask = S_IRUSR | S_IWUSR + os.chmod(path, bitmask) + + +def chmod_750(path): + """ make file/directory only executable to user and group """ + from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP + + if os.path.exists(path): + bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP + os.chmod(path, bitmask) + + +def chmod_755(path): + """ make file executable by all """ from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP, S_IROTH, S_IXOTH if os.path.exists(path): diff --git a/python/vyos/version.py b/python/vyos/version.py index 383efbc1e..d51a940d6 100644 --- a/python/vyos/version.py +++ b/python/vyos/version.py @@ -44,7 +44,7 @@ def get_version_data(file=version_file): file (str): path to the version file Returns: - dict: version data + dict: version data, if it can not be found and empty dict The optional ``file`` argument comes in handy in upgrade scripts that need to retrieve information from images other than the running image. @@ -52,17 +52,20 @@ def get_version_data(file=version_file): is an implementation detail and may change in the future, while the interface of this module will stay the same. """ - with open(file, 'r') as f: - version_data = json.load(f) - return version_data + try: + with open(file, 'r') as f: + version_data = json.load(f) + return version_data + except FileNotFoundError: + return {} def get_version(file=None): """ - Get the version number + Get the version number, or an empty string if it could not be determined """ version_data = None if file: version_data = get_version_data(file=file) else: version_data = get_version_data() - return version_data["version"] + return version_data.get('version','') diff --git a/src/completion/list_interfaces.py b/src/completion/list_interfaces.py index 98b32797a..e27281433 100755 --- a/src/completion/list_interfaces.py +++ b/src/completion/list_interfaces.py @@ -2,7 +2,14 @@ import sys import argparse -from vyos.ifconfig import Interface +from vyos.ifconfig import Section + + +def matching(feature): + for section in Section.feature(feature): + for intf in Section.interfaces(section): + yield intf + parser = argparse.ArgumentParser() group = parser.add_mutually_exclusive_group() @@ -13,46 +20,23 @@ group.add_argument("-bo", "--bondable", action="store_true", help="List all bond args = parser.parse_args() -# XXX: Need to be rewritten using the data in the class definition -# XXX: It can be done once vti and input are moved into vyos -# XXX: We store for each class what type they are (broadcast, bridgeabe, ...) - if args.type: try: - interfaces = Interface.listing(args.type) - + interfaces = Section.interfaces(args.type) + print(" ".join(interfaces)) except ValueError as e: print(e, file=sys.stderr) print("") elif args.broadcast: - eth = Interface.listing("ethernet") - bridge = Interface.listing("bridge") - bond = Interface.listing("bonding") - interfaces = eth + bridge + bond + print(" ".join(matching("broadcast"))) elif args.bridgeable: - eth = Interface.listing("ethernet") - bond = Interface.listing("bonding") - l2tpv3 = Interface.listing("l2tpv3") - openvpn = Interface.listing("openvpn") - wireless = Interface.listing("wireless") - tunnel = Interface.listing("tunnel") - vxlan = Interface.listing("vxlan") - geneve = Interface.listing("geneve") - - interfaces = eth + bond + l2tpv3 + openvpn + vxlan + tunnel + wireless + geneve + print(" ".join(matching("bridgeable"))) elif args.bondable: - interfaces = [] - eth = Interface.listing("ethernet") - # we need to filter out VLAN interfaces identified by a dot (.) in their name - for intf in eth: - if not '.' in intf: - interfaces.append(intf) + print(" ".join([intf for intf in matching("bondable") if '.' not in intf])) else: - interfaces = Interface.listing() - -print(" ".join(interfaces)) + print(" ".join(Section.interfaces())) diff --git a/src/completion/list_openvpn_clients.py b/src/completion/list_openvpn_clients.py index 17b0c7008..177ac90c9 100755 --- a/src/completion/list_openvpn_clients.py +++ b/src/completion/list_openvpn_clients.py @@ -18,7 +18,7 @@ import os import sys import argparse -from vyos.ifconfig import Interface +from vyos.ifconfig import Section def get_client_from_interface(interface): clients = [] @@ -50,7 +50,7 @@ if __name__ == "__main__": if args.interface: clients = get_client_from_interface(args.interface) elif args.all: - for interface in Interface.listing("openvpn"): + for interface in Section.interfaces("openvpn"): clients += get_client_from_interface(interface) print(" ".join(clients)) diff --git a/src/conf_mode/accel_l2tp.py b/src/conf_mode/accel_l2tp.py deleted file mode 100755 index 4ca5a858a..000000000 --- a/src/conf_mode/accel_l2tp.py +++ /dev/null @@ -1,397 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2019-2020 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import sys -import os -import re -import jinja2 -import socket -import time - -from jinja2 import FileSystemLoader, Environment - -from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir -from vyos import ConfigError -from vyos.util import run - - -pidfile = r'/var/run/accel_l2tp.pid' -l2tp_cnf_dir = r'/etc/accel-ppp/l2tp' -chap_secrets = l2tp_cnf_dir + '/chap-secrets' -l2tp_conf = l2tp_cnf_dir + '/l2tp.config' -# accel-pppd -d -c /etc/accel-ppp/l2tp/l2tp.config -p /var/run/accel_l2tp.pid - -# config path creation -if not os.path.exists(l2tp_cnf_dir): - os.makedirs(l2tp_cnf_dir) - -### -# inline helper functions -### -# depending on hw and threads, daemon needs a little to start -# if it takes longer than 100 * 0.5 secs, exception is being raised -# not sure if that's the best way to check it, but it worked so far quite well -### - - -def chk_con(): - cnt = 0 - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - while True: - try: - s.connect(("127.0.0.1", 2004)) - break - except ConnectionRefusedError: - time.sleep(0.5) - cnt += 1 - if cnt == 100: - raise("failed to start l2tp server") - break - - -def _accel_cmd(command): - return run(f'/usr/bin/accel-cmd -p 2004 {command}') - -### -# inline helper functions end -### - - -def get_config(): - c = Config() - if not c.exists('vpn l2tp remote-access '): - return None - - c.set_level('vpn l2tp remote-access') - config_data = { - 'authentication': { - 'mode': 'local', - 'local-users': { - }, - 'radiussrv': {}, - 'radiusopt': {}, - 'auth_proto': [], - 'mppe': 'prefer' - }, - 'outside_addr': '', - 'gateway_address': '10.255.255.0', - 'dns': [], - 'dnsv6': [], - 'wins': [], - 'client_ip_pool': None, - 'client_ip_subnets': [], - 'client_ipv6_pool': {}, - 'mtu': '1436', - 'ip6_column': '', - 'ip6_dp_column': '', - 'ppp_options': {}, - } - - ### general options ### - - if c.exists('dns-servers server-1'): - config_data['dns'].append(c.return_value('dns-servers server-1')) - if c.exists('dns-servers server-2'): - config_data['dns'].append(c.return_value('dns-servers server-2')) - if c.exists('dnsv6-servers'): - for dns6_server in c.return_values('dnsv6-servers'): - config_data['dnsv6'].append(dns6_server) - if c.exists('wins-servers server-1'): - config_data['wins'].append(c.return_value('wins-servers server-1')) - if c.exists('wins-servers server-2'): - config_data['wins'].append(c.return_value('wins-servers server-2')) - if c.exists('outside-address'): - config_data['outside_addr'] = c.return_value('outside-address') - - # auth local - if c.exists('authentication mode local'): - if c.exists('authentication local-users username'): - for usr in c.list_nodes('authentication local-users username'): - config_data['authentication']['local-users'].update( - { - usr: { - 'passwd': '', - 'state': 'enabled', - 'ip': '*', - 'upload': None, - 'download': None - } - } - ) - - if c.exists('authentication local-users username ' + usr + ' password'): - config_data['authentication']['local-users'][usr]['passwd'] = c.return_value( - 'authentication local-users username ' + usr + ' password') - if c.exists('authentication local-users username ' + usr + ' disable'): - config_data['authentication']['local-users'][usr]['state'] = 'disable' - if c.exists('authentication local-users username ' + usr + ' static-ip'): - config_data['authentication']['local-users'][usr]['ip'] = c.return_value( - 'authentication local-users username ' + usr + ' static-ip') - if c.exists('authentication local-users username ' + usr + ' rate-limit download'): - config_data['authentication']['local-users'][usr]['download'] = c.return_value( - 'authentication local-users username ' + usr + ' rate-limit download') - if c.exists('authentication local-users username ' + usr + ' rate-limit upload'): - config_data['authentication']['local-users'][usr]['upload'] = c.return_value( - 'authentication local-users username ' + usr + ' rate-limit upload') - - # authentication mode radius servers and settings - - if c.exists('authentication mode radius'): - config_data['authentication']['mode'] = 'radius' - rsrvs = c.list_nodes('authentication radius server') - for rsrv in rsrvs: - if c.return_value('authentication radius server ' + rsrv + ' fail-time') == None: - ftime = '0' - else: - ftime = str(c.return_value( - 'authentication radius server ' + rsrv + ' fail-time')) - if c.return_value('authentication radius-server ' + rsrv + ' req-limit') == None: - reql = '0' - else: - reql = str(c.return_value( - 'authentication radius server ' + rsrv + ' req-limit')) - - config_data['authentication']['radiussrv'].update( - { - rsrv: { - 'secret': c.return_value('authentication radius server ' + rsrv + ' key'), - 'fail-time': ftime, - 'req-limit': reql - } - } - ) - # Source ip address feature - if c.exists('authentication radius source-address'): - config_data['authentication']['radius_source_address'] = c.return_value( - 'authentication radius source-address') - - # advanced radius-setting - if c.exists('authentication radius acct-timeout'): - config_data['authentication']['radiusopt']['acct-timeout'] = c.return_value( - 'authentication radius acct-timeout') - if c.exists('authentication radius max-try'): - config_data['authentication']['radiusopt']['max-try'] = c.return_value( - 'authentication radius max-try') - if c.exists('authentication radius timeout'): - config_data['authentication']['radiusopt']['timeout'] = c.return_value( - 'authentication radius timeout') - if c.exists('authentication radius nas-identifier'): - config_data['authentication']['radiusopt']['nas-id'] = c.return_value( - 'authentication radius nas-identifier') - if c.exists('authentication radius dae-server'): - # Set default dae-server port if not defined - if c.exists('authentication radius dae-server port'): - dae_server_port = c.return_value( - 'authentication radius dae-server port') - else: - dae_server_port = "3799" - config_data['authentication']['radiusopt'].update( - { - 'dae-srv': { - 'ip-addr': c.return_value('authentication radius dae-server ip-address'), - 'port': dae_server_port, - 'secret': str(c.return_value('authentication radius dae-server secret')) - } - } - ) - # filter-id is the internal accel default if attribute is empty - # set here as default for visibility which may change in the future - if c.exists('authentication radius rate-limit enable'): - if not c.exists('authentication radius rate-limit attribute'): - config_data['authentication']['radiusopt']['shaper'] = { - 'attr': 'Filter-Id' - } - else: - config_data['authentication']['radiusopt']['shaper'] = { - 'attr': c.return_value('authentication radius rate-limit attribute') - } - if c.exists('authentication radius rate-limit vendor'): - config_data['authentication']['radiusopt']['shaper']['vendor'] = c.return_value( - 'authentication radius rate-limit vendor') - - if c.exists('client-ip-pool'): - if c.exists('client-ip-pool start') and c.exists('client-ip-pool stop'): - config_data['client_ip_pool'] = c.return_value( - 'client-ip-pool start') + '-' + re.search('[0-9]+$', c.return_value('client-ip-pool stop')).group(0) - - if c.exists('client-ip-pool subnet'): - config_data['client_ip_subnets'] = c.return_values( - 'client-ip-pool subnet') - - if c.exists('client-ipv6-pool prefix'): - config_data['client_ipv6_pool']['prefix'] = c.return_values( - 'client-ipv6-pool prefix') - config_data['ip6_column'] = 'ip6,' - if c.exists('client-ipv6-pool delegate-prefix'): - config_data['client_ipv6_pool']['delegate_prefix'] = c.return_values( - 'client-ipv6-pool delegate-prefix') - config_data['ip6_dp_column'] = 'ip6-dp,' - - if c.exists('mtu'): - config_data['mtu'] = c.return_value('mtu') - - # gateway address - if c.exists('gateway-address'): - config_data['gateway_address'] = c.return_value('gateway-address') - else: - # calculate gw-ip-address - if c.exists('client-ip-pool start'): - # use start ip as gw-ip-address - config_data['gateway_address'] = c.return_value( - 'client-ip-pool start') - elif c.exists('client-ip-pool subnet'): - # use first ip address from first defined pool - lst_ip = re.findall("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}", c.return_values( - 'client-ip-pool subnet')[0]) - config_data['gateway_address'] = lst_ip[0] - - if c.exists('authentication require'): - auth_mods = {'pap': 'pap', 'chap': 'auth_chap_md5', - 'mschap': 'auth_mschap_v1', 'mschap-v2': 'auth_mschap_v2'} - for proto in c.return_values('authentication require'): - config_data['authentication']['auth_proto'].append( - auth_mods[proto]) - else: - config_data['authentication']['auth_proto'] = ['auth_mschap_v2'] - - if c.exists('authentication mppe'): - config_data['authentication']['mppe'] = c.return_value( - 'authentication mppe') - - if c.exists('idle'): - config_data['idle_timeout'] = c.return_value('idle') - - # LNS secret - if c.exists('lns shared-secret'): - config_data['lns_shared_secret'] = c.return_value('lns shared-secret') - - if c.exists('ccp-disable'): - config_data['ccp_disable'] = True - - # ppp_options - ppp_options = {} - if c.exists('ppp-options'): - if c.exists('ppp-options lcp-echo-failure'): - ppp_options['lcp-echo-failure'] = c.return_value( - 'ppp-options lcp-echo-failure') - if c.exists('ppp-options lcp-echo-interval'): - ppp_options['lcp-echo-interval'] = c.return_value( - 'ppp-options lcp-echo-interval') - - if len(ppp_options) != 0: - config_data['ppp_options'] = ppp_options - - return config_data - - -def verify(c): - if c == None: - return None - - if c['authentication']['mode'] == 'local': - if not c['authentication']['local-users']: - raise ConfigError( - 'l2tp-server authentication local-users required') - for usr in c['authentication']['local-users']: - if not c['authentication']['local-users'][usr]['passwd']: - raise ConfigError('user ' + usr + ' requires a password') - - if c['authentication']['mode'] == 'radius': - if len(c['authentication']['radiussrv']) == 0: - raise ConfigError('radius server required') - for rsrv in c['authentication']['radiussrv']: - if c['authentication']['radiussrv'][rsrv]['secret'] == None: - raise ConfigError('radius server ' + rsrv + - ' needs a secret configured') - - # check for the existence of a client ip pool - if not c['client_ip_pool'] and not c['client_ip_subnets']: - raise ConfigError( - "set vpn l2tp remote-access client-ip-pool requires subnet or start/stop IP pool") - - # check ipv6 - if 'delegate_prefix' in c['client_ipv6_pool'] and not 'prefix' in c['client_ipv6_pool']: - raise ConfigError( - "\"set vpn l2tp remote-access client-ipv6-pool prefix\" required for delegate-prefix ") - - if len(c['dnsv6']) > 3: - raise ConfigError("Maximum allowed dnsv6-servers addresses is 3") - - -def generate(c): - if c == None: - return None - - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'l2tp') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader, trim_blocks=True) - - # accel-cmd reload doesn't work so any change results in a restart of the daemon - try: - if os.cpu_count() == 1: - c['thread_cnt'] = 1 - else: - c['thread_cnt'] = int(os.cpu_count()/2) - except KeyError: - if os.cpu_count() == 1: - c['thread_cnt'] = 1 - else: - c['thread_cnt'] = int(os.cpu_count()/2) - - tmpl = env.get_template('l2tp.config.tmpl') - config_text = tmpl.render(c) - open(l2tp_conf, 'w').write(config_text) - - if c['authentication']['local-users']: - tmpl = env.get_template('chap-secrets.tmpl') - chap_secrets_txt = tmpl.render(c) - old_umask = os.umask(0o077) - open(chap_secrets, 'w').write(chap_secrets_txt) - os.umask(old_umask) - - return c - - -def apply(c): - if c == None: - if os.path.exists(pidfile): - _accel_cmd('shutdown hard') - if os.path.exists(pidfile): - os.remove(pidfile) - return None - - if not os.path.exists(pidfile): - ret = run(f'/usr/sbin/accel-pppd -c {l2tp_conf} -p {pidfile} -d') - chk_con() - if ret != 0 and os.path.exists(pidfile): - os.remove(pidfile) - raise ConfigError('accel-pppd failed to start') - else: - # if gw ip changes, only restart doesn't work - _accel_cmd('restart') - - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - sys.exit(1) diff --git a/src/conf_mode/arp.py b/src/conf_mode/arp.py index 3daa892d7..fde7dc521 100755 --- a/src/conf_mode/arp.py +++ b/src/conf_mode/arp.py @@ -22,7 +22,7 @@ import re import syslog as sl from vyos.config import Config -from vyos.util import run +from vyos.util import call from vyos import ConfigError arp_cmd = '/usr/sbin/arp' @@ -82,12 +82,12 @@ def generate(c): def apply(c): for ip_addr in c['remove']: sl.syslog(sl.LOG_NOTICE, "arp -d " + ip_addr) - run(f'{arp_cmd} -d {ip_addr} >/dev/null 2>&1') + call(f'{arp_cmd} -d {ip_addr} >/dev/null 2>&1') for ip_addr in c['update']: sl.syslog(sl.LOG_NOTICE, "arp -s " + ip_addr + " " + c['update'][ip_addr]) updated = c['update'][ip_addr] - run(f'{arp_cmd} -s {ip_addr} {updated}') + call(f'{arp_cmd} -s {ip_addr} {updated}') if __name__ == '__main__': diff --git a/src/conf_mode/bcast_relay.py b/src/conf_mode/bcast_relay.py index f6d90776c..a3bc76ef8 100755 --- a/src/conf_mode/bcast_relay.py +++ b/src/conf_mode/bcast_relay.py @@ -19,12 +19,11 @@ import fnmatch from sys import exit from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError -from vyos.util import run +from vyos.util import call +from vyos.template import render config_file = r'/etc/default/udp-broadcast-relay' @@ -112,11 +111,6 @@ def generate(relay): if relay is None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'bcast-relay') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - config_dir = os.path.dirname(config_file) config_filename = os.path.basename(config_file) active_configs = [] @@ -146,16 +140,13 @@ def generate(relay): # configuration filename contains instance id file = config_file + str(r['id']) - tmpl = env.get_template('udp-broadcast-relay.tmpl') - config_text = tmpl.render(r) - with open(file, 'w') as f: - f.write(config_text) + render(file, 'bcast-relay/udp-broadcast-relay.tmpl', r) return None def apply(relay): # first stop all running services - run('sudo systemctl stop udp-broadcast-relay@{1..99}') + call('systemctl stop udp-broadcast-relay@{1..99}.service') if (relay is None) or relay['disabled']: return None @@ -165,7 +156,7 @@ def apply(relay): # Don't start individual instance when it's disabled if r['disabled']: continue - run('sudo systemctl start udp-broadcast-relay@{0}'.format(r['id'])) + call('systemctl start udp-broadcast-relay@{0}.service'.format(r['id'])) return None diff --git a/src/conf_mode/dhcp_relay.py b/src/conf_mode/dhcp_relay.py index 1d6d4c6e3..ce0e01308 100755 --- a/src/conf_mode/dhcp_relay.py +++ b/src/conf_mode/dhcp_relay.py @@ -16,15 +16,14 @@ import os -from jinja2 import FileSystemLoader, Environment from sys import exit from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir +from vyos.template import render +from vyos.util import call from vyos import ConfigError -from vyos.util import run -config_file = r'/etc/default/isc-dhcp-relay' +config_file = r'/run/dhcp-relay/dhcp.conf' default_config_data = { 'interface': [], @@ -96,28 +95,25 @@ def verify(relay): def generate(relay): # bail out early - looks like removal from running config - if relay is None: + if not relay: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'dhcp-relay') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - tmpl = env.get_template('config.tmpl') - config_text = tmpl.render(relay) - with open(config_file, 'w') as f: - f.write(config_text) + # Create configuration directory on demand + dirname = os.path.dirname(config_file) + if not os.path.isdir(dirname): + os.mkdir(dirname) + render(config_file, 'dhcp-relay/config.tmpl', relay) return None def apply(relay): - if relay is not None: - run('sudo systemctl restart isc-dhcp-relay.service') + if relay: + call('systemctl restart isc-dhcp-relay.service') else: # DHCP relay support is removed in the commit - run('sudo systemctl stop isc-dhcp-relay.service') - os.unlink(config_file) + call('systemctl stop isc-dhcp-relay.service') + if os.path.exists(config_file): + os.unlink(config_file) return None diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index 69aebe2f4..da01f16eb 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -17,25 +17,19 @@ import os from ipaddress import ip_address, ip_network -from jinja2 import FileSystemLoader, Environment from socket import inet_ntoa from struct import pack from sys import exit from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos.validate import is_subnet_connected from vyos import ConfigError -from vyos.util import run +from vyos.template import render +from vyos.util import call, chown - -config_file = r'/etc/dhcp/dhcpd.conf' -lease_file = r'/config/dhcpd.leases' -pid_file = r'/var/run/dhcpd.pid' -daemon_config_file = r'/etc/default/isc-dhcpv4-server' +config_file = r'/run/dhcp-server/dhcpd.conf' default_config_data = { - 'lease_file': lease_file, 'disabled': False, 'ddns_enable': False, 'global_parameters': [], @@ -451,7 +445,7 @@ def get_config(): return dhcp def verify(dhcp): - if (dhcp is None) or (dhcp['disabled'] is True): + if not dhcp or dhcp['disabled']: return None # If DHCP is enabled we need one share-network @@ -597,49 +591,29 @@ def verify(dhcp): return None def generate(dhcp): - if dhcp is None: - return None - - if dhcp['disabled'] is True: - print('Warning: DHCP server will be deactivated because it is disabled') + if not dhcp or dhcp['disabled']: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'dhcp-server') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) + # Create configuration directory on demand + dirname = os.path.dirname(config_file) + if not os.path.isdir(dirname): + os.mkdir(dirname) - tmpl = env.get_template('dhcpd.conf.tmpl') - config_text = tmpl.render(dhcp) # Please see: https://phabricator.vyos.net/T1129 for quoting of the raw parameters # we can pass to ISC DHCPd - config_text = config_text.replace(""",'"') - - with open(config_file, 'w') as f: - f.write(config_text) - - tmpl = env.get_template('daemon.tmpl') - config_text = tmpl.render(dhcp) - with open(daemon_config_file, 'w') as f: - f.write(config_text) - + render(config_file, 'dhcp-server/dhcpd.conf.tmpl', dhcp, + formater=lambda _: _.replace(""", '"')) return None def apply(dhcp): - if (dhcp is None) or dhcp['disabled']: + if not dhcp or dhcp['disabled']: # DHCP server is removed in the commit - run('sudo systemctl stop isc-dhcpv4-server.service') + call('systemctl stop isc-dhcp-server.service') if os.path.exists(config_file): os.unlink(config_file) - if os.path.exists(daemon_config_file): - os.unlink(daemon_config_file) - else: - # If our file holding DHCP leases does yet not exist - create it - if not os.path.exists(lease_file): - os.mknod(lease_file) - - run('sudo systemctl restart isc-dhcpv4-server.service') + return None + call('systemctl restart isc-dhcp-server.service') return None if __name__ == '__main__': diff --git a/src/conf_mode/dhcpv6_relay.py b/src/conf_mode/dhcpv6_relay.py index a67deb6c7..cb5a4bbfb 100755 --- a/src/conf_mode/dhcpv6_relay.py +++ b/src/conf_mode/dhcpv6_relay.py @@ -18,15 +18,13 @@ import os from sys import exit from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError -from vyos.util import run +from vyos.util import call +from vyos.template import render - -config_file = r'/etc/default/isc-dhcpv6-relay' +config_file = r'/run/dhcp-relay/dhcpv6.conf' default_config_data = { 'listen_addr': [], @@ -86,25 +84,22 @@ def generate(relay): if relay is None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'dhcpv6-relay') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - tmpl = env.get_template('config.tmpl') - config_text = tmpl.render(relay) - with open(config_file, 'w') as f: - f.write(config_text) + # Create configuration directory on demand + dirname = os.path.dirname(config_file) + if not os.path.isdir(dirname): + os.mkdir(dirname) + render(config_file, 'dhcpv6-relay/config.tmpl', relay) return None def apply(relay): if relay is not None: - run('sudo systemctl restart isc-dhcpv6-relay.service') + call('systemctl restart isc-dhcp-relay6.service') else: # DHCPv6 relay support is removed in the commit - run('sudo systemctl stop isc-dhcpv6-relay.service') - os.unlink(config_file) + call('systemctl stop isc-dhcp-relay6.service') + if os.path.exists(config_file): + os.unlink(config_file) return None diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py index 003e80915..94a307826 100755 --- a/src/conf_mode/dhcpv6_server.py +++ b/src/conf_mode/dhcpv6_server.py @@ -19,22 +19,16 @@ import ipaddress from sys import exit from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir +from vyos.template import render +from vyos.util import call from vyos.validate import is_subnet_connected from vyos import ConfigError -from vyos.util import run - -config_file = r'/etc/dhcp/dhcpdv6.conf' -lease_file = r'/config/dhcpdv6.leases' -pid_file = r'/var/run/dhcpdv6.pid' -daemon_config_file = r'/etc/default/isc-dhcpv6-server' +config_file = r'/run/dhcp-server/dhcpdv6.conf' default_config_data = { - 'lease_file': lease_file, 'preference': '', 'disabled': False, 'shared_network': [] @@ -222,10 +216,7 @@ def get_config(): return dhcpv6 def verify(dhcpv6): - if dhcpv6 is None: - return None - - if dhcpv6['disabled']: + if not dhcpv6 or dhcpv6['disabled']: return None # If DHCP is enabled we need one share-network @@ -337,44 +328,25 @@ def verify(dhcpv6): return None def generate(dhcpv6): - if dhcpv6 is None: - return None - - if dhcpv6['disabled']: - print('Warning: DHCPv6 server will be deactivated because it is disabled') + if not dhcpv6 or dhcpv6['disabled']: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'dhcpv6-server') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - tmpl = env.get_template('dhcpdv6.conf.tmpl') - config_text = tmpl.render(dhcpv6) - with open(config_file, 'w') as f: - f.write(config_text) - - tmpl = env.get_template('daemon.tmpl') - config_text = tmpl.render(dhcpv6) - with open(daemon_config_file, 'w') as f: - f.write(config_text) + # Create configuration directory on demand + dirname = os.path.dirname(config_file) + if not os.path.isdir(dirname): + os.mkdir(dirname) + render(config_file, 'dhcpv6-server/dhcpdv6.conf.tmpl', dhcpv6) return None def apply(dhcpv6): - if (dhcpv6 is None) or dhcpv6['disabled']: + if not dhcpv6 or dhcpv6['disabled']: # DHCP server is removed in the commit - run('sudo systemctl stop isc-dhcpv6-server.service') + call('systemctl stop isc-dhcp-server6.service') if os.path.exists(config_file): os.unlink(config_file) - if os.path.exists(daemon_config_file): - os.unlink(daemon_config_file) - else: - # If our file holding DHCPv6 leases does yet not exist - create it - if not os.path.exists(lease_file): - os.mknod(lease_file) - run('sudo systemctl restart isc-dhcpv6-server.service') + call('systemctl restart isc-dhcp-server6.service') return None diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index 5dc599425..567dfa4b3 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -19,20 +19,19 @@ import argparse from sys import exit from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos.hostsd_client import Client as hostsd_client from vyos.util import wait_for_commit_lock from vyos import ConfigError -from vyos.util import run +from vyos.util import call +from vyos.template import render parser = argparse.ArgumentParser() parser.add_argument("--dhclient", action="store_true", help="Started from dhclient-script") -config_file = r'/etc/powerdns/recursor.conf' +config_file = r'/run/powerdns/recursor.conf' default_config_data = { 'allow_from': [], @@ -153,25 +152,21 @@ def generate(dns): if dns is None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'dns-forwarding') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader, trim_blocks=True) + dirname = os.path.dirname(config_file) + if not os.path.exists(dirname): + os.mkdir(dirname) - tmpl = env.get_template('recursor.conf.tmpl') - config_text = tmpl.render(dns) - with open(config_file, 'w') as f: - f.write(config_text) + render(config_file, 'dns-forwarding/recursor.conf.tmpl', dns, trim_blocks=True) return None def apply(dns): if dns is None: # DNS forwarding is removed in the commit - run("systemctl stop pdns-recursor") + call("systemctl stop pdns-recursor.service") if os.path.isfile(config_file): os.unlink(config_file) else: - run("systemctl restart pdns-recursor") + call("systemctl restart pdns-recursor.service") if __name__ == '__main__': args = parser.parse_args() diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py index b9163f7b3..038f77cf9 100755 --- a/src/conf_mode/dynamic_dns.py +++ b/src/conf_mode/dynamic_dns.py @@ -18,18 +18,14 @@ import os from sys import exit from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from stat import S_IRUSR, S_IWUSR from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError -from vyos.util import run +from vyos.util import call +from vyos.template import render - -config_file = r'/etc/ddclient/ddclient.conf' -cache_file = r'/var/cache/ddclient/ddclient.cache' -pid_file = r'/var/run/ddclient/ddclient.pid' +config_file = r'/run/ddclient/ddclient.conf' # Mapping of service name to service protocol default_service_protocol = { @@ -48,9 +44,7 @@ default_service_protocol = { default_config_data = { 'interfaces': [], - 'cache_file': cache_file, - 'deleted': False, - 'pid_file': pid_file + 'deleted': False } def get_config(): @@ -221,28 +215,13 @@ def verify(dyndns): def generate(dyndns): # bail out early - looks like removal from running config if dyndns['deleted']: - if os.path.exists(config_file): - os.unlink(config_file) - return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'dynamic-dns') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - dirname = os.path.dirname(dyndns['pid_file']) - if not os.path.exists(dirname): - os.mkdir(dirname) - dirname = os.path.dirname(config_file) if not os.path.exists(dirname): os.mkdir(dirname) - tmpl = env.get_template('ddclient.conf.tmpl') - config_text = tmpl.render(dyndns) - with open(config_file, 'w') as f: - f.write(config_text) + render(config_file, 'dynamic-dns/ddclient.conf.tmpl', dyndns) # Config file must be accessible only by its owner os.chmod(config_file, S_IRUSR | S_IWUSR) @@ -250,18 +229,13 @@ def generate(dyndns): return None def apply(dyndns): - if os.path.exists(dyndns['cache_file']): - os.unlink(dyndns['cache_file']) - - if os.path.exists('/etc/ddclient.conf'): - os.unlink('/etc/ddclient.conf') - if dyndns['deleted']: - run('/etc/init.d/ddclient stop') - if os.path.exists(dyndns['pid_file']): - os.unlink(dyndns['pid_file']) + call('systemctl stop ddclient.service') + if os.path.exists(config_file): + os.unlink(config_file) + else: - run('/etc/init.d/ddclient restart') + call('systemctl restart ddclient.service') return None diff --git a/src/conf_mode/firewall_options.py b/src/conf_mode/firewall_options.py index 90f004bc4..0b800f48f 100755 --- a/src/conf_mode/firewall_options.py +++ b/src/conf_mode/firewall_options.py @@ -21,7 +21,7 @@ import copy from vyos.config import Config from vyos import ConfigError -from vyos.util import run +from vyos.util import call default_config_data = { @@ -87,19 +87,19 @@ def apply(tcp): target = 'VYOS_FW_OPTIONS' # always cleanup iptables - run('iptables --table mangle --delete FORWARD --jump {} >&/dev/null'.format(target)) - run('iptables --table mangle --flush {} >&/dev/null'.format(target)) - run('iptables --table mangle --delete-chain {} >&/dev/null'.format(target)) + call('iptables --table mangle --delete FORWARD --jump {} >&/dev/null'.format(target)) + call('iptables --table mangle --flush {} >&/dev/null'.format(target)) + call('iptables --table mangle --delete-chain {} >&/dev/null'.format(target)) # always cleanup ip6tables - run('ip6tables --table mangle --delete FORWARD --jump {} >&/dev/null'.format(target)) - run('ip6tables --table mangle --flush {} >&/dev/null'.format(target)) - run('ip6tables --table mangle --delete-chain {} >&/dev/null'.format(target)) + call('ip6tables --table mangle --delete FORWARD --jump {} >&/dev/null'.format(target)) + call('ip6tables --table mangle --flush {} >&/dev/null'.format(target)) + call('ip6tables --table mangle --delete-chain {} >&/dev/null'.format(target)) # Setup new iptables rules if tcp['new_chain4']: - run('iptables --table mangle --new-chain {} >&/dev/null'.format(target)) - run('iptables --table mangle --append FORWARD --jump {} >&/dev/null'.format(target)) + call('iptables --table mangle --new-chain {} >&/dev/null'.format(target)) + call('iptables --table mangle --append FORWARD --jump {} >&/dev/null'.format(target)) for opts in tcp['intf_opts']: intf = opts['intf'] @@ -111,13 +111,13 @@ def apply(tcp): # adjust TCP MSS per interface if mss: - run('iptables --table mangle --append {} --out-interface {} --protocol tcp ' \ + call('iptables --table mangle --append {} --out-interface {} --protocol tcp ' '--tcp-flags SYN,RST SYN --jump TCPMSS --set-mss {} >&/dev/null'.format(target, intf, mss)) # Setup new ip6tables rules if tcp['new_chain6']: - run('ip6tables --table mangle --new-chain {} >&/dev/null'.format(target)) - run('ip6tables --table mangle --append FORWARD --jump {} >&/dev/null'.format(target)) + call('ip6tables --table mangle --new-chain {} >&/dev/null'.format(target)) + call('ip6tables --table mangle --append FORWARD --jump {} >&/dev/null'.format(target)) for opts in tcp['intf_opts']: intf = opts['intf'] @@ -129,7 +129,7 @@ def apply(tcp): # adjust TCP MSS per interface if mss: - run('ip6tables --table mangle --append {} --out-interface {} --protocol tcp ' + call('ip6tables --table mangle --append {} --out-interface {} --protocol tcp ' '--tcp-flags SYN,RST SYN --jump TCPMSS --set-mss {} >&/dev/null'.format(target, intf, mss)) return None diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py index 1008f3fae..1354488ac 100755 --- a/src/conf_mode/flow_accounting_conf.py +++ b/src/conf_mode/flow_accounting_conf.py @@ -16,17 +16,18 @@ import os import re +from sys import exit import ipaddress from ipaddress import ip_address from jinja2 import FileSystemLoader, Environment -from sys import exit +from vyos.ifconfig import Section from vyos.ifconfig import Interface from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError from vyos.util import cmd +from vyos.render import render # default values @@ -60,7 +61,7 @@ def _sflow_default_agentip(config): return config.return_value('protocols ospfv3 parameters router-id') # if router-id was not found, use first available ip of any interface - for iface in Interface.listing(): + for iface in Section.interfaces(): for address in Interface(iface).get_addr(): # return an IP, if this is not loopback regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$') @@ -82,7 +83,7 @@ def _iptables_get_nflog(): for iptables_variant in ['iptables', 'ip6tables']: # run iptables, save output and split it by lines iptables_command = "sudo {0} -t {1} -S {2}".format(iptables_variant, iptables_nflog_table, iptables_nflog_chain) - cmd(iptables_command, universal_newlines=True, message='Failed to get flows list') + cmd(iptables_command, message='Failed to get flows list') iptables_out = stdout.splitlines() # parse each line and add information to list @@ -234,7 +235,7 @@ def verify(config): # check that all configured interfaces exists in the system for iface in config['interfaces']: - if not iface in Interface.listing(): + if not iface in Section.interfaces(): # chnged from error to warning to allow adding dynamic interfaces and interface templates # raise ConfigError("The {} interface is not presented in the system".format(iface)) print("Warning: the {} interface is not presented in the system".format(iface)) @@ -262,7 +263,7 @@ def verify(config): # check if configured sFlow agent-id exist in the system agent_id_presented = None - for iface in Interface.listing(): + for iface in Section.interfaces(): for address in Interface(iface).get_addr(): # check an IP, if this is not loopback regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$') @@ -334,16 +335,10 @@ def generate(config): timeout_string = "{}:{}={}".format(timeout_string, timeout_type, timeout_value) config['netflow']['timeout_string'] = timeout_string - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'netflow') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - # Generate daemon configs - tmpl = env.get_template('uacctd.conf.tmpl') - config_text = tmpl.render(templatecfg = config, snaplen = default_captured_packet_size) - with open(uacctd_conf_path, 'w') as file: - file.write(config_text) + render(uacctd_conf_path, 'netflow/uacctd.conf.tmpl', { + 'templatecfg': config, + 'snaplen': default_captured_packet_size, + }) def apply(config): @@ -351,9 +346,9 @@ def apply(config): command = None # Check if flow-accounting was removed and define command if not config['flow-accounting-configured']: - command = '/usr/bin/sudo /bin/systemctl stop uacctd' + command = 'systemctl stop uacctd.service' else: - command = '/usr/bin/sudo /bin/systemctl restart uacctd' + command = 'systemctl restart uacctd.service' # run command to start or stop flow-accounting cmd(command, raising=ConfigError, message='Failed to start/stop flow-accounting') diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index 690d1e030..dd5819f9f 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -33,7 +33,9 @@ import vyos.hostsd_client from vyos.config import Config from vyos import ConfigError -from vyos.util import cmd, run +from vyos.util import cmd +from vyos.util import call +from vyos.util import run default_config_data = { @@ -157,21 +159,21 @@ def apply(config): # rsyslog runs into a race condition at boot time with systemd # restart rsyslog only if the hostname changed. hostname_old = cmd('hostnamectl --static') - cmd(f'hostnamectl set-hostname --static {hostname_new}') + call(f'hostnamectl set-hostname --static {hostname_new}') # Restart services that use the hostname if hostname_new != hostname_old: - run("systemctl restart rsyslog.service") + call("systemctl restart rsyslog.service") # If SNMP is running, restart it too - ret = run("pgrep snmpd > /dev/null") + ret = run("pgrep snmpd") if ret == 0: - run("systemctl restart snmpd.service") + call("systemctl restart snmpd.service") # restart pdns if it is used - ret = run('/usr/bin/rec_control ping >/dev/null 2>&1') + ret = run('/usr/bin/rec_control ping') if ret == 0: - run('/etc/init.d/pdns-recursor restart >/dev/null') + call('systemctl restart pdns-recursor.service') return None diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py index 91b8aa34b..26f4aea7f 100755 --- a/src/conf_mode/http-api.py +++ b/src/conf_mode/http-api.py @@ -24,7 +24,8 @@ from copy import deepcopy import vyos.defaults from vyos.config import Config from vyos import ConfigError -from vyos.util import cmd, run +from vyos.util import cmd +from vyos.util import call config_file = '/etc/vyos/http-api.conf' @@ -91,9 +92,9 @@ def generate(http_api): def apply(http_api): if http_api is not None: - run('sudo systemctl restart vyos-http-api.service') + call('sudo systemctl restart vyos-http-api.service') else: - run('sudo systemctl stop vyos-http-api.service') + call('sudo systemctl stop vyos-http-api.service') for dep in dependencies: cmd(f'{vyos_conf_scripts_dir}/{dep}', raising=ConfigError) diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index 777792229..7d3a1b9cb 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -18,15 +18,14 @@ import os from sys import exit from copy import deepcopy -from jinja2 import FileSystemLoader, Environment import vyos.defaults import vyos.certbot_util from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError -from vyos.util import run +from vyos.util import call +from vyos.template import render config_file = '/etc/nginx/sites-available/default' @@ -133,26 +132,18 @@ def generate(https): if https is None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'https') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader, trim_blocks=True) - if 'server_block_list' not in https or not https['server_block_list']: https['server_block_list'] = [default_server_block] - tmpl = env.get_template('nginx.default.tmpl') - config_text = tmpl.render(https) - with open(config_file, 'w') as f: - f.write(config_text) + render(config_file, 'https/nginx.default.tmpl', https, trim_blocks=True) return None def apply(https): if https is not None: - run('sudo systemctl restart nginx.service') + call('sudo systemctl restart nginx.service') else: - run('sudo systemctl stop nginx.service') + call('sudo systemctl stop nginx.service') if __name__ == '__main__': try: diff --git a/src/conf_mode/igmp_proxy.py b/src/conf_mode/igmp_proxy.py index abe473530..9fa591a2c 100755 --- a/src/conf_mode/igmp_proxy.py +++ b/src/conf_mode/igmp_proxy.py @@ -18,13 +18,12 @@ import os from sys import exit from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from netifaces import interfaces from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError -from vyos.util import run +from vyos.util import call +from vyos.template import render config_file = r'/etc/igmpproxy.conf' @@ -116,26 +115,17 @@ def generate(igmp_proxy): print('Warning: IGMP Proxy will be deactivated because it is disabled') return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'igmp-proxy') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - tmpl = env.get_template('igmpproxy.conf.tmpl') - config_text = tmpl.render(igmp_proxy) - with open(config_file, 'w') as f: - f.write(config_text) - + render(config_file, 'igmp-proxy/igmpproxy.conf.tmpl', igmp_proxy) return None def apply(igmp_proxy): if igmp_proxy is None or igmp_proxy['disable']: # IGMP Proxy support is removed in the commit - run('sudo systemctl stop igmpproxy.service') + call('sudo systemctl stop igmpproxy.service') if os.path.exists(config_file): os.unlink(config_file) else: - run('systemctl restart igmpproxy.service') + call('systemctl restart igmpproxy.service') return None diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py index 6a002bc06..fd1f218d1 100755 --- a/src/conf_mode/interfaces-bonding.py +++ b/src/conf_mode/interfaces-bonding.py @@ -24,7 +24,8 @@ from vyos.ifconfig import BondIf from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config from vyos.configdict import list_diff, vlan_to_dict from vyos.config import Config -from vyos.util import run, is_bridge_member +from vyos.util import is_bridge_member +from vyos.util import call from vyos import ConfigError default_config_data = { @@ -91,7 +92,7 @@ def get_config(): if not os.path.isfile('/sys/class/net/bonding_masters'): import syslog syslog.syslog(syslog.LOG_NOTICE, "loading bonding kernel module") - if run('modprobe bonding max_bonds=0 miimon=250') != 0: + if call('modprobe bonding max_bonds=0 miimon=250') != 0: syslog.syslog(syslog.LOG_NOTICE, "failed loading bonding kernel module") raise ConfigError("failed loading bonding kernel module") @@ -398,32 +399,20 @@ def apply(bond): # update interface description used e.g. within SNMP b.set_alias(bond['description']) - # get DHCP config dictionary and update values - opt = b.get_dhcp_options() - if bond['dhcp_client_id']: - opt['client_id'] = bond['dhcp_client_id'] + b.dhcp.v4.options['client_id'] = bond['dhcp_client_id'] if bond['dhcp_hostname']: - opt['hostname'] = bond['dhcp_hostname'] + b.dhcp.v4.options['hostname'] = bond['dhcp_hostname'] if bond['dhcp_vendor_class_id']: - opt['vendor_class_id'] = bond['dhcp_vendor_class_id'] - - # store DHCP config dictionary - used later on when addresses are aquired - b.set_dhcp_options(opt) - - # get DHCPv6 config dictionary and update values - opt = b.get_dhcpv6_options() + b.dhcp.v4.options['vendor_class_id'] = bond['dhcp_vendor_class_id'] if bond['dhcpv6_prm_only']: - opt['dhcpv6_prm_only'] = True + b.dhcp.v6.options['dhcpv6_prm_only'] = True if bond['dhcpv6_temporary']: - opt['dhcpv6_temporary'] = True - - # store DHCPv6 config dictionary - used later on when addresses are required - b.set_dhcpv6_options(opt) + b.dhcp.v6.options['dhcpv6_temporary'] = True # ignore link state changes b.set_link_detect(bond['disable_link_detect']) diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py index 79247ee51..93c6db97e 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -300,32 +300,20 @@ def apply(bridge): # update interface description used e.g. within SNMP br.set_alias(bridge['description']) - # get DHCP config dictionary and update values - opt = br.get_dhcp_options() - if bridge['dhcp_client_id']: - opt['client_id'] = bridge['dhcp_client_id'] + br.dhcp.v4.options['client_id'] = bridge['dhcp_client_id'] if bridge['dhcp_hostname']: - opt['hostname'] = bridge['dhcp_hostname'] + br.dhcp.v4.options['hostname'] = bridge['dhcp_hostname'] if bridge['dhcp_vendor_class_id']: - opt['vendor_class_id'] = bridge['dhcp_vendor_class_id'] - - # store DHCPv6 config dictionary - used later on when addresses are aquired - br.set_dhcp_options(opt) - - # get DHCPv6 config dictionary and update values - opt = br.get_dhcpv6_options() + br.dhcp.v4.options['vendor_class_id'] = bridge['dhcp_vendor_class_id'] if bridge['dhcpv6_prm_only']: - opt['dhcpv6_prm_only'] = True + br.dhcp.v6.options['dhcpv6_prm_only'] = True if bridge['dhcpv6_temporary']: - opt['dhcpv6_temporary'] = True - - # store DHCPv6 config dictionary - used later on when addresses are aquired - br.set_dhcpv6_options(opt) + br.dhcp.v6.options['dhcpv6_temporary'] = True # assign/remove VRF br.set_vrf(bridge['vrf']) diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index 15e9b4185..5a977d797 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -301,32 +301,20 @@ def apply(eth): # update interface description used e.g. within SNMP e.set_alias(eth['description']) - # get DHCP config dictionary and update values - opt = e.get_dhcp_options() - if eth['dhcp_client_id']: - opt['client_id'] = eth['dhcp_client_id'] + e.dhcp.v4.options['client_id'] = eth['dhcp_client_id'] if eth['dhcp_hostname']: - opt['hostname'] = eth['dhcp_hostname'] + e.dhcp.v4.options['hostname'] = eth['dhcp_hostname'] if eth['dhcp_vendor_class_id']: - opt['vendor_class_id'] = eth['dhcp_vendor_class_id'] - - # store DHCP config dictionary - used later on when addresses are aquired - e.set_dhcp_options(opt) - - # get DHCPv6 config dictionary and update values - opt = e.get_dhcpv6_options() + e.dhcp.v4.options['vendor_class_id'] = eth['dhcp_vendor_class_id'] if eth['dhcpv6_prm_only']: - opt['dhcpv6_prm_only'] = True + e.dhcp.v6.options['dhcpv6_prm_only'] = True if eth['dhcpv6_temporary']: - opt['dhcpv6_temporary'] = True - - # store DHCPv6 config dictionary - used later on when addresses are aquired - e.set_dhcpv6_options(opt) + e.dhcp.v6.options['dhcpv6_temporary'] = True # ignore link state changes e.set_link_detect(eth['disable_link_detect']) diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py index 0400cb849..11ba9acdd 100755 --- a/src/conf_mode/interfaces-l2tpv3.py +++ b/src/conf_mode/interfaces-l2tpv3.py @@ -22,7 +22,8 @@ from copy import deepcopy from vyos.config import Config from vyos.ifconfig import L2TPv3If, Interface from vyos import ConfigError -from vyos.util import run, is_bridge_member +from vyos.util import call +from vyos.util import is_bridge_member from netifaces import interfaces default_config_data = { @@ -51,7 +52,7 @@ def check_kmod(): modules = ['l2tp_eth', 'l2tp_netlink', 'l2tp_ip', 'l2tp_ip6'] for module in modules: if not os.path.exists(f'/sys/module/{module}'): - if run(f'modprobe {module}') != 0: + if call(f'modprobe {module}') != 0: raise ConfigError(f'Loading Kernel module {module} failed') def get_config(): diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index e9b40bb38..b42765586 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -17,23 +17,20 @@ import os import re -from jinja2 import FileSystemLoader, Environment from copy import deepcopy -from sys import exit -from stat import S_IRUSR,S_IRWXU,S_IRGRP,S_IXGRP,S_IROTH,S_IXOTH -from grp import getgrnam -from ipaddress import ip_address,ip_network,IPv4Interface +from sys import exit,stderr +from ipaddress import IPv4Address,IPv4Network,summarize_address_range from netifaces import interfaces -from pwd import getpwnam from time import sleep from shutil import rmtree from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos.ifconfig import VTunIf -from vyos.util import process_running, cmd, is_bridge_member +from vyos.util import call, is_bridge_member, chown, chmod_600, chmod_755 from vyos.validate import is_addr_assigned from vyos import ConfigError +from vyos.template import render + user = 'openvpn' group = 'openvpn' @@ -75,10 +72,14 @@ default_config_data = { 'server_domain': '', 'server_max_conn': '', 'server_dns_nameserver': [], + 'server_pool': False, + 'server_pool_start': '', + 'server_pool_stop': '', + 'server_pool_netmask': '', 'server_push_route': [], 'server_reject_unconfigured': False, 'server_subnet': '', - 'server_topology': '', + 'server_topology': 'net30', 'shared_secret_file': '', 'tls': False, 'tls_auth': '', @@ -97,32 +98,9 @@ default_config_data = { def get_config_name(intf): - cfg_file = r'/opt/vyatta/etc/openvpn/openvpn-{}.conf'.format(intf) + cfg_file = f'/run/openvpn/{intf}.conf' return cfg_file -def openvpn_mkdir(directory): - # create directory on demand - if not os.path.exists(directory): - os.mkdir(directory) - - # fix permissions - corresponds to mode 755 - os.chmod(directory, S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH) - uid = getpwnam(user).pw_uid - gid = getgrnam(group).gr_gid - os.chown(directory, uid, gid) - -def fixup_permission(filename, permission=S_IRUSR): - """ - Check if the given file exists and change ownershit to root/vyattacfg - and appripriate file access permissions - default is user and group readable - """ - if os.path.isfile(filename): - os.chmod(filename, permission) - - # make file owned by root / vyattacfg - uid = getpwnam('root').pw_uid - gid = getgrnam('vyattacfg').gr_gid - os.chown(filename, uid, gid) def checkCertHeader(header, filename): """ @@ -139,6 +117,66 @@ def checkCertHeader(header, filename): return False +def getDefaultServer(network, topology, devtype): + """ + Gets the default server parameters for a "server" directive. + Currently only IPv4 routed but may be extended to support bridged and/or IPv6 in the future. + Logic from openvpn's src/openvpn/helper.c. + Returns a dict with addresses or False if the input parameters were incorrect. + """ + if not (topology and devtype): + return False + + if not (devtype == 'tun' or devtype == 'tap'): + return False + + if not network.prefixlen: + return False + elif (devtype == 'tun' and network.prefixlen > 29) or (devtype == 'tap' and network.prefixlen > 30): + return False + + server = { + 'local': '', + 'remote_netmask': '', + 'client_remote_netmask': '', + 'pool_start': '', + 'pool_stop': '', + 'pool_netmask': '' + } + + if devtype == 'tun': + if topology == 'net30' or topology == 'point-to-point': + server['local'] = network[1] + server['remote_netmask'] = network[2] + server['client_remote_netmask'] = server['local'] + + # pool start is 4th host IP in subnet (.4 in a /24) + server['pool_start'] = network[4] + + if network.prefixlen == 29: + server['pool_stop'] = network.broadcast_address + else: + # pool end is -4 from the broadcast address (.251 in a /24) + server['pool_stop'] = network[-5] + + elif topology == 'subnet': + server['local'] = network[1] + server['remote_netmask'] = str(network.netmask) + server['client_remote_netmask'] = server['remote_netmask'] + server['pool_start'] = network[2] + server['pool_stop'] = network[-3] + server['pool_netmask'] = server['remote_netmask'] + + elif devtype == 'tap': + server['local'] = network[1] + server['remote_netmask'] = str(network.netmask) + server['client_remote_netmask'] = server['remote_netmask'] + server['pool_start'] = network[2] + server['pool_stop'] = network[-2] + server['pool_netmask'] = server['remote_netmask'] + + return server + def get_config(): openvpn = deepcopy(default_config_data) conf = Config() @@ -308,10 +346,10 @@ def get_config(): # Server-mode subnet (from which client IPs are allocated) if conf.exists('server subnet'): - network = conf.return_value('server subnet') - tmp = IPv4Interface(network).with_netmask + # server_network is used later in this function + server_network = IPv4Network(conf.return_value('server subnet')) # convert the network in format: "192.0.2.0 255.255.255.0" for later use in template - openvpn['server_subnet'] = tmp.replace(r'/', ' ') + openvpn['server_subnet'] = server_network.with_netmask.replace(r'/', ' ') # Client-specific settings for client in conf.list_nodes('server client'): @@ -326,19 +364,6 @@ def get_config(): 'remote_netmask': '' } - # note: with "topology subnet", this is "<ip> <netmask>". - # with "topology p2p", this is "<ip> <our_ip>". - if openvpn['server_topology'] == 'subnet': - # we are only interested in the netmask portion of server_subnet - data['remote_netmask'] = openvpn['server_subnet'].split(' ')[1] - else: - # we need the server subnet in format 192.0.2.0/255.255.255.0 - subnet = openvpn['server_subnet'].replace(' ', r'/') - # get iterator over the usable hosts in the network - tmp = ip_network(subnet).hosts() - # OpenVPN always uses the subnets first available IP address - data['remote_netmask'] = list(tmp)[0] - # Option to disable client connection if conf.exists('disable'): data['disable'] = True @@ -349,13 +374,11 @@ def get_config(): # Route to be pushed to the client for network in conf.return_values('push-route'): - tmp = IPv4Interface(network).with_netmask - data['push_route'].append(tmp.replace(r'/', ' ')) + data['push_route'].append(IPv4Network(network).with_netmask.replace(r'/', ' ')) # Subnet belonging to the client for network in conf.return_values('subnet'): - tmp = IPv4Interface(network).with_netmask - data['subnet'].append(tmp.replace(r'/', ' ')) + data['subnet'].append(IPv4Network(network).with_netmask.replace(r'/', ' ')) # Append to global client list openvpn['client'].append(data) @@ -363,6 +386,19 @@ def get_config(): # re-set configuration level conf.set_level('interfaces openvpn ' + openvpn['intf']) + # Server client IP pool + if conf.exists('server client-ip-pool'): + openvpn['server_pool'] = True + + if conf.exists('server client-ip-pool start'): + openvpn['server_pool_start'] = conf.return_value('server client-ip-pool start') + + if conf.exists('server client-ip-pool stop'): + openvpn['server_pool_stop'] = conf.return_value('server client-ip-pool stop') + + if conf.exists('server client-ip-pool netmask'): + openvpn['server_pool_netmask'] = conf.return_value('server client-ip-pool netmask') + # DNS suffix to be pushed to all clients if conf.exists('server domain-name'): openvpn['server_domain'] = conf.return_value('server domain-name') @@ -378,8 +414,7 @@ def get_config(): # Route to be pushed to all clients if conf.exists('server push-route'): for network in conf.return_values('server push-route'): - tmp = IPv4Interface(network).with_netmask - openvpn['server_push_route'].append(tmp.replace(r'/', ' ')) + openvpn['server_push_route'].append(IPv4Network(network).with_netmask.replace(r'/', ' ')) # Reject connections from clients that are not explicitly configured if conf.exists('server reject-unconfigured-clients'): @@ -428,6 +463,7 @@ def get_config(): # Minimum required TLS version if conf.exists('tls tls-version-min'): openvpn['tls_version_min'] = conf.return_value('tls tls-version-min') + openvpn['tls'] = True if conf.exists('shared-secret-key-file'): openvpn['shared_secret_file'] = conf.return_value('shared-secret-key-file') @@ -440,6 +476,26 @@ def get_config(): if not openvpn['tls_dh'] and openvpn['tls_key'] and checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls_key']): openvpn['tls_dh'] = 'none' + # Set defaults where necessary. + # If any of the input parameters are missing or wrong, + # this will return False and no defaults will be set. + default_server = getDefaultServer(server_network, openvpn['server_topology'], openvpn['type']) + if default_server: + # server-bridge doesn't require a pool so don't set defaults for it + if not openvpn['bridge_member']: + openvpn['server_pool'] = True + if not openvpn['server_pool_start']: + openvpn['server_pool_start'] = default_server['pool_start'] + + if not openvpn['server_pool_stop']: + openvpn['server_pool_stop'] = default_server['pool_stop'] + + if not openvpn['server_pool_netmask']: + openvpn['server_pool_netmask'] = default_server['pool_netmask'] + + for client in openvpn['client']: + client['remote_netmask'] = default_server['client_remote_netmask'] + return openvpn def verify(openvpn): @@ -489,7 +545,11 @@ def verify(openvpn): # OpenVPN site-to-site - VERIFY # if openvpn['mode'] == 'site-to-site': - if not (openvpn['local_address'] or openvpn['bridge_member']): + if openvpn['ncp_ciphers']: + raise ConfigError('encryption ncp-ciphers cannot be specified in site-to-site mode, only server or client') + + if openvpn['mode'] == 'site-to-site' and not openvpn['bridge_member']: + if not openvpn['local_address']: raise ConfigError('Must specify "local-address" or "bridge member interface"') for host in openvpn['remote_host']: @@ -506,15 +566,10 @@ def verify(openvpn): if openvpn['local_address'] == openvpn['local_host']: raise ConfigError('"local-address" cannot be the same as "local-host"') - if openvpn['ncp_ciphers']: - raise ConfigError('encryption ncp-ciphers cannot be specified in site-to-site mode, only server or client') - else: + # checks for client-server or site-to-site bridged if openvpn['local_address'] or openvpn['remote_address']: - raise ConfigError('Cannot specify "local-address" or "remote-address" in client-server mode') - - elif openvpn['bridge_member']: - raise ConfigError('Cannot specify "local-address" or "remote-address" in bridge mode') + raise ConfigError('Cannot specify "local-address" or "remote-address" in client-server or bridge mode') # # OpenVPN server mode - VERIFY @@ -535,9 +590,41 @@ def verify(openvpn): if not openvpn['tls_dh'] and not checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls_key']): raise ConfigError('Must specify "tls dh-file" when not using EC keys in server mode') - if not openvpn['server_subnet']: + if openvpn['server_subnet']: + subnet = IPv4Network(openvpn['server_subnet'].replace(' ', '/')) + + if openvpn['type'] == 'tun' and subnet.prefixlen > 29: + raise ConfigError('Server subnets smaller than /29 with device type "tun" are not supported') + elif openvpn['type'] == 'tap' and subnet.prefixlen > 30: + raise ConfigError('Server subnets smaller than /30 with device type "tap" are not supported') + + for client in openvpn['client']: + if client['ip'] and not IPv4Address(client['ip']) in subnet: + raise ConfigError(f'Client IP "{client["ip"]}" not in server subnet "{subnet}"') + + else: if not openvpn['bridge_member']: - raise ConfigError('Must specify "server subnet" option in server mode') + raise ConfigError('Must specify "server subnet" or "bridge member interface" in server mode') + + + if openvpn['server_pool']: + if not (openvpn['server_pool_start'] and openvpn['server_pool_stop']): + raise ConfigError('Server client-ip-pool requires both start and stop addresses in bridged mode') + else: + v4PoolStart = IPv4Address(openvpn['server_pool_start']) + v4PoolStop = IPv4Address(openvpn['server_pool_stop']) + if v4PoolStart > v4PoolStop: + raise ConfigError(f'Server client-ip-pool start address {v4PoolStart} is larger than stop address {v4PoolStop}') + if (int(v4PoolStop) - int(v4PoolStart) >= 65536): + raise ConfigError(f'Server client-ip-pool is too large [{v4PoolStart} -> {v4PoolStop}], maximum is 65536 addresses.') + + v4PoolNets = list(summarize_address_range(v4PoolStart, v4PoolStop)) + for client in openvpn['client']: + if client['ip']: + for v4PoolNet in v4PoolNets: + if IPv4Address(client['ip']) in v4PoolNet: + print(f'Warning: Client "{client["name"]}" IP {client["ip"]} is in server IP pool, it is not reserved for this client.', + file=stderr) else: # checks for both client and site-to-site go here @@ -665,143 +752,98 @@ def verify(openvpn): if not openvpn['auth_pass']: raise ConfigError('Password for authentication is missing') - # - # Client - # - subnet = openvpn['server_subnet'].replace(' ', '/') - for client in openvpn['client']: - if client['ip'] and not ip_address(client['ip']) in ip_network(subnet): - raise ConfigError('Client IP "{}" not in server subnet "{}'.format(client['ip'], subnet)) - return None def generate(openvpn): if openvpn['deleted'] or openvpn['disable']: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'openvpn') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - interface = openvpn['intf'] directory = os.path.dirname(get_config_name(interface)) - # we can't know which clients were deleted, remove all client configs - if os.path.isdir(os.path.join(directory, 'ccd', interface)): - rmtree(os.path.join(directory, 'ccd', interface), ignore_errors=True) + # we can't know in advance which clients have been, + # remove all client configs + ccd_dir = os.path.join(directory, 'ccd', interface) + if os.path.isdir(ccd_dir): + rmtree(ccd_dir, ignore_errors=True) # create config directory on demand - openvpn_mkdir(directory) - # create status directory on demand - openvpn_mkdir(directory + '/status') - # create client config dir on demand - openvpn_mkdir(directory + '/ccd') - # crete client config dir per interface on demand - openvpn_mkdir(directory + '/ccd/' + interface) + directories = [] + directories.append(f'{directory}/status') + directories.append(f'{directory}/ccd/{interface}') + for onedir in directories: + if not os.path.exists(onedir): + os.makedirs(onedir, 0o755) + chown(onedir, user, group) # Fix file permissons for keys - fixup_permission(openvpn['shared_secret_file']) - fixup_permission(openvpn['tls_key']) + fix_permissions = [] + fix_permissions.append(openvpn['shared_secret_file']) + fix_permissions.append(openvpn['tls_key']) # Generate User/Password authentication file + user_auth_file = f'/tmp/openvpn-{interface}-pw' if openvpn['auth']: - auth_file = '/tmp/openvpn-{}-pw'.format(interface) - with open(auth_file, 'w') as f: + with open(user_auth_file, 'w') as f: f.write('{}\n{}'.format(openvpn['auth_user'], openvpn['auth_pass'])) - - fixup_permission(auth_file) + # also change permission on auth file + fix_permissions.append(user_auth_file) else: # delete old auth file if present - if os.path.isfile('/tmp/openvpn-{}-pw'.format(interface)): - os.remove('/tmp/openvpn-{}-pw'.format(interface)) - - # get numeric uid/gid - uid = getpwnam(user).pw_uid - gid = getgrnam(group).gr_gid + if os.path.isfile(user_auth_file): + os.remove(user_auth_file) # Generate client specific configuration for client in openvpn['client']: - client_file = directory + '/ccd/' + interface + '/' + client['name'] - tmpl = env.get_template('client.conf.tmpl') - client_text = tmpl.render(client) - with open(client_file, 'w') as f: - f.write(client_text) - os.chown(client_file, uid, gid) - - tmpl = env.get_template('server.conf.tmpl') - config_text = tmpl.render(openvpn) + client_file = os.path.join(ccd_dir, client['name']) + render(client_file, 'openvpn/client.conf.tmpl', client) + chown(client_file, user, group) + # we need to support quoting of raw parameters from OpenVPN CLI # see https://phabricator.vyos.net/T1632 - config_text = config_text.replace(""",'"') - with open(get_config_name(interface), 'w') as f: - f.write(config_text) - os.chown(get_config_name(interface), uid, gid) + render(get_config_name(interface), 'openvpn/server.conf.tmpl', openvpn, + formater=lambda _: _.replace(""", '"')) + chown(get_config_name(interface), user, group) + + # Fixup file permissions + for file in fix_permissions: + chmod_600(file) return None def apply(openvpn): - pidfile = '/var/run/openvpn/{}.pid'.format(openvpn['intf']) - - # Always stop OpenVPN service. We can not send a SIGUSR1 for restart of the - # service as the configuration is not re-read. Stop daemon only if it's - # running - it could have died or killed by someone evil - if process_running(pidfile): - command = 'start-stop-daemon' - command += ' --stop ' - command += ' --quiet' - command += ' --oknodo' - command += ' --pidfile ' + pidfile - cmd(command) - - # cleanup old PID file - if os.path.isfile(pidfile): - os.remove(pidfile) + interface = openvpn['intf'] + call(f'systemctl stop openvpn@{interface}.service') # Do some cleanup when OpenVPN is disabled/deleted if openvpn['deleted'] or openvpn['disable']: # cleanup old configuration file - if os.path.isfile(get_config_name(openvpn['intf'])): - os.remove(get_config_name(openvpn['intf'])) + if os.path.isfile(get_config_name(interface)): + os.remove(get_config_name(interface)) # cleanup client config dir - directory = os.path.dirname(get_config_name(openvpn['intf'])) - if os.path.isdir(os.path.join(directory, 'ccd', openvpn['intf'])): - rmtree(os.path.join(directory, 'ccd', openvpn['intf']), ignore_errors=True) - - # cleanup auth file - if os.path.isfile('/tmp/openvpn-{}-pw'.format(openvpn['intf'])): - os.remove('/tmp/openvpn-{}-pw'.format(openvpn['intf'])) + directory = os.path.dirname(get_config_name(interface)) + ccd_dir = os.path.join(directory, 'ccd', interface) + if os.path.isdir(ccd_dir): + rmtree(ccd_dir, ignore_errors=True) return None # On configuration change we need to wait for the 'old' interface to # vanish from the Kernel, if it is not gone, OpenVPN will report: # ERROR: Cannot ioctl TUNSETIFF vtun10: Device or resource busy (errno=16) - while openvpn['intf'] in interfaces(): + while interface in interfaces(): sleep(0.250) # 250ms # No matching OpenVPN process running - maybe it got killed or none # existed - nevertheless, spawn new OpenVPN process - command = 'start-stop-daemon' - command += ' --start ' - command += ' --quiet' - command += ' --oknodo' - command += ' --pidfile ' + pidfile - command += ' --exec /usr/sbin/openvpn' - # now pass arguments to openvpn binary - command += ' --' - command += ' --daemon openvpn-' + openvpn['intf'] - command += ' --config ' + get_config_name(openvpn['intf']) - - # execute assembled command - cmd(command) + call(f'systemctl start openvpn@{interface}.service') # better late then sorry ... but we can only set interface alias after # OpenVPN has been launched and created the interface cnt = 0 - while openvpn['intf'] not in interfaces(): + while interface not in interfaces(): # If VPN tunnel can't be established because the peer/server isn't # (temporarily) available, the vtun interface never becomes registered # with the kernel, and the commit would hang if there is no bail out @@ -816,7 +858,7 @@ def apply(openvpn): try: # we need to catch the exception if the interface is not up due to # reason stated above - o = VTunIf(openvpn['intf']) + o = VTunIf(interface) # update interface description used e.g. within SNMP o.set_alias(openvpn['description']) # IPv6 address autoconfiguration @@ -834,7 +876,7 @@ def apply(openvpn): # TAP interface needs to be brought up explicitly if openvpn['type'] == 'tap': if not openvpn['disable']: - VTunIf(openvpn['intf']).set_admin_state('up') + VTunIf(interface).set_admin_state('up') return None diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py index 407547175..f942b7d2f 100755 --- a/src/conf_mode/interfaces-pppoe.py +++ b/src/conf_mode/interfaces-pppoe.py @@ -18,14 +18,14 @@ import os from sys import exit from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from netifaces import interfaces from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos.ifconfig import Interface -from vyos.util import chown_file, chmod_x, cmd +from vyos.util import chown, chmod_755, cmd from vyos import ConfigError +from vyos.template import render + default_config_data = { 'access_concentrator': '', @@ -155,14 +155,12 @@ def verify(pppoe): if vrf_name and vrf_name not in interfaces(): raise ConfigError(f'VRF {vrf_name} does not exist') + if pppoe['on_demand'] and pppoe['vrf']: + raise ConfigError('On-demand dialing and VRF can not be used at the same time') + return None def generate(pppoe): - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir["data"], "templates", "pppoe") - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - # set up configuration file path variables where our templates will be # rendered into intf = pppoe['intf'] @@ -192,40 +190,26 @@ def generate(pppoe): else: # Create PPP configuration files - tmpl = env.get_template('peer.tmpl') - config_text = tmpl.render(pppoe) - with open(config_pppoe, 'w') as f: - f.write(config_text) - + render(config_pppoe, 'pppoe/peer.tmpl', + pppoe, trim_blocks=True) # Create script for ip-pre-up.d - tmpl = env.get_template('ip-pre-up.script.tmpl') - config_text = tmpl.render(pppoe) - with open(script_pppoe_pre_up, 'w') as f: - f.write(config_text) - + render(script_pppoe_pre_up, 'pppoe/ip-pre-up.script.tmpl', + pppoe, trim_blocks=True) # Create script for ip-up.d - tmpl = env.get_template('ip-up.script.tmpl') - config_text = tmpl.render(pppoe) - with open(script_pppoe_ip_up, 'w') as f: - f.write(config_text) - + render(script_pppoe_ip_up, 'pppoe/ip-up.script.tmpl', + pppoe, trim_blocks=True) # Create script for ip-down.d - tmpl = env.get_template('ip-down.script.tmpl') - config_text = tmpl.render(pppoe) - with open(script_pppoe_ip_down, 'w') as f: - f.write(config_text) - + render(script_pppoe_ip_down, 'pppoe/ip-down.script.tmpl', + pppoe, trim_blocks=True) # Create script for ipv6-up.d - tmpl = env.get_template('ipv6-up.script.tmpl') - config_text = tmpl.render(pppoe) - with open(script_pppoe_ipv6_up, 'w') as f: - f.write(config_text) + render(script_pppoe_ipv6_up, 'pppoe/ipv6-up.script.tmpl', + pppoe, trim_blocks=True) # make generated script file executable - chmod_x(script_pppoe_pre_up) - chmod_x(script_pppoe_ip_up) - chmod_x(script_pppoe_ip_down) - chmod_x(script_pppoe_ipv6_up) + chmod_755(script_pppoe_pre_up) + chmod_755(script_pppoe_ip_up) + chmod_755(script_pppoe_ip_down) + chmod_755(script_pppoe_ipv6_up) return None @@ -240,7 +224,7 @@ def apply(pppoe): cmd(f'systemctl start ppp@{intf}.service') # make logfile owned by root / vyattacfg - chown_file(pppoe['logfile'], 'root', 'vyattacfg') + chown(pppoe['logfile'], 'root', 'vyattacfg') return None diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py index 50b5a12a0..655006146 100755 --- a/src/conf_mode/interfaces-pseudo-ethernet.py +++ b/src/conf_mode/interfaces-pseudo-ethernet.py @@ -51,8 +51,8 @@ default_config_data = { 'ipv6_forwarding': 1, 'ipv6_dup_addr_detect': 1, 'intf': '', - 'link': '', - 'link_changed': False, + 'source_interface': '', + 'source_interface_changed': False, 'mac': '', 'mode': 'private', 'vif_s': [], @@ -166,12 +166,12 @@ def get_config(): if conf.exists('ipv6 dup-addr-detect-transmits'): peth['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits')) - # Lower link device - if conf.exists(['link']): - peth['link'] = conf.return_value(['link']) - tmp = conf.return_effective_value(['link']) - if tmp != peth['link']: - peth['link_changed'] = True + # Physical interface + if conf.exists(['source-interface']): + peth['source_interface'] = conf.return_value(['source-interface']) + tmp = conf.return_effective_value(['source-interface']) + if tmp != peth['source_interface']: + peth['source_interface_changed'] = True # Media Access Control (MAC) address if conf.exists(['mac']): @@ -227,10 +227,10 @@ def verify(peth): 'is a member of bridge "{1}"!'.format(interface, bridge)) return None - if not peth['link']: + if not peth['source_interface']: raise ConfigError('Link device must be set for virtual ethernet {}'.format(peth['intf'])) - if not peth['link'] in interfaces(): + if not peth['source_interface'] in interfaces(): raise ConfigError('Pseudo-ethernet source interface does not exist') vrf_name = peth['vrf'] @@ -253,12 +253,12 @@ def apply(peth): p.remove() return None - elif peth['link_changed']: + elif peth['source_interface_changed']: # Check if MACVLAN interface already exists. Parameters like the - # underlaying link device can not be changed on the fly and the - # interface needs to be recreated from the bottom. + # underlaying source-interface device can not be changed on the fly + # and the interface needs to be recreated from the bottom. # - # link_changed also means - the interface was not present in the + # source_interface_changed also means - the interface was not present in the # beginning and is newly created if peth['intf'] in interfaces(): p = MACVLANIf(peth['intf']) @@ -269,7 +269,7 @@ def apply(peth): conf = deepcopy(MACVLANIf.get_config()) # Assign MACVLAN instance configuration parameters to config dict - conf['link'] = peth['link'] + conf['source_interface'] = peth['source_interface'] conf['mode'] = peth['mode'] # It is safe to "re-create" the interface always, there is a sanity check @@ -281,32 +281,20 @@ def apply(peth): # update interface description used e.g. within SNMP p.set_alias(peth['description']) - # get DHCP config dictionary and update values - opt = p.get_dhcp_options() - if peth['dhcp_client_id']: - opt['client_id'] = peth['dhcp_client_id'] + p.dhcp.v4.options['client_id'] = peth['dhcp_client_id'] if peth['dhcp_hostname']: - opt['hostname'] = peth['dhcp_hostname'] + p.dhcp.v4.options['hostname'] = peth['dhcp_hostname'] if peth['dhcp_vendor_class_id']: - opt['vendor_class_id'] = peth['dhcp_vendor_class_id'] - - # store DHCP config dictionary - used later on when addresses are aquired - p.set_dhcp_options(opt) - - # get DHCPv6 config dictionary and update values - opt = p.get_dhcpv6_options() + p.dhcp.v4.options['vendor_class_id'] = peth['dhcp_vendor_class_id'] if peth['dhcpv6_prm_only']: - opt['dhcpv6_prm_only'] = True + p.dhcp.v6.options['dhcpv6_prm_only'] = True if peth['dhcpv6_temporary']: - opt['dhcpv6_temporary'] = True - - # store DHCPv6 config dictionary - used later on when addresses are aquired - p.set_dhcpv6_options(opt) + p.dhcp.v6.options['dhcpv6_temporary'] = True # ignore link state changes p.set_link_detect(peth['disable_link_detect']) diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index 646e61c53..c51048aeb 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -26,21 +26,68 @@ from vyos.ifconfig.afi import IP4, IP6 from vyos.configdict import list_diff from vyos.validate import is_ipv4, is_ipv6 from vyos import ConfigError +from vyos.dicts import FixedDict +class ConfigurationState(Config): + """ + The current API require a dict to be generated by get_config() + which is then consumed by verify(), generate() and apply() -class FixedDict(dict): - def __init__ (self, **options): - self._allowed = options.keys() - super().__init__(**options) + ConfiguartionState is an helper class wrapping Config and providing + an common API to this dictionary structure - def __setitem__ (self, k, v): - if k not in self._allowed: - raise ConfigError(f'Option "{k}" has no defined default') - super().__setitem__(k, v) + Its to_dict() function return a dictionary containing three fields, + each a dict, called options, changes, actions. + options: + + contains the configuration options for the dict and its value + {'options': {'commment': 'test'}} will be set if + 'set interface dummy dum1 description test' was used and + the key 'commment' is used to index the description info. + + changes: + + per key, let us know how the data was modified using one of the action + a special key called 'section' is used to indicate what happened to the + section. for example: + + 'set interface dummy dum1 description test' when no interface was setup + will result in the following changes + {'changes': {'section': 'create', 'comment': 'create'}} + + on an existing interface, depending if there was a description + 'set interface dummy dum1 description test' will result in one of + {'changes': {'comment': 'create'}} (not present before) + {'changes': {'comment': 'static'}} (unchanged) + {'changes': {'comment': 'modify'}} (changed from half) + + and 'delete interface dummy dummy1 description' will result in: + {'changes': {'comment': 'delete'}} + + actions: + + for each action list the configuration key which were changes + in our example if we added the 'description' and added an IP we would have + {'actions': { 'create': ['comment'], 'modify': ['addresses-add']}} + + the actions are: + 'create': it did not exist previously and was created + 'modify': it did exist previously but its content changed + 'static': it did exist and did not change + 'delete': it was present but was removed from the configuration + 'absent': it was not and is not present + which for each field represent how it was modified since the last commit + """ -class ConfigurationState (Config): def __init__ (self, section, default): + """ + initialise the class for a given configuration path: + + >>> conf = ConfigurationState('interfaces ethernet eth1') + all further references to get_value(s) and get_effective(s) + will be for this part of the configuration (eth1) + """ super().__init__() self.section = section self.default = deepcopy(default) @@ -61,6 +108,15 @@ class ConfigurationState (Config): self.changes['section'] = 'create' def _act(self, section): + """ + Returns for a given configuration field determine what happened to it + + 'create': it did not exist previously and was created + 'modify': it did exist previously but its content changed + 'static': it did exist and did not change + 'delete': it was present but was removed from the configuration + 'absent': it was not and is not present + """ if self.exists(section): if self.exists_effective(section): if self.return_value(section) != self.return_effective_value(section): @@ -89,24 +145,71 @@ class ConfigurationState (Config): self.options[name] = value def get_value(self, name, key, default=None): + """ + >>> conf.get_value('comment', 'description') + will place the string of 'interface dummy description test' + into the dictionnary entry 'comment' using Config.return_value + (the data in the configuration to apply) + """ if self._action(name, key) in ('delete', 'absent'): return return self._get(name, key, default, self.return_value) def get_values(self, name, key, default=None): + """ + >>> conf.get_values('addresses-add', 'address') + will place a list made of the IP present in 'interface dummy dum1 address' + into the dictionnary entry 'addr' using Config.return_values + (the data in the configuration to apply) + """ if self._action(name, key) in ('delete', 'absent'): return return self._get(name, key, default, self.return_values) def get_effective(self, name, key, default=None): + """ + >>> conf.get_value('comment', 'description') + will place the string of 'interface dummy description test' + into the dictionnary entry 'comment' using Config.return_effective_value + (the data in the configuration to apply) + """ self._action(name, key) return self._get(name, key, default, self.return_effective_value) def get_effectives(self, name, key, default=None): + """ + >>> conf.get_effectives('addresses-add', 'address') + will place a list made of the IP present in 'interface ethernet eth1 address' + into the dictionnary entry 'addresses-add' using Config.return_effectives_value + (the data in the un-modified configuration) + """ self._action(name, key) return self._get(name, key, default, self.return_effectives_value) def load(self, mapping): + """ + load will take a dictionary defining how we wish the configuration + to be parsed and apply this definition to set the data. + + >>> mapping = { + 'addresses-add' : ('address', True, None), + 'comment' : ('description', False, 'auto'), + } + >>> conf.load(mapping) + + mapping is a dictionary where each key represents the name we wish + to have (such as 'addresses-add'), with a list a content representing + how the data should be parsed: + - the configuration section name + such as 'address' under 'interface ethernet eth1' + - boolean indicating if this data can have multiple values + for 'address', True, as multiple IPs can be set + for 'description', False, as it is a single string + - default represent the default value if absent from the configuration + 'None' indicate that no default should be set if the configuration + does not have the configuration section + + """ for local_name, (config_name, multiple, default) in mapping.items(): if multiple: self.get_values(local_name, config_name, default) @@ -114,12 +217,21 @@ class ConfigurationState (Config): self.get_value(local_name, config_name, default) def remove_default (self,*options): + """ + remove all the values which were not changed from the default + """ for option in options: if self.exists(option) and self_return_value(option) != self.default[option]: continue del self.options[option] def to_dict (self): + """ + provide a dictionary with the generated data for the configuration + options: the configuration value for the key + changes: per key how they changed from the previous configuration + actions: per changes all the options which were changed + """ # as we have to use a dict() for the API for verify and apply the options return { 'options': self.options, @@ -203,7 +315,8 @@ def get_class (options): } kls = dispatch[options['type']] - if options['type'] == 'gre' and not options['remote']: + if options['type'] == 'gre' and not options['remote'] \ + and not options['key'] and not options['multicast']: # will use GreTapIf on GreIf deletion but it does not matter return GRETapIf elif options['type'] == 'sit' and options['6rd-prefix']: @@ -471,11 +584,17 @@ def apply(conf): if changes['section'] in 'create' and option in tunnel.options: # it was setup at creation continue + if not options[option]: + # remote can be set to '' and it would generate an invalide command + continue tunnel.set_interface(option, options[option]) # set other interface properties for option in ('alias', 'mtu', 'link_detect', 'multicast', 'allmulticast', 'vrf', 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits'): + if not options[option]: + # should never happen but better safe + continue tunnel.set_interface(option, options[option]) # Configure interface address(es) diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index b9bfb242a..6639a9b0d 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -42,7 +42,8 @@ default_config_data = { 'ipv6_eui64_prefix': '', 'ipv6_forwarding': 1, 'ipv6_dup_addr_detect': 1, - 'link': '', + 'source_address': '', + 'source_interface': '', 'mtu': 1450, 'remote': '', 'remote_port': 8472, # The Linux implementation of VXLAN pre-dates @@ -124,9 +125,13 @@ def get_config(): if conf.exists('ipv6 dup-addr-detect-transmits'): vxlan['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits')) + # VXLAN source address + if conf.exists('source-address'): + vxlan['source_address'] = conf.return_value('source-address') + # VXLAN underlay interface - if conf.exists('link'): - vxlan['link'] = conf.return_value('link') + if conf.exists('source-interface'): + vxlan['source_interface'] = conf.return_value('source-interface') # Maximum Transmission Unit (MTU) if conf.exists('mtu'): @@ -162,18 +167,22 @@ def verify(vxlan): print('WARNING: RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU') if vxlan['group']: - if not vxlan['link']: + if not vxlan['source_interface']: raise ConfigError('Multicast VXLAN requires an underlaying interface ') - if not vxlan['link'] in interfaces(): + + if not vxlan['source_interface'] in interfaces(): raise ConfigError('VXLAN source interface does not exist') + if not (vxlan['group'] or vxlan['remote'] or vxlan['source_address']): + raise ConfigError('Group, remote or source-address must be configured') + if not vxlan['vni']: raise ConfigError('Must configure VNI for VXLAN') - if vxlan['link']: + if vxlan['source_interface']: # VXLAN adds a 50 byte overhead - we need to check the underlaying MTU # if our configured MTU is at least 50 bytes less - underlay_mtu = int(Interface(vxlan['link']).get_mtu()) + underlay_mtu = int(Interface(vxlan['source_interface']).get_mtu()) if underlay_mtu < (vxlan['mtu'] + 50): raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \ 'MTU is to small ({})'.format(underlay_mtu)) @@ -202,7 +211,8 @@ def apply(vxlan): # Assign VXLAN instance configuration parameters to config dict conf['vni'] = vxlan['vni'] conf['group'] = vxlan['group'] - conf['dev'] = vxlan['link'] + conf['src_address'] = vxlan['source_address'] + conf['src_interface'] = vxlan['source_interface'] conf['remote'] = vxlan['remote'] conf['port'] = vxlan['remote_port'] diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index 54121a6c1..8bf81c747 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -14,173 +14,181 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import sys import os import re +from sys import exit from copy import deepcopy from netifaces import interfaces -from vyos import ConfigError from vyos.config import Config from vyos.configdict import list_diff -from vyos.util import run, is_bridge_member from vyos.ifconfig import WireGuardIf +from vyos.util import chown, is_bridge_member, chmod_750 +from vyos.util import call +from vyos import ConfigError kdir = r'/config/auth/wireguard' +default_config_data = { + 'intfc': '', + 'address': [], + 'address_remove': [], + 'description': '', + 'lport': None, + 'deleted': False, + 'disable': False, + 'fwmark': 0x00, + 'mtu': 1420, + 'peer': [], + 'peer_remove': [], # stores public keys of peers to remove + 'pk': f'{kdir}/default/private.key', + 'vrf': '' +} + def _check_kmod(): - if not os.path.exists('/sys/module/wireguard'): - if run('modprobe wireguard') != 0: - raise ConfigError("modprobe wireguard failed") + modules = ['wireguard'] + for module in modules: + if not os.path.exists(f'/sys/module/{module}'): + if call(f'modprobe {module}') != 0: + raise ConfigError(f'Loading Kernel module {module} failed') def _migrate_default_keys(): if os.path.exists(f'{kdir}/private.key') and not os.path.exists(f'{kdir}/default/private.key'): - old_umask = os.umask(0o027) location = f'{kdir}/default' - run(f'sudo mkdir -p {location}') - run(f'sudo chgrp vyattacfg {location}') - run(f'sudo chmod 750 {location}') + if not os.path.exists(location): + os.makedirs(location) + + chown(location, 'root', 'vyattacfg') + chmod_750(location) os.rename(f'{kdir}/private.key', f'{location}/private.key') os.rename(f'{kdir}/public.key', f'{location}/public.key') - os.umask(old_umask) def get_config(): - c = Config() - if not c.exists(['interfaces', 'wireguard']): - return None + conf = Config() + base = ['interfaces', 'wireguard'] # determine tagNode instance if 'VYOS_TAGNODE_VALUE' not in os.environ: raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - dflt_cnf = { - 'intfc': '', - 'addr': [], - 'addr_remove': [], - 'descr': '', - 'lport': None, - 'delete': False, - 'state': 'up', - 'fwmark': 0x00, - 'mtu': 1420, - 'peer': {}, - 'peer_remove': [], - 'pk': '{}/default/private.key'.format(kdir) - } - - ifname = str(os.environ['VYOS_TAGNODE_VALUE']) - wg = deepcopy(dflt_cnf) - wg['intfc'] = ifname - wg['descr'] = ifname - - c.set_level(['interfaces', 'wireguard']) - - # interface removal state - if not c.exists(ifname) and c.exists_effective(ifname): - wg['delete'] = True - - if not wg['delete']: - c.set_level(['interfaces', 'wireguard', ifname]) - if c.exists(['address']): - wg['addr'] = c.return_values(['address']) - - # determine addresses which need to be removed - eff_addr = c.return_effective_values(['address']) - wg['addr_remove'] = list_diff(eff_addr, wg['addr']) - - # ifalias description - if c.exists(['description']): - wg['descr'] = c.return_value(['description']) - - # link state - if c.exists(['disable']): - wg['state'] = 'down' - - # local port to listen on - if c.exists(['port']): - wg['lport'] = c.return_value(['port']) - - # fwmark value - if c.exists(['fwmark']): - wg['fwmark'] = c.return_value(['fwmark']) - - # mtu - if c.exists('mtu'): - wg['mtu'] = c.return_value('mtu') - - # private key - if c.exists(['private-key']): - wg['pk'] = "{0}/{1}/private.key".format( - kdir, c.return_value(['private-key'])) - - # peer removal, wg identifies peers by its pubkey - peer_eff = c.list_effective_nodes(['peer']) - peer_rem = list_diff(peer_eff, c.list_nodes(['peer'])) - for p in peer_rem: - wg['peer_remove'].append( - c.return_effective_value(['peer', p, 'pubkey'])) - - # peer settings - if c.exists(['peer']): - for p in c.list_nodes(['peer']): - if not c.exists(['peer', p, 'disable']): - wg['peer'].update( - { - p: { - 'allowed-ips': [], - 'address': '', - 'port': '', - 'pubkey': '' - } - } - ) - # peer allowed-ips - if c.exists(['peer', p, 'allowed-ips']): - wg['peer'][p]['allowed-ips'] = c.return_values( - ['peer', p, 'allowed-ips']) - # peer address - if c.exists(['peer', p, 'address']): - wg['peer'][p]['address'] = c.return_value( - ['peer', p, 'address']) - # peer port - if c.exists(['peer', p, 'port']): - wg['peer'][p]['port'] = c.return_value( - ['peer', p, 'port']) - # persistent-keepalive - if c.exists(['peer', p, 'persistent-keepalive']): - wg['peer'][p]['persistent-keepalive'] = c.return_value( - ['peer', p, 'persistent-keepalive']) - # preshared-key - if c.exists(['peer', p, 'preshared-key']): - wg['peer'][p]['psk'] = c.return_value( - ['peer', p, 'preshared-key']) - # peer pubkeys - key_eff = c.return_effective_value(['peer', p, 'pubkey']) - key_cfg = c.return_value(['peer', p, 'pubkey']) - wg['peer'][p]['pubkey'] = key_cfg - - # on a pubkey change we need to remove the pubkey first - # peers are identified by pubkey, so key update means - # peer removal and re-add - if key_eff != key_cfg and key_eff != None: - wg['peer_remove'].append(key_cfg) - - # if a peer is disabled, we have to exec a remove for it's pubkey - else: - peer_key = c.return_value(['peer', p, 'pubkey']) - wg['peer_remove'].append(peer_key) + wg = deepcopy(default_config_data) + wg['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + + # Check if interface has been removed + if not conf.exists(base + [wg['intf']]): + wg['deleted'] = True + return wg + + conf.set_level(base + [wg['intf']]) + + # retrieve configured interface addresses + if conf.exists(['address']): + wg['address'] = conf.return_values(['address']) + + # get interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed + eff_addr = conf.return_effective_values(['address']) + wg['address_remove'] = list_diff(eff_addr, wg['address']) + + # retrieve interface description + if conf.exists(['description']): + wg['description'] = conf.return_value(['description']) + + # disable interface + if conf.exists(['disable']): + wg['disable'] = True + + # local port to listen on + if conf.exists(['port']): + wg['lport'] = conf.return_value(['port']) + + # fwmark value + if conf.exists(['fwmark']): + wg['fwmark'] = int(conf.return_value(['fwmark'])) + + # Maximum Transmission Unit (MTU) + if conf.exists('mtu'): + wg['mtu'] = int(conf.return_value(['mtu'])) + + # retrieve VRF instance + if conf.exists('vrf'): + wg['vrf'] = conf.return_value('vrf') + + # private key + if conf.exists(['private-key']): + wg['pk'] = "{0}/{1}/private.key".format( + kdir, conf.return_value(['private-key'])) + + # peer removal, wg identifies peers by its pubkey + peer_eff = conf.list_effective_nodes(['peer']) + peer_rem = list_diff(peer_eff, conf.list_nodes(['peer'])) + for peer in peer_rem: + wg['peer_remove'].append( + conf.return_effective_value(['peer', peer, 'pubkey'])) + + # peer settings + if conf.exists(['peer']): + for p in conf.list_nodes(['peer']): + # set new config level for this peer + conf.set_level(base + [wg['intf'], 'peer', p]) + peer = { + 'allowed-ips': [], + 'address': '', + 'name': p, + 'persistent_keepalive': '', + 'port': '', + 'psk': '', + 'pubkey': '' + } + + # peer allowed-ips + if conf.exists(['allowed-ips']): + peer['allowed-ips'] = conf.return_values(['allowed-ips']) + + # peer address + if conf.exists(['address']): + peer['address'] = conf.return_value(['address']) + + # peer port + if conf.exists(['port']): + peer['port'] = conf.return_value(['port']) + + # persistent-keepalive + if conf.exists(['persistent-keepalive']): + peer['persistent_keepalive'] = conf.return_value(['persistent-keepalive']) + + # preshared-key + if conf.exists(['preshared-key']): + peer['psk'] = conf.return_value(['preshared-key']) + + # peer pubkeys + if conf.exists(['pubkey']): + key_eff = conf.return_effective_value(['pubkey']) + key_cfg = conf.return_value(['pubkey']) + peer['pubkey'] = key_cfg + + # on a pubkey change we need to remove the pubkey first + # peers are identified by pubkey, so key update means + # peer removal and re-add + if key_eff != key_cfg and key_eff != None: + wg['peer_remove'].append(key_cfg) + + # if a peer is disabled, we have to exec a remove for it's pubkey + if conf.exists(['disable']): + wg['peer_remove'].append(peer['pubkey']) + else: + wg['peer'].append(peer) + return wg -def verify(c): - if not c: - return None +def verify(wg): + interface = wg['intf'] - if c['delete']: - interface = c['intfc'] + if wg['deleted']: is_member, bridge = is_bridge_member(interface) if is_member: # can not use a f'' formatted-string here as bridge would not get @@ -189,98 +197,100 @@ def verify(c): 'is a member of bridge "{1}"!'.format(interface, bridge)) return None - if not os.path.exists(c['pk']): - raise ConfigError( - "No keys found, generate them by executing: \'run generate wireguard [keypair|named-keypairs]\'") - - if not c['delete']: - if not c['addr']: - raise ConfigError("ERROR: IP address required") - if not c['peer']: - raise ConfigError("ERROR: peer required") - for p in c['peer']: - if not c['peer'][p]['allowed-ips']: - raise ConfigError("ERROR: allowed-ips required for peer " + p) - if not c['peer'][p]['pubkey']: - raise ConfigError("peer pubkey required for peer " + p) - - -def apply(c): - # no wg configs left, remove all interface from system - # maybe move it into ifconfig.py - if not c: - net_devs = os.listdir('/sys/class/net/') - for dev in net_devs: - if os.path.isdir('/sys/class/net/' + dev): - buf = open('/sys/class/net/' + dev + '/uevent', 'r').read() - if re.search("DEVTYPE=wireguard", buf, re.I | re.M): - wg_intf = re.sub("INTERFACE=", "", re.search( - "INTERFACE=.*", buf, re.I | re.M).group(0)) - # XXX: we are ignoring any errors here - run(f'ip l d dev {wg_intf} >/dev/null') - return None + vrf_name = wg['vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF "{vrf_name}" does not exist') + + if not os.path.exists(wg['pk']): + raise ConfigError('No keys found, generate them by executing:\n' \ + '"run generate wireguard [keypair|named-keypairs]"') + if not wg['address']: + raise ConfigError(f'IP address required for interface "{interface}"!') + + if not wg['peer']: + raise ConfigError(f'Peer required for interface "{interface}"!') + + # run checks on individual configured WireGuard peer + for peer in wg['peer']: + peer_name = peer['name'] + if not peer['allowed-ips']: + raise ConfigError(f'Peer allowed-ips required for peer "{peer_name}"!') + + if not peer['pubkey']: + raise ConfigError(f'Peer public-key required for peer "{peer_name}"!') + + +def apply(wg): # init wg class - intfc = WireGuardIf(c['intfc']) + w = WireGuardIf(wg['intf']) # single interface removal - if c['delete']: - intfc.remove() + if wg['deleted']: + w.remove() return None - # remove IP addresses - for ip in c['addr_remove']: - intfc.del_addr(ip) + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second + for addr in wg['address_remove']: + w.del_addr(addr) + for addr in wg['address']: + w.add_addr(addr) - # add IP addresses - for ip in c['addr']: - intfc.add_addr(ip) + # Maximum Transmission Unit (MTU) + w.set_mtu(wg['mtu']) - # interface mtu - intfc.set_mtu(int(c['mtu'])) + # update interface description used e.g. within SNMP + w.set_alias(wg['description']) - # ifalias for snmp from description - intfc.set_alias(str(c['descr'])) + # assign/remove VRF + w.set_vrf(wg['vrf']) # remove peers - if c['peer_remove']: - for pkey in c['peer_remove']: - intfc.remove_peer(pkey) + for pub_key in wg['peer_remove']: + w.remove_peer(pub_key) # peer pubkey # setting up the wg interface - intfc.config['private-key'] = c['pk'] - for p in c['peer']: + w.config['private-key'] = c['pk'] + + for peer in wg['peer']: # peer pubkey - intfc.config['pubkey'] = str(c['peer'][p]['pubkey']) + w.config['pubkey'] = peer['pubkey'] # peer allowed-ips - intfc.config['allowed-ips'] = c['peer'][p]['allowed-ips'] + w.config['allowed-ips'] = peer['allowed-ips'] # local listen port - if c['lport']: - intfc.config['port'] = c['lport'] + if wg['lport']: + w.config['port'] = wg['lport'] # fwmark if c['fwmark']: - intfc.config['fwmark'] = c['fwmark'] + w.config['fwmark'] = wg['fwmark'] + # endpoint - if c['peer'][p]['address'] and c['peer'][p]['port']: - intfc.config['endpoint'] = "{}:{}".format(c['peer'][p]['address'], c['peer'][p]['port']) + if peer['address'] and peer['port']: + w.config['endpoint'] = '{}:{}'.format( + peer['address'], peer['port']) # persistent-keepalive - if 'persistent-keepalive' in c['peer'][p]: - intfc.config['keepalive'] = c['peer'][p]['persistent-keepalive'] + if peer['persistent_keepalive']: + w.config['keepalive'] = peer['persistent_keepalive'] # maybe move it into ifconfig.py # preshared-key - needs to be read from a file - if 'psk' in c['peer'][p]: + if peer['psk']: psk_file = '/config/auth/wireguard/psk' - old_umask = os.umask(0o077) - open(psk_file, 'w').write(str(c['peer'][p]['psk'])) - os.umask(old_umask) - intfc.config['psk'] = psk_file - intfc.update() + with open(psk_file, 'w') as f: + f.write(peer['psk']) + w.config['psk'] = psk_file + + w.update() - # interface state - intfc.set_admin_state(c['state']) + # Enable/Disable interface + if wg['disable']: + w.set_admin_state('down') + else: + w.set_admin_state('up') return None @@ -293,4 +303,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index 709085b0f..498c24df0 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -19,21 +19,18 @@ from sys import exit from re import findall from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from netifaces import interfaces from netaddr import EUI, mac_unix_expanded from vyos.config import Config from vyos.configdict import list_diff, vlan_to_dict -from vyos.defaults import directories as vyos_data_dir from vyos.ifconfig import WiFiIf from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config -from vyos.util import process_running, chmod_x, chown_file, run, is_bridge_member +from vyos.util import chown, is_bridge_member, call from vyos import ConfigError +from vyos.template import render -user = 'root' -group = 'vyattacfg' default_config_data = { 'address': [], @@ -115,43 +112,16 @@ default_config_data = { } def get_conf_file(conf_type, intf): - cfg_dir = '/var/run/' + conf_type + cfg_dir = '/run/' + conf_type # create directory on demand if not os.path.exists(cfg_dir): - os.mkdir(cfg_dir) - chmod_x(cfg_dir) - chown_file(cfg_dir, user, group) + os.makedirs(cfg_dir, 0o755) + chown(cfg_dir, 'root', 'vyattacfg') - cfg_file = cfg_dir + r'/{}.cfg'.format(intf) + cfg_file = cfg_dir + r'/{}.conf'.format(intf) return cfg_file -def get_pid(conf_type, intf): - cfg_dir = '/var/run/' + conf_type - - # create directory on demand - if not os.path.exists(cfg_dir): - os.mkdir(cfg_dir) - chmod_x(cfg_dir) - chown_file(cfg_dir, user, group) - - cfg_file = cfg_dir + r'/{}.pid'.format(intf) - return cfg_file - - -def get_wpa_suppl_config_name(intf): - cfg_dir = '/var/run/wpa_supplicant' - - # create directory on demand - if not os.path.exists(cfg_dir): - os.mkdir(cfg_dir) - chmod_x(cfg_dir) - chown_file(cfg_dir, user, group) - - cfg_file = cfg_dir + r'/{}.cfg'.format(intf) - return cfg_file - - def get_config(): wifi = deepcopy(default_config_data) conf = Config() @@ -570,6 +540,9 @@ def verify(wifi): if not wifi['phy']: raise ConfigError('You must specify physical-device') + if not wifi['mode']: + raise ConfigError('You must specify a WiFi mode') + if wifi['op_mode'] == 'ap': c = Config() if not c.exists('system wifi-regulatory-domain'): @@ -627,38 +600,20 @@ def verify(wifi): return None def generate(wifi): - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir["data"], "templates", "wifi") - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) + interface = wifi['intf'] # always stop hostapd service first before reconfiguring it - pidfile = get_pid('hostapd', wifi['intf']) - if process_running(pidfile): - command = 'start-stop-daemon' - command += ' --stop ' - command += ' --quiet' - command += ' --oknodo' - command += ' --pidfile ' + pidfile - run(command) - + call(f'systemctl stop hostapd@{interface}.service') # always stop wpa_supplicant service first before reconfiguring it - pidfile = get_pid('wpa_supplicant', wifi['intf']) - if process_running(pidfile): - command = 'start-stop-daemon' - command += ' --stop ' - command += ' --quiet' - command += ' --oknodo' - command += ' --pidfile ' + pidfile - run(command) + call(f'systemctl stop wpa_supplicant@{interface}.service') # Delete config files if interface is removed if wifi['deleted']: - if os.path.isfile(get_conf_file('hostapd', wifi['intf'])): - os.unlink(get_conf_file('hostapd', wifi['intf'])) + if os.path.isfile(get_conf_file('hostapd', interface)): + os.unlink(get_conf_file('hostapd', interface)) - if os.path.isfile(get_conf_file('wpa_supplicant', wifi['intf'])): - os.unlink(get_conf_file('wpa_supplicant', wifi['intf'])) + if os.path.isfile(get_conf_file('wpa_supplicant', interface)): + os.unlink(get_conf_file('wpa_supplicant', interface)) return None @@ -676,7 +631,7 @@ def generate(wifi): tmp |= 0x020000000000 # we now need to add an offset to our MAC address indicating this # subinterfaces index - tmp += int(findall(r'\d+', wifi['intf'])[0]) + tmp += int(findall(r'\d+', interface)[0]) # convert integer to "real" MAC address representation mac = EUI(hex(tmp).split('x')[-1]) @@ -686,22 +641,19 @@ def generate(wifi): # render appropriate new config files depending on access-point or station mode if wifi['op_mode'] == 'ap': - tmpl = env.get_template('hostapd.conf.tmpl') - config_text = tmpl.render(wifi) - with open(get_conf_file('hostapd', wifi['intf']), 'w') as f: - f.write(config_text) + conf = get_conf_file('hostapd', interface) + render(conf, 'wifi/hostapd.conf.tmpl', wifi) elif wifi['op_mode'] == 'station': - tmpl = env.get_template('wpa_supplicant.conf.tmpl') - config_text = tmpl.render(wifi) - with open(get_conf_file('wpa_supplicant', wifi['intf']), 'w') as f: - f.write(config_text) + conf = get_conf_file('wpa_supplicant', interface) + render(conf, 'wifi/wpa_supplicant.conf.tmpl', wifi) return None def apply(wifi): + interface = wifi['intf'] if wifi['deleted']: - w = WiFiIf(wifi['intf']) + w = WiFiIf(interface) # delete interface w.remove() else: @@ -714,7 +666,7 @@ def apply(wifi): conf['phy'] = wifi['phy'] # Finally create the new interface - w = WiFiIf(wifi['intf'], **conf) + w = WiFiIf(interface, **conf) # assign/remove VRF w.set_vrf(wifi['vrf']) @@ -722,32 +674,20 @@ def apply(wifi): # update interface description used e.g. within SNMP w.set_alias(wifi['description']) - # get DHCP config dictionary and update values - opt = w.get_dhcp_options() - if wifi['dhcp_client_id']: - opt['client_id'] = wifi['dhcp_client_id'] + w.dhcp.v4.options['client_id'] = wifi['dhcp_client_id'] if wifi['dhcp_hostname']: - opt['hostname'] = wifi['dhcp_hostname'] + w.dhcp.v4.options['hostname'] = wifi['dhcp_hostname'] if wifi['dhcp_vendor_class_id']: - opt['vendor_class_id'] = wifi['dhcp_vendor_class_id'] - - # store DHCP config dictionary - used later on when addresses are aquired - w.set_dhcp_options(opt) - - # get DHCPv6 config dictionary and update values - opt = w.get_dhcpv6_options() + w.dhcp.v4.options['vendor_class_id'] = wifi['dhcp_vendor_class_id'] if wifi['dhcpv6_prm_only']: - opt['dhcpv6_prm_only'] = True + w.dhcp.v6.options['dhcpv6_prm_only'] = True if wifi['dhcpv6_temporary']: - opt['dhcpv6_temporary'] = True - - # store DHCPv6 config dictionary - used later on when addresses are aquired - w.set_dhcpv6_options(opt) + w.dhcp.v6.options['dhcpv6_temporary'] = True # ignore link state changes w.set_link_detect(wifi['disable_link_detect']) @@ -786,7 +726,7 @@ def apply(wifi): # remove no longer required VLAN interfaces (vif) for vif in wifi['vif_remove']: - e.del_vlan(vif) + w.del_vlan(vif) # create VLAN interfaces (vif) for vif in wifi['vif']: @@ -796,11 +736,11 @@ def apply(wifi): try: # on system bootup the above condition is true but the interface # does not exists, which throws an exception, but that's legal - e.del_vlan(vif['id']) + w.del_vlan(vif['id']) except: pass - vlan = e.add_vlan(vif['id']) + vlan = w.add_vlan(vif['id']) apply_vlan_config(vlan, vif) # Enable/Disable interface - interface is always placed in @@ -811,38 +751,10 @@ def apply(wifi): # Physical interface is now configured. Proceed by starting hostapd or # wpa_supplicant daemon. When type is monitor we can just skip this. if wifi['op_mode'] == 'ap': - command = 'start-stop-daemon' - command += ' --start ' - command += ' --quiet' - command += ' --oknodo' - command += ' --pidfile ' + get_pid('hostapd', wifi['intf']) - command += ' --exec /usr/sbin/hostapd' - # now pass arguments to hostapd binary - command += ' -- ' - command += ' -B' - command += ' -P ' + get_pid('hostapd', wifi['intf']) - command += ' ' + get_conf_file('hostapd', wifi['intf']) - - # execute assembled command - run(command) + call(f'systemctl start hostapd@{interface}.service') elif wifi['op_mode'] == 'station': - command = 'start-stop-daemon' - command += ' --start ' - command += ' --quiet' - command += ' --oknodo' - command += ' --pidfile ' + get_pid('hostapd', wifi['intf']) - command += ' --exec /sbin/wpa_supplicant' - # now pass arguments to hostapd binary - command += ' -- ' - command += ' -s -B -D nl80211' - command += ' -P ' + get_pid('wpa_supplicant', wifi['intf']) - command += ' -i ' + wifi['intf'] - command += ' -c ' + \ - get_conf_file('wpa_supplicant', wifi['intf']) - - # execute assembled command - run(command) + call(f'systemctl start wpa_supplicant@{interface}.service') return None diff --git a/src/conf_mode/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py index 49445aaa4..da1855cd9 100755 --- a/src/conf_mode/interfaces-wirelessmodem.py +++ b/src/conf_mode/interfaces-wirelessmodem.py @@ -18,13 +18,17 @@ import os from sys import exit from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from netifaces import interfaces from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir -from vyos.util import chown_file, chmod_x, cmd, run, is_bridge_member +from vyos.util import chown +from vyos.util import chmod_755 +from vyos.util import is_bridge_member +from vyos.util import cmd +from vyos.util import call from vyos import ConfigError +from vyos.template import render + default_config_data = { 'address': [], @@ -48,7 +52,7 @@ def check_kmod(): modules = ['option', 'usb_wwan', 'usbserial'] for module in modules: if not os.path.exists(f'/sys/module/{module}'): - if run(f'modprobe {module}') != 0: + if call(f'modprobe {module}') != 0: raise ConfigError(f'Loading Kernel module {module} failed') def get_config(): @@ -139,11 +143,6 @@ def verify(wwan): return None def generate(wwan): - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'wwan') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - # set up configuration file path variables where our templates will be # rendered into intf = wwan['intf'] @@ -173,39 +172,20 @@ def generate(wwan): else: # Create PPP configuration files - tmpl = env.get_template('peer.tmpl') - config_text = tmpl.render(wwan) - with open(config_wwan, 'w') as f: - f.write(config_text) - + render(config_wwan, 'wwan/peer.tmpl', wwan) # Create PPP chat script - tmpl = env.get_template('chat.tmpl') - config_text = tmpl.render(wwan) - with open(config_wwan_chat, 'w') as f: - f.write(config_text) - + render(config_wwan_chat, 'wwan/chat.tmpl', wwan) # Create script for ip-pre-up.d - tmpl = env.get_template('ip-pre-up.script.tmpl') - config_text = tmpl.render(wwan) - with open(script_wwan_pre_up, 'w') as f: - f.write(config_text) - + render(script_wwan_pre_up, 'wwan/ip-pre-up.script.tmpl', wwan) # Create script for ip-up.d - tmpl = env.get_template('ip-up.script.tmpl') - config_text = tmpl.render(wwan) - with open(script_wwan_ip_up, 'w') as f: - f.write(config_text) - + render(script_wwan_ip_up, 'wwan/ip-up.script.tmpl', wwan) # Create script for ip-down.d - tmpl = env.get_template('ip-down.script.tmpl') - config_text = tmpl.render(wwan) - with open(script_wwan_ip_down, 'w') as f: - f.write(config_text) + render(script_wwan_ip_down, 'wwan/ip-down.script.tmpl', wwan) # make generated script file executable - chmod_x(script_wwan_pre_up) - chmod_x(script_wwan_ip_up) - chmod_x(script_wwan_ip_down) + chmod_755(script_wwan_pre_up) + chmod_755(script_wwan_ip_up) + chmod_755(script_wwan_ip_down) return None @@ -219,7 +199,7 @@ def apply(wwan): intf = wwan['intf'] cmd(f'systemctl start ppp@{intf}.service') # make logfile owned by root / vyattacfg - chown_file(wwan['logfile'], 'root', 'vyattacfg') + chown(wwan['logfile'], 'root', 'vyattacfg') return None diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py index c2f5c8e07..4fffa11ee 100755 --- a/src/conf_mode/ipsec-settings.py +++ b/src/conf_mode/ipsec-settings.py @@ -18,13 +18,13 @@ import re import os from time import sleep -from jinja2 import FileSystemLoader, Environment from sys import exit from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError -from vyos.util import run +from vyos.util import call +from vyos.template import render + ra_conn_name = "remote-access" charon_conf_file = "/etc/strongswan.d/charon.conf" @@ -99,7 +99,7 @@ def get_config(): ### Remove config from file by delimiter def remove_confs(delim_begin, delim_end, conf_file): - run("sed -i '/"+delim_begin+"/,/"+delim_end+"/d' "+conf_file) + call("sed -i '/"+delim_begin+"/,/"+delim_end+"/d' "+conf_file) ### Checking certificate storage and notice if certificate not in /config directory @@ -112,7 +112,7 @@ def check_cert_file_store(cert_name, file_path, dts_path): else: ### Cpy file to /etc/ipsec.d/certs/ /etc/ipsec.d/cacerts/ # todo make check - ret = run('cp -f '+file_path+' '+dts_path) + ret = call('cp -f '+file_path+' '+dts_path) if ret: raise ConfigError("L2TP VPN configuration error: Cannot copy "+file_path) @@ -147,43 +147,26 @@ def verify(data): raise ConfigError("L2TP VPN configuration error: \"vpn ipsec ipsec-interfaces\" must be specified.") def generate(data): - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'ipsec') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader, trim_blocks=True) - - tmpl = env.get_template('charon.tmpl') - config_text = tmpl.render(data) - with open(charon_conf_file, 'w') as f: - f.write(config_text) + render(charon_conf_file, 'ipsec/charon.tmpl', data, trim_blocks=True) if data["ipsec_l2tp"]: remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_flie) - tmpl = env.get_template('ipsec.secrets.tmpl') - l2pt_ipsec_secrets_txt = tmpl.render(c) old_umask = os.umask(0o077) - with open(ipsec_secrets_flie,'w') as f: - f.write(l2pt_ipsec_secrets_txt) + render(ipsec_secrets_flie, 'ipsec/ipsec.secrets.tmpl', c, trim_blocks=True) os.umask(old_umask) - tmpl = env.get_template('remote-access.tmpl') - ipsec_ra_conn_txt = tmpl.render(c) old_umask = os.umask(0o077) # Create tunnels directory if does not exist if not os.path.exists(ipsec_ra_conn_dir): os.makedirs(ipsec_ra_conn_dir) - with open(ipsec_ra_conn_file,'w') as f: - f.write(ipsec_ra_conn_txt) + render(ipsec_ra_conn_file, 'ipsec/remote-access.tmpl', c, trim_blocks=True) os.umask(old_umask) - - tmpl = env.get_template('ipsec.conf.tmpl') - l2pt_ipsec_conf_txt = tmpl.render(c) old_umask = os.umask(0o077) - with open(ipsec_conf_flie,'a') as f: - f.write(l2pt_ipsec_conf_txt) + render(ipsec_conf_flie, 'ipsec/ipsec.conf.tmpl', c, trim_blocks=True) os.umask(old_umask) else: @@ -193,12 +176,12 @@ def generate(data): remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_flie) def restart_ipsec(): - run('ipsec restart >&/dev/null') + call('ipsec restart >&/dev/null') # counter for apply swanctl config counter = 10 while counter <= 10: if os.path.exists(charon_pidfile): - run('swanctl -q >&/dev/null') + call('swanctl -q >&/dev/null') break counter -=1 sleep(1) diff --git a/src/conf_mode/le_cert.py b/src/conf_mode/le_cert.py index a4dbecbaa..2db31d3fc 100755 --- a/src/conf_mode/le_cert.py +++ b/src/conf_mode/le_cert.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2019-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,8 +13,6 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# import sys import os @@ -22,8 +20,8 @@ import os import vyos.defaults from vyos.config import Config from vyos import ConfigError -from vyos.util import cmd, run - +from vyos.util import cmd +from vyos.util import call vyos_conf_scripts_dir = vyos.defaults.directories['conf_mode'] @@ -85,17 +83,17 @@ def generate(cert): # certbot will attempt to reload nginx, even with 'certonly'; # start nginx if not active - ret = run('systemctl is-active --quiet nginx.ervice') + ret = call('systemctl is-active --quiet nginx.service') if ret: - run('sudo systemctl start nginx.service') + call('systemctl start nginx.service') request_certbot(cert) def apply(cert): if cert is not None: - run('sudo systemctl restart certbot.timer') + call('systemctl restart certbot.timer') else: - run('sudo systemctl stop certbot.timer') + call('systemctl stop certbot.timer') return None for dep in dependencies: diff --git a/src/conf_mode/lldp.py b/src/conf_mode/lldp.py index c090bba83..d128c1fe6 100755 --- a/src/conf_mode/lldp.py +++ b/src/conf_mode/lldp.py @@ -18,15 +18,14 @@ import os import re from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from sys import exit from vyos.config import Config from vyos.validate import is_addr_assigned,is_loopback_addr -from vyos.defaults import directories as vyos_data_dir from vyos.version import get_version_data from vyos import ConfigError -from vyos.util import run +from vyos.util import call +from vyos.template import render config_file = "/etc/default/lldpd" @@ -210,11 +209,6 @@ def generate(lldp): if lldp is None: return - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'lldp') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - # generate listen on interfaces for intf in lldp['interface_list']: tmp = '' @@ -226,25 +220,18 @@ def generate(lldp): lldp['options']['listen_on'].append(tmp) # generate /etc/default/lldpd - tmpl = env.get_template('lldpd.tmpl') - config_text = tmpl.render(lldp) - with open(config_file, 'w') as f: - f.write(config_text) - + render(config_file, 'lldp/lldpd.tmpl', lldp) # generate /etc/lldpd.d/01-vyos.conf - tmpl = env.get_template('vyos.conf.tmpl') - config_text = tmpl.render(lldp) - with open(vyos_config_file, 'w') as f: - f.write(config_text) + render(vyos_config_file, 'lldp/vyos.conf.tmpl', lldp) def apply(lldp): if lldp: # start/restart lldp service - run('sudo systemctl restart lldpd.service') + call('sudo systemctl restart lldpd.service') else: # LLDP service has been terminated - run('sudo systemctl stop lldpd.service') + call('sudo systemctl stop lldpd.service') os.unlink(config_file) os.unlink(vyos_config_file) diff --git a/src/conf_mode/mdns_repeater.py b/src/conf_mode/mdns_repeater.py index 2bccd9153..a652553f7 100755 --- a/src/conf_mode/mdns_repeater.py +++ b/src/conf_mode/mdns_repeater.py @@ -18,14 +18,12 @@ import os from sys import exit from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from netifaces import ifaddresses, AF_INET from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError -from vyos.util import run - +from vyos.util import call +from vyos.template import render config_file = r'/etc/default/mdns-repeater' @@ -82,25 +80,16 @@ def generate(mdns): print('Warning: mDNS repeater will be deactivated because it is disabled') return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'mdns-repeater') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - tmpl = env.get_template('mdns-repeater.tmpl') - config_text = tmpl.render(mdns) - with open(config_file, 'w') as f: - f.write(config_text) - + render(config_file, 'mdns-repeater/mdns-repeater.tmpl', mdns) return None def apply(mdns): if (mdns is None) or mdns['disabled']: - run('sudo systemctl stop mdns-repeater') + call('systemctl stop mdns-repeater.service') if os.path.exists(config_file): os.unlink(config_file) else: - run('sudo systemctl restart mdns-repeater') + call('systemctl restart mdns-repeater.service') return None diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py index 998022a8c..6d32f7fd6 100755 --- a/src/conf_mode/ntp.py +++ b/src/conf_mode/ntp.py @@ -18,14 +18,12 @@ import os from copy import deepcopy from ipaddress import ip_network -from jinja2 import FileSystemLoader, Environment from sys import exit from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir +from vyos.util import call +from vyos.template import render from vyos import ConfigError -from vyos.util import run - config_file = r'/etc/ntp.conf' @@ -100,24 +98,15 @@ def generate(ntp): if ntp is None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'ntp') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - tmpl = env.get_template('ntp.conf.tmpl') - config_text = tmpl.render(ntp) - with open(config_file, 'w') as f: - f.write(config_text) - + render(config_file, 'ntp/ntp.conf.tmpl', ntp) return None def apply(ntp): if ntp is not None: - run('sudo systemctl restart ntp.service') + call('sudo systemctl restart ntp.service') else: # NTP support is removed in the commit - run('sudo systemctl stop ntp.service') + call('sudo systemctl stop ntp.service') os.unlink(config_file) return None diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py index a62d2158e..ed8c3637b 100755 --- a/src/conf_mode/protocols_bfd.py +++ b/src/conf_mode/protocols_bfd.py @@ -18,13 +18,12 @@ import os from sys import exit from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos.validate import is_ipv6_link_local, is_ipv6 from vyos import ConfigError -from vyos.util import run +from vyos.util import call +from vyos.template import render config_file = r'/tmp/bfd.frr' @@ -191,23 +190,14 @@ def generate(bfd): if bfd is None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'frr-bfd') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - tmpl = env.get_template('bfd.frr.tmpl') - config_text = tmpl.render(bfd) - with open(config_file, 'w') as f: - f.write(config_text) - + render(config_file, 'frr-bfd/bfd.frr.tmpl', bfd) return None def apply(bfd): if bfd is None: return None - run("vtysh -d bfdd -f " + config_file) + call("vtysh -d bfdd -f " + config_file) if os.path.exists(config_file): os.remove(config_file) diff --git a/src/conf_mode/protocols_igmp.py b/src/conf_mode/protocols_igmp.py index 6e819a15a..9b338c5b9 100755 --- a/src/conf_mode/protocols_igmp.py +++ b/src/conf_mode/protocols_igmp.py @@ -17,13 +17,12 @@ import os from ipaddress import IPv4Address -from jinja2 import FileSystemLoader, Environment from sys import exit from vyos import ConfigError from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir -from vyos.util import run +from vyos.util import call +from vyos.template import render config_file = r'/tmp/igmp.frr' @@ -88,16 +87,7 @@ def generate(igmp): if igmp is None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'igmp') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - tmpl = env.get_template('igmp.frr.tmpl') - config_text = tmpl.render(igmp) - with open(config_file, 'w') as f: - f.write(config_text) - + render(config_file, 'igmp/igmp.frr.tmpl', igmp) return None def apply(igmp): @@ -105,7 +95,7 @@ def apply(igmp): return None if os.path.exists(config_file): - run("sudo vtysh -d pimd -f " + config_file) + call("sudo vtysh -d pimd -f " + config_file) os.remove(config_file) return None diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py index 6e5d08397..0a241277d 100755 --- a/src/conf_mode/protocols_mpls.py +++ b/src/conf_mode/protocols_mpls.py @@ -16,18 +16,16 @@ import os -from jinja2 import FileSystemLoader, Environment - from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError -from vyos.util import run +from vyos.util import call +from vyos.template import render config_file = r'/tmp/ldpd.frr' def sysctl(name, value): - run('sysctl -wq {}={}'.format(name, value)) + call('sysctl -wq {}={}'.format(name, value)) def get_config(): conf = Config() @@ -129,16 +127,7 @@ def generate(mpls): if mpls is None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'mpls') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - tmpl = env.get_template('ldpd.frr.tmpl') - config_text = tmpl.render(mpls) - with open(config_file, 'w') as f: - f.write(config_text) - + render(config_file, 'mpls/ldpd.frr.tmpl', mpls) return None def apply(mpls): @@ -162,7 +151,7 @@ def apply(mpls): operate_mpls_on_intfc(diactive_ifaces, 0) if os.path.exists(config_file): - run("sudo vtysh -d ldpd -f " + config_file) + call("sudo vtysh -d ldpd -f " + config_file) os.remove(config_file) return None diff --git a/src/conf_mode/protocols_pim.py b/src/conf_mode/protocols_pim.py index 9b74fe992..f12de4a72 100755 --- a/src/conf_mode/protocols_pim.py +++ b/src/conf_mode/protocols_pim.py @@ -17,13 +17,12 @@ import os from ipaddress import IPv4Address -from jinja2 import FileSystemLoader, Environment from sys import exit from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError -from vyos.util import run +from vyos.util import call +from vyos.template import render config_file = r'/tmp/pimd.frr' @@ -115,16 +114,7 @@ def generate(pim): if pim is None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'pim') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - tmpl = env.get_template('pimd.frr.tmpl') - config_text = tmpl.render(pim) - with open(config_file, 'w') as f: - f.write(config_text) - + render(config_file, 'pim/pimd.frr.tmpl', pim) return None def apply(pim): @@ -132,7 +122,7 @@ def apply(pim): return None if os.path.exists(config_file): - run("vtysh -d pimd -f " + config_file) + call("vtysh -d pimd -f " + config_file) os.remove(config_file) return None diff --git a/src/conf_mode/salt-minion.py b/src/conf_mode/salt-minion.py index bd1d44bc8..236480854 100755 --- a/src/conf_mode/salt-minion.py +++ b/src/conf_mode/salt-minion.py @@ -17,16 +17,15 @@ import os from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from pwd import getpwnam from socket import gethostname from sys import exit from urllib3 import PoolManager from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError -from vyos.util import run +from vyos.util import call +from vyos.template import render config_file = r'/etc/salt/minion' @@ -88,18 +87,10 @@ def generate(salt): if salt is None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'salt-minion') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - if not os.path.exists(directory): os.makedirs(directory) - tmpl = env.get_template('minion.tmpl') - config_text = tmpl.render(salt) - with open(config_file, 'w') as f: - f.write(config_text) + render(config_file, 'salt-minion/minion.tmpl', salt) path = "/etc/salt/" for path in paths: @@ -126,10 +117,10 @@ def generate(salt): def apply(salt): if salt is not None: - run("sudo systemctl restart salt-minion") + call("sudo systemctl restart salt-minion") else: # Salt access is removed in the commit - run("sudo systemctl stop salt-minion") + call("sudo systemctl stop salt-minion") os.unlink(config_file) return None diff --git a/src/conf_mode/service-ipoe.py b/src/conf_mode/service-ipoe.py index 5bd4aea2e..3a14d92ef 100755 --- a/src/conf_mode/service-ipoe.py +++ b/src/conf_mode/service-ipoe.py @@ -17,15 +17,15 @@ import os import re -from jinja2 import FileSystemLoader, Environment from socket import socket, AF_INET, SOCK_STREAM from sys import exit from time import sleep from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError from vyos.util import run +from vyos.template import render + ipoe_cnf_dir = r'/etc/accel-ppp/ipoe' ipoe_cnf = ipoe_cnf_dir + r'/ipoe.config' @@ -219,25 +219,15 @@ def generate(c): if c == None or not c: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'ipoe-server') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader, trim_blocks=True) - c['thread_cnt'] = _get_cpu() if c['auth']['mech'] == 'local': - tmpl = env.get_template('chap-secrets.tmpl') - chap_secrets_txt = tmpl.render(c) old_umask = os.umask(0o077) - with open(chap_secrets, 'w') as f: - f.write(chap_secrets_txt) + render(chap_secrets, 'ipoe-server/chap-secrets.tmpl', c, trim_blocks=True) os.umask(old_umask) - tmpl = env.get_template('ipoe.config.tmpl') - config_text = tmpl.render(c) - with open(ipoe_cnf, 'w') as f: - f.write(config_text) + render(ipoe_cnf, 'ipoe-server/ipoe.config.tmpl', c, trim_blocks=True) + # return c ?? return c diff --git a/src/conf_mode/service-pppoe.py b/src/conf_mode/service-pppoe.py index d3fc82406..a96249199 100755 --- a/src/conf_mode/service-pppoe.py +++ b/src/conf_mode/service-pppoe.py @@ -17,15 +17,15 @@ import os import re -from jinja2 import FileSystemLoader, Environment from socket import socket, AF_INET, SOCK_STREAM from sys import exit from time import sleep from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError from vyos.util import run +from vyos.template import render + pidfile = r'/var/run/accel_pppoe.pid' pppoe_cnf_dir = r'/etc/accel-ppp/pppoe' @@ -376,11 +376,6 @@ def generate(c): if c == None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'pppoe-server') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader, trim_blocks=True) - # accel-cmd reload doesn't work so any change results in a restart of the # daemon try: @@ -394,17 +389,11 @@ def generate(c): else: c['thread_cnt'] = int(os.cpu_count() / 2) - tmpl = env.get_template('pppoe.config.tmpl') - config_text = tmpl.render(c) - with open(pppoe_conf, 'w') as f: - f.write(config_text) + render(pppoe_conf, 'pppoe-server/pppoe.config.tmpl', c, trim_blocks=True) if c['authentication']['local-users']: - tmpl = env.get_template('chap-secrets.tmpl') - chap_secrets_txt = tmpl.render(c) old_umask = os.umask(0o077) - with open(chap_secrets, 'w') as f: - f.write(chap_secrets_txt) + render(chap_secrets, 'pppoe-server/chap-secrets.tmpl', c, trim_blocks=True) os.umask(old_umask) return c diff --git a/src/conf_mode/service-router-advert.py b/src/conf_mode/service-router-advert.py index 0173b7242..620f3eacf 100755 --- a/src/conf_mode/service-router-advert.py +++ b/src/conf_mode/service-router-advert.py @@ -16,14 +16,13 @@ import os -from jinja2 import FileSystemLoader, Environment from stat import S_IRUSR, S_IWUSR, S_IRGRP from sys import exit from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError -from vyos.util import run +from vyos.util import call +from vyos.template import render config_file = r'/etc/radvd.conf' @@ -139,15 +138,7 @@ def generate(rtradv): if not rtradv['interfaces']: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'router-advert') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader, trim_blocks=True) - - tmpl = env.get_template('radvd.conf.tmpl') - config_text = tmpl.render(rtradv) - with open(config_file, 'w') as f: - f.write(config_text) + render(config_file, 'router-advert/radvd.conf.tmpl', rtradv, trim_blocks=True) # adjust file permissions of new configuration file if os.path.exists(config_file): @@ -158,13 +149,13 @@ def generate(rtradv): def apply(rtradv): if not rtradv['interfaces']: # bail out early - looks like removal from running config - run('systemctl stop radvd.service') + call('systemctl stop radvd.service') if os.path.exists(config_file): os.unlink(config_file) return None - run('systemctl restart radvd.service') + call('systemctl restart radvd.service') return None if __name__ == '__main__': diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index 414236c88..d654dcb84 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -20,14 +20,13 @@ from binascii import hexlify from time import sleep from stat import S_IRWXU, S_IXGRP, S_IXOTH, S_IROTH, S_IRGRP from sys import exit -from jinja2 import FileSystemLoader, Environment from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos.validate import is_ipv4, is_addr_assigned from vyos.version import get_version_data from vyos import ConfigError -from vyos.util import run +from vyos.util import call +from vyos.template import render config_file_client = r'/etc/snmp/snmp.conf' @@ -509,7 +508,7 @@ def generate(snmp): # # As we are manipulating the snmpd user database we have to stop it first! # This is even save if service is going to be removed - run('systemctl stop snmpd.service') + call('systemctl stop snmpd.service') config_files = [config_file_client, config_file_daemon, config_file_access, config_file_user] for file in config_files: @@ -518,34 +517,14 @@ def generate(snmp): if snmp is None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'snmp') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - # Write client config file - tmpl = env.get_template('etc.snmp.conf.tmpl') - config_text = tmpl.render(snmp) - with open(config_file_client, 'w') as f: - f.write(config_text) - + render(config_file_client, 'snmp/etc.snmp.conf.tmpl', snmp) # Write server config file - tmpl = env.get_template('etc.snmpd.conf.tmpl') - config_text = tmpl.render(snmp) - with open(config_file_daemon, 'w') as f: - f.write(config_text) - + render(config_file_daemon, 'snmp/etc.snmpd.conf.tmpl', snmp) # Write access rights config file - tmpl = env.get_template('usr.snmpd.conf.tmpl') - config_text = tmpl.render(snmp) - with open(config_file_access, 'w') as f: - f.write(config_text) - + render(config_file_access, 'snmp/usr.snmpd.conf.tmpl', snmp) # Write access rights config file - tmpl = env.get_template('var.snmpd.conf.tmpl') - config_text = tmpl.render(snmp) - with open(config_file_user, 'w') as f: - f.write(config_text) + render(config_file_user, 'snmp/var.snmpd.conf.tmpl', snmp) return None @@ -554,7 +533,7 @@ def apply(snmp): return None # start SNMP daemon - run("systemctl restart snmpd.service") + call("systemctl restart snmpd.service") # Passwords are not available immediately in the configuration file, # after daemon startup - we wait until they have been processed by @@ -595,15 +574,15 @@ def apply(snmp): # Now update the running configuration # - # Currently when executing run() the environment does not + # Currently when executing call() the environment does not # have the vyos_libexec_dir variable set, see Phabricator T685. - run('/opt/vyatta/sbin/my_set service snmp v3 user "{0}" auth encrypted-key "{1}" > /dev/null'.format(cfg['user'], cfg['auth_pw'])) - run('/opt/vyatta/sbin/my_set service snmp v3 user "{0}" privacy encrypted-key "{1}" > /dev/null'.format(cfg['user'], cfg['priv_pw'])) - run('/opt/vyatta/sbin/my_delete service snmp v3 user "{0}" auth plaintext-key > /dev/null'.format(cfg['user'])) - run('/opt/vyatta/sbin/my_delete service snmp v3 user "{0}" privacy plaintext-key > /dev/null'.format(cfg['user'])) + call('/opt/vyatta/sbin/my_set service snmp v3 user "{0}" auth encrypted-key "{1}" > /dev/null'.format(cfg['user'], cfg['auth_pw'])) + call('/opt/vyatta/sbin/my_set service snmp v3 user "{0}" privacy encrypted-key "{1}" > /dev/null'.format(cfg['user'], cfg['priv_pw'])) + call('/opt/vyatta/sbin/my_delete service snmp v3 user "{0}" auth plaintext-key > /dev/null'.format(cfg['user'])) + call('/opt/vyatta/sbin/my_delete service snmp v3 user "{0}" privacy plaintext-key > /dev/null'.format(cfg['user'])) # Enable AgentX in FRR - run('vtysh -c "configure terminal" -c "agentx" >/dev/null') + call('vtysh -c "configure terminal" -c "agentx" >/dev/null') return None diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py index a85dcd7f2..ae79eac2d 100755 --- a/src/conf_mode/ssh.py +++ b/src/conf_mode/ssh.py @@ -15,13 +15,12 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -from jinja2 import FileSystemLoader, Environment from sys import exit from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError -from vyos.util import run +from vyos.util import call +from vyos.template import render config_file = r'/etc/ssh/sshd_config' @@ -120,23 +119,15 @@ def generate(ssh): if ssh is None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'ssh') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader, trim_blocks=True) - - tmpl = env.get_template('sshd_config.tmpl') - config_text = tmpl.render(ssh) - with open(config_file, 'w') as f: - f.write(config_text) + render(config_file, 'ssh/sshd_config.tmpl', ssh, trim_blocks=True) return None def apply(ssh): if ssh is not None and 'port' in ssh.keys(): - run("sudo systemctl restart ssh.service") + call("sudo systemctl restart ssh.service") else: # SSH access is removed in the commit - run("sudo systemctl stop ssh.service") + call("sudo systemctl stop ssh.service") if os.path.isfile(config_file): os.unlink(config_file) diff --git a/src/conf_mode/system-ip.py b/src/conf_mode/system-ip.py index 66f563939..8a1ac8411 100755 --- a/src/conf_mode/system-ip.py +++ b/src/conf_mode/system-ip.py @@ -20,7 +20,7 @@ from sys import exit from copy import deepcopy from vyos.config import Config from vyos import ConfigError -from vyos.util import run +from vyos.util import call default_config_data = { @@ -31,7 +31,7 @@ default_config_data = { } def sysctl(name, value): - run('sysctl -wq {}={}'.format(name, value)) + call('sysctl -wq {}={}'.format(name, value)) def get_config(): ip_opt = deepcopy(default_config_data) diff --git a/src/conf_mode/system-ipv6.py b/src/conf_mode/system-ipv6.py index 4e3de6fe9..04a063564 100755 --- a/src/conf_mode/system-ipv6.py +++ b/src/conf_mode/system-ipv6.py @@ -21,7 +21,7 @@ from sys import exit from copy import deepcopy from vyos.config import Config from vyos import ConfigError -from vyos.util import run +from vyos.util import call ipv6_disable_file = '/etc/modprobe.d/vyos_disable_ipv6.conf' @@ -37,7 +37,7 @@ default_config_data = { } def sysctl(name, value): - run('sysctl -wq {}={}'.format(name, value)) + call('sysctl -wq {}={}'.format(name, value)) def get_config(): ip_opt = deepcopy(default_config_data) diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py index 7c99fce39..6008ca0b3 100755 --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -16,7 +16,6 @@ import os -from jinja2 import FileSystemLoader, Environment from psutil import users from pwd import getpwall, getpwnam from stat import S_IRUSR, S_IWUSR, S_IRWXU, S_IRGRP, S_IXGRP @@ -24,9 +23,12 @@ from sys import exit from vyos.config import Config from vyos.configdict import list_diff -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError -from vyos.util import cmd, run +from vyos.util import cmd +from vyos.util import call +from vyos.util import DEVNULL +from vyos.template import render + radius_config_file = "/etc/pam_radius_auth.conf" @@ -207,19 +209,19 @@ def generate(login): # remove old plaintext password # and set new encrypted password - run("vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_set system login user '{}' authentication plaintext-password '' >/dev/null".format(user['name'])) - run("vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_set system login user '{}' authentication encrypted-password '{}' >/dev/null".format(user['name'], user['password_encrypted'])) + os.system("vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_set system login user '{}' authentication plaintext-password '' >/dev/null".format(user['name'])) + os.system("vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_set system login user '{}' authentication encrypted-password '{}' >/dev/null".format(user['name'], user['password_encrypted'])) - if len(login['radius_server']) > 0: - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'system-login') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) + # env = os.environ.copy() + # env['vyos_libexec_dir'] = '/usr/libexec/vyos' - tmpl = env.get_template('pam_radius_auth.conf.tmpl') - config_text = tmpl.render(login) - with open(radius_config_file, 'w') as f: - f.write(config_text) + # call("/opt/vyatta/sbin/my_set system login user '{}' authentication plaintext-password ''".format(user['name']), + # env=env) + # call("/opt/vyatta/sbin/my_set system login user '{}' authentication encrypted-password '{}'".format(user['name'], user['password_encrypted']), + # env=env) + + if len(login['radius_server']) > 0: + render(radius_config_file, 'system-login/pam_radius_auth.conf.tmpl', login) uid = getpwnam('root').pw_uid gid = getpwnam('root').pw_gid @@ -255,7 +257,7 @@ def apply(login): command += " {}".format(user['name']) try: - run(command) + cmd(command) uid = getpwnam(user['name']).pw_uid gid = getpwnam(user['name']).pw_gid @@ -295,10 +297,10 @@ def apply(login): # Logout user if he is logged in if user in list(set([tmp[0] for tmp in users()])): print('{} is logged in, forcing logout'.format(user)) - run('pkill -HUP -u {}'.format(user)) + call('pkill -HUP -u {}'.format(user)) # Remove user account but leave home directory to be safe - run('userdel -r {} 2>/dev/null'.format(user)) + call(f'userdel -r {user}', stderr=DEVNULL) except Exception as e: raise ConfigError('Deleting user "{}" raised an exception: {}'.format(user, e)) @@ -308,8 +310,10 @@ def apply(login): # if len(login['radius_server']) > 0: try: + env = os.environ.copy() + env['DEBIAN_FRONTEND'] = 'noninteractive' # Enable RADIUS in PAM - run("DEBIAN_FRONTEND=noninteractive pam-auth-update --package --enable radius") + cmd("pam-auth-update --package --enable radius", env=env) # Make NSS system aware of RADIUS, too command = "sed -i -e \'/\smapname/b\' \ @@ -320,15 +324,18 @@ def apply(login): -e \'/^group:[^#]*$/s/: */&mapname /\' \ /etc/nsswitch.conf" - run(command) + cmd(command) except Exception as e: raise ConfigError('RADIUS configuration failed: {}'.format(e)) else: try: + env = os.environ.copy() + env['DEBIAN_FRONTEND'] = 'noninteractive' + # Disable RADIUS in PAM - run("DEBIAN_FRONTEND=noninteractive pam-auth-update --package --remove radius") + cmd("pam-auth-update --package --remove radius", env=env) command = "sed -i -e \'/^passwd:.*mapuid[ \t]/s/mapuid[ \t]//\' \ -e \'/^passwd:.*[ \t]mapname/s/[ \t]mapname//\' \ @@ -336,10 +343,10 @@ def apply(login): -e \'s/[ \t]*$//\' \ /etc/nsswitch.conf" - run(command) + cmd(command) except Exception as e: - raise ConfigError('Removing RADIUS configuration failed'.format(e)) + raise ConfigError('Removing RADIUS configuration failed.\n{}'.format(e)) return None diff --git a/src/conf_mode/system-options.py b/src/conf_mode/system-options.py index 063a82463..b3dbc82fb 100755 --- a/src/conf_mode/system-options.py +++ b/src/conf_mode/system-options.py @@ -52,9 +52,9 @@ def generate(opt): def apply(opt): # Beep action if opt['beep_if_fully_booted']: - run('systemctl enable vyos-beep.service >/dev/null 2>&1') + run('systemctl enable vyos-beep.service') else: - run('systemctl disable vyos-beep.service >/dev/null 2>&1') + run('systemctl disable vyos-beep.service') # Ctrl-Alt-Delete action if opt['ctrl_alt_del'] == 'ignore': diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py index 25b9b5bed..9da3d9157 100755 --- a/src/conf_mode/system-syslog.py +++ b/src/conf_mode/system-syslog.py @@ -17,13 +17,13 @@ import os import re -from jinja2 import FileSystemLoader, Environment from sys import exit from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError from vyos.util import run +from vyos.template import render + def get_config(): c = Config() @@ -192,22 +192,13 @@ def generate(c): if c == None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'syslog') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader, trim_blocks=True) - - tmpl = env.get_template('rsyslog.conf.tmpl') - config_text = tmpl.render(c) - with open('/etc/rsyslog.d/vyos-rsyslog.conf', 'w') as f: - f.write(config_text) + conf = '/etc/rsyslog.d/vyos-rsyslog.conf' + render(conf, 'syslog/rsyslog.conf.tmpl', c, trim_blocks=True) # eventually write for each file its own logrotate file, since size is # defined it shouldn't matter - tmpl = env.get_template('logrotate.tmpl') - config_text = tmpl.render(c) - with open('/etc/logrotate.d/vyos-rsyslog', 'w') as f: - f.write(config_text) + conf = '/etc/logrotate.d/vyos-rsyslog' + render(conf, 'syslog/logrotate.tmpl', c, trim_blocks=True) def verify(c): @@ -253,8 +244,8 @@ def verify(c): def apply(c): if not c: - return run('systemctl stop syslog') - return run('systemctl restart syslog') + return run('systemctl stop syslog.service') + return run('systemctl restart syslog.service') if __name__ == '__main__': try: diff --git a/src/conf_mode/system-timezone.py b/src/conf_mode/system-timezone.py index 2f8dc9e89..25b949a79 100755 --- a/src/conf_mode/system-timezone.py +++ b/src/conf_mode/system-timezone.py @@ -20,7 +20,7 @@ import os from copy import deepcopy from vyos.config import Config from vyos import ConfigError -from vyos.util import run +from vyos.util import call default_config_data = { @@ -42,7 +42,7 @@ def generate(tz): pass def apply(tz): - run('/usr/bin/timedatectl set-timezone {}'.format(tz['name'])) + call('/usr/bin/timedatectl set-timezone {}'.format(tz['name'])) if __name__ == '__main__': try: diff --git a/src/conf_mode/system-wifi-regdom.py b/src/conf_mode/system-wifi-regdom.py index 943c42274..b222df0a9 100755 --- a/src/conf_mode/system-wifi-regdom.py +++ b/src/conf_mode/system-wifi-regdom.py @@ -18,11 +18,11 @@ import os from copy import deepcopy from sys import exit -from jinja2 import FileSystemLoader, Environment from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError +from vyos.template import render + config_80211_file='/etc/modprobe.d/cfg80211.conf' config_crda_file='/etc/default/crda' @@ -67,21 +67,8 @@ def generate(regdom): return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'wifi') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - tmpl = env.get_template('cfg80211.conf.tmpl') - config_text = tmpl.render(regdom) - with open(config_80211_file, 'w') as f: - f.write(config_text) - - tmpl = env.get_template('crda.tmpl') - config_text = tmpl.render(regdom) - with open(config_crda_file, 'w') as f: - f.write(config_text) - + render(config_80211_file, 'wifi/cfg80211.conf.tmpl', regdom) + render(config_crda_file, 'wifi/crda.tmpl', regdom) return None def apply(regdom): diff --git a/src/conf_mode/tftp_server.py b/src/conf_mode/tftp_server.py index df8155084..94c8bcf03 100755 --- a/src/conf_mode/tftp_server.py +++ b/src/conf_mode/tftp_server.py @@ -20,14 +20,13 @@ import pwd from copy import deepcopy from glob import glob -from jinja2 import FileSystemLoader, Environment from sys import exit from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos.validate import is_ipv4, is_addr_assigned from vyos import ConfigError -from vyos.util import run +from vyos.util import call +from vyos.template import render config_file = r'/etc/default/tftpd' @@ -90,11 +89,6 @@ def generate(tftpd): if tftpd is None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'tftp-server') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - idx = 0 for listen in tftpd['listen']: config = deepcopy(tftpd) @@ -103,11 +97,8 @@ def generate(tftpd): else: config['listen'] = ["[" + listen + "]" + tftpd['port'] + " -6"] - tmpl = env.get_template('default.tmpl') - config_text = tmpl.render(config) file = config_file + str(idx) - with open(file, 'w') as f: - f.write(config_text) + render(file, 'tftp-server/default.tmpl', config) idx = idx + 1 @@ -115,7 +106,7 @@ def generate(tftpd): def apply(tftpd): # stop all services first - then we will decide - run('systemctl stop tftpd@{0..20}') + call('systemctl stop tftpd@{0..20}.service') # bail out early - e.g. service deletion if tftpd is None: @@ -140,7 +131,7 @@ def apply(tftpd): idx = 0 for listen in tftpd['listen']: - run('systemctl restart tftpd@{0}.service'.format(idx)) + call('systemctl restart tftpd@{0}.service'.format(idx)) idx = idx + 1 return None diff --git a/src/conf_mode/vpn-pptp.py b/src/conf_mode/vpn-pptp.py index 45b2c4b40..15b80f984 100755 --- a/src/conf_mode/vpn-pptp.py +++ b/src/conf_mode/vpn-pptp.py @@ -17,15 +17,15 @@ import os import re -from jinja2 import FileSystemLoader, Environment from socket import socket, AF_INET, SOCK_STREAM from sys import exit from time import sleep from vyos.config import Config -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError from vyos.util import run +from vyos.template import render + pidfile = r'/var/run/accel_pptp.pid' pptp_cnf_dir = r'/etc/accel-ppp/pptp' @@ -206,11 +206,6 @@ def generate(c): if c == None: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'pptp') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader, trim_blocks=True) - # accel-cmd reload doesn't work so any change results in a restart of the daemon try: if os.cpu_count() == 1: @@ -223,19 +218,13 @@ def generate(c): else: c['thread_cnt'] = int(os.cpu_count()/2) - tmpl = env.get_template('pptp.config.tmpl') - config_text = tmpl.render(c) - with open(pptp_conf, 'w') as f: - f.write(config_text) + render(pptp_conf, 'pptp/pptp.config.tmpl', c, trim_blocks=True) if c['authentication']['local-users']: - tmpl = env.get_template('chap-secrets.tmpl') - chap_secrets_txt = tmpl.render(c) old_umask = os.umask(0o077) - with open(chap_secrets, 'w') as f: - f.write(chap_secrets_txt) + render(chap_secrets, 'pptp/chap-secrets.tmpl', c, trim_blocks=True) os.umask(old_umask) - + # return c ?? return c diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py new file mode 100755 index 000000000..a8b183bef --- /dev/null +++ b/src/conf_mode/vpn_l2tp.py @@ -0,0 +1,386 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import re + +from copy import deepcopy +from stat import S_IRUSR, S_IWUSR, S_IRGRP +from sys import exit +from time import sleep + +from ipaddress import ip_network + +from vyos.config import Config +from vyos.util import call +from vyos.validate import is_ipv4 +from vyos import ConfigError +from vyos.template import render + + +l2tp_conf = '/run/accel-pppd/l2tp.conf' +l2tp_chap_secrets = '/run/accel-pppd/l2tp.chap-secrets' + +default_config_data = { + 'auth_mode': 'local', + 'auth_ppp_mppe': 'prefer', + 'auth_proto': ['auth_mschap_v2'], + 'chap_secrets_file': l2tp_chap_secrets, # used in Jinja2 template + 'client_ip_pool': None, + 'client_ip_subnets': [], + 'client_ipv6_pool': [], + 'client_ipv6_delegate_prefix': [], + 'dnsv4': [], + 'dnsv6': [], + 'gateway_address': '10.255.255.0', + 'local_users' : [], + 'mtu': '1436', + 'outside_addr': '', + 'ppp_mppe': 'prefer', + 'ppp_echo_failure' : '3', + 'ppp_echo_interval' : '30', + 'ppp_echo_timeout': '0', + 'radius_server': [], + 'radius_acct_tmo': '3', + 'radius_max_try': '3', + 'radius_timeout': '3', + 'radius_nas_id': '', + 'radius_nas_ip': '', + 'radius_source_address': '', + 'radius_shaper_attr': '', + 'radius_shaper_vendor': '', + 'radius_dynamic_author': '', + 'wins': [], + 'ip6_column': [], + 'thread_cnt': 1 +} + +def get_config(): + conf = Config() + base_path = ['vpn', 'l2tp', 'remote-access'] + if not conf.exists(base_path): + return None + + conf.set_level(base_path) + l2tp = deepcopy(default_config_data) + + cpu = os.cpu_count() + if cpu > 1: + l2tp['thread_cnt'] = int(cpu/2) + + ### general options ### + if conf.exists(['name-server']): + for name_server in conf.return_values(['name-server']): + if is_ipv4(name_server): + l2tp['dnsv4'].append(name_server) + else: + l2tp['dnsv6'].append(name_server) + + if conf.exists(['wins-server']): + l2tp['wins'] = conf.return_values(['wins-server']) + + if conf.exists('outside-address'): + l2tp['outside_addr'] = conf.return_value('outside-address') + + if conf.exists(['authentication', 'mode']): + l2tp['auth_mode'] = conf.return_value(['authentication', 'mode']) + + if conf.exists(['authentication', 'protocols']): + auth_mods = { + 'pap': 'auth_pap', + 'chap': 'auth_chap_md5', + 'mschap': 'auth_mschap_v1', + 'mschap-v2': 'auth_mschap_v2' + } + + for proto in conf.return_values(['authentication', 'protocols']): + l2tp['auth_proto'].append(auth_mods[proto]) + + if conf.exists(['authentication', 'mppe']): + l2tp['auth_ppp_mppe'] = conf.return_value(['authentication', 'mppe']) + + # + # local auth + if conf.exists(['authentication', 'local-users']): + for username in conf.list_nodes(['authentication', 'local-users', 'username']): + user = { + 'name' : username, + 'password' : '', + 'state' : 'enabled', + 'ip' : '*', + 'upload' : None, + 'download' : None + } + + conf.set_level(base_path + ['authentication', 'local-users', 'username', username]) + + if conf.exists(['password']): + user['password'] = conf.return_value(['password']) + + if conf.exists(['disable']): + user['state'] = 'disable' + + if conf.exists(['static-ip']): + user['ip'] = conf.return_value(['static-ip']) + + if conf.exists(['rate-limit', 'download']): + user['download'] = conf.return_value(['rate-limit', 'download']) + + if conf.exists(['rate-limit', 'upload']): + user['upload'] = conf.return_value(['rate-limit', 'upload']) + + l2tp['local_users'].append(user) + + # + # RADIUS auth and settings + conf.set_level(base_path + ['authentication', 'radius']) + if conf.exists(['server']): + for server in conf.list_nodes(['server']): + radius = { + 'server' : server, + 'key' : '', + 'fail_time' : 0, + 'port' : '1812' + } + + conf.set_level(base_path + ['authentication', 'radius', 'server', server]) + + if conf.exists(['fail-time']): + radius['fail-time'] = conf.return_value(['fail-time']) + + if conf.exists(['port']): + radius['port'] = conf.return_value(['port']) + + if conf.exists(['key']): + radius['key'] = conf.return_value(['key']) + + if not conf.exists(['disable']): + l2tp['radius_server'].append(radius) + + # + # advanced radius-setting + conf.set_level(base_path + ['authentication', 'radius']) + + if conf.exists(['acct-timeout']): + l2tp['radius_acct_tmo'] = conf.return_value(['acct-timeout']) + + if conf.exists(['max-try']): + l2tp['radius_max_try'] = conf.return_value(['max-try']) + + if conf.exists(['timeout']): + l2tp['radius_timeout'] = conf.return_value(['timeout']) + + if conf.exists(['nas-identifier']): + l2tp['radius_nas_id'] = conf.return_value(['nas-identifier']) + + if conf.exists(['nas-ip-address']): + l2tp['radius_nas_ip'] = conf.return_value(['nas-ip-address']) + + if conf.exists(['source-address']): + l2tp['radius_source_address'] = conf.return_value(['source-address']) + + # Dynamic Authorization Extensions (DOA)/Change Of Authentication (COA) + if conf.exists(['dynamic-author']): + dae = { + 'port' : '', + 'server' : '', + 'key' : '' + } + + if conf.exists(['dynamic-author', 'server']): + dae['server'] = conf.return_value(['dynamic-author', 'server']) + + if conf.exists(['dynamic-author', 'port']): + dae['port'] = conf.return_value(['dynamic-author', 'port']) + + if conf.exists(['dynamic-author', 'key']): + dae['key'] = conf.return_value(['dynamic-author', 'key']) + + l2tp['radius_dynamic_author'] = dae + + if conf.exists(['rate-limit', 'enable']): + l2tp['radius_shaper_attr'] = 'Filter-Id' + c_attr = ['rate-limit', 'enable', 'attribute'] + if conf.exists(c_attr): + l2tp['radius_shaper_attr'] = conf.return_value(c_attr) + + c_vendor = ['rate-limit', 'enable', 'vendor'] + if conf.exists(c_vendor): + l2tp['radius_shaper_vendor'] = conf.return_value(c_vendor) + + conf.set_level(base_path) + if conf.exists(['client-ip-pool']): + if conf.exists(['client-ip-pool', 'start']) and conf.exists(['client-ip-pool', 'stop']): + start = conf.return_value(['client-ip-pool', 'start']) + stop = conf.return_value(['client-ip-pool', 'stop']) + l2tp['client_ip_pool'] = start + '-' + re.search('[0-9]+$', stop).group(0) + + if conf.exists(['client-ip-pool', 'subnet']): + l2tp['client_ip_subnets'] = conf.return_values(['client-ip-pool', 'subnet']) + + if conf.exists(['client-ipv6-pool', 'prefix']): + l2tp['ip6_column'].append('ip6') + for prefix in conf.list_nodes(['client-ipv6-pool', 'prefix']): + tmp = { + 'prefix': prefix, + 'mask': '64' + } + + if conf.exists(['client-ipv6-pool', 'prefix', prefix, 'mask']): + tmp['mask'] = conf.return_value(['client-ipv6-pool', 'prefix', prefix, 'mask']) + + l2tp['client_ipv6_pool'].append(tmp) + + if conf.exists(['client-ipv6-pool', 'delegate']): + l2tp['ip6_column'].append('ip6-db') + for prefix in conf.list_nodes(['client-ipv6-pool', 'delegate']): + tmp = { + 'prefix': prefix, + 'mask': '' + } + + if conf.exists(['client-ipv6-pool', 'delegate', prefix, 'mask']): + tmp['mask'] = conf.return_value(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']) + + l2tp['client_ipv6_delegate_prefix'].append(tmp) + + if conf.exists(['mtu']): + l2tp['mtu'] = conf.return_value(['mtu']) + + # gateway address + if conf.exists(['gateway-address']): + l2tp['gateway_address'] = conf.return_value(['gateway-address']) + else: + # calculate gw-ip-address + if conf.exists(['client-ip-pool', 'start']): + # use start ip as gw-ip-address + l2tp['gateway_address'] = conf.return_value(['client-ip-pool', 'start']) + + elif conf.exists(['client-ip-pool', 'subnet']): + # use first ip address from first defined pool + subnet = conf.return_values(['client-ip-pool', 'subnet'])[0] + subnet = ip_network(subnet) + l2tp['gateway_address'] = str(list(subnet.hosts())[0]) + + # LNS secret + if conf.exists(['lns', 'shared-secret']): + l2tp['lns_shared_secret'] = conf.return_value(['lns', 'shared-secret']) + + if conf.exists(['ccp-disable']): + l2tp[['ccp_disable']] = True + + # PPP options + if conf.exists(['idle']): + l2tp['ppp_echo_timeout'] = conf.return_value(['idle']) + + if conf.exists(['ppp-options', 'lcp-echo-failure']): + l2tp['ppp_echo_failure'] = conf.return_value(['ppp-options', 'lcp-echo-failure']) + + if conf.exists(['ppp-options', 'lcp-echo-interval']): + l2tp['ppp_echo_interval'] = conf.return_value(['ppp-options', 'lcp-echo-interval']) + + return l2tp + + +def verify(l2tp): + if not l2tp: + return None + + if l2tp['auth_mode'] == 'local': + if not l2tp['local_users']: + raise ConfigError('L2TP local auth mode requires local users to be configured!') + + for user in l2tp['local_users']: + if not user['password']: + raise ConfigError(f"Password required for user {user['name']}") + + elif l2tp['auth_mode'] == 'radius': + if len(l2tp['radius_server']) == 0: + raise ConfigError("RADIUS authentication requires at least one server") + + for radius in l2tp['radius_server']: + if not radius['key']: + raise ConfigError(f"Missing RADIUS secret for server {{ radius['key'] }}") + + # check for the existence of a client ip pool + if not (l2tp['client_ip_pool'] or l2tp['client_ip_subnets']): + raise ConfigError( + "set vpn l2tp remote-access client-ip-pool requires subnet or start/stop IP pool") + + # check ipv6 + if l2tp['client_ipv6_delegate_prefix'] and not l2tp['client_ipv6_pool']: + raise ConfigError('IPv6 prefix delegation requires client-ipv6-pool prefix') + + for prefix in l2tp['client_ipv6_delegate_prefix']: + if not prefix['mask']: + raise ConfigError('Delegation-prefix required for individual delegated networks') + + if len(l2tp['wins']) > 2: + raise ConfigError('Not more then two IPv4 WINS name-servers can be configured') + + if len(l2tp['dnsv4']) > 2: + raise ConfigError('Not more then two IPv4 DNS name-servers can be configured') + + if len(l2tp['dnsv6']) > 3: + raise ConfigError('Not more then three IPv6 DNS name-servers can be configured') + + return None + + +def generate(l2tp): + if not l2tp: + return None + + dirname = os.path.dirname(l2tp_conf) + if not os.path.exists(dirname): + os.mkdir(dirname) + + render(l2tp_conf, 'l2tp/l2tp.config.tmpl', c, trim_blocks=True) + + if l2tp['auth_mode'] == 'local': + render(l2tp_chap_secrets, 'l2tp/chap-secrets.tmpl', l2tp) + os.chmod(l2tp_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP) + + else: + if os.path.exists(l2tp_chap_secrets): + os.unlink(l2tp_chap_secrets) + + return None + + +def apply(l2tp): + if not l2tp: + call('systemctl stop accel-ppp@l2tp.service') + + if os.path.exists(l2tp_conf): + os.unlink(l2tp_conf) + + if os.path.exists(l2tp_chap_secrets): + os.unlink(l2tp_chap_secrets) + + return None + + call('systemctl restart accel-ppp@l2tp.service') + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index ca0844c50..438731972 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -18,49 +18,24 @@ import os from time import sleep from sys import exit -from socket import socket, AF_INET, SOCK_STREAM from copy import deepcopy from stat import S_IRUSR, S_IWUSR, S_IRGRP -from jinja2 import FileSystemLoader, Environment from vyos.config import Config from vyos import ConfigError -from vyos.defaults import directories as vyos_data_dir -from vyos.util import process_running -from vyos.util import process_running, cmd, run - -pidfile = r'/var/run/accel_sstp.pid' -sstp_cnf_dir = r'/etc/accel-ppp/sstp' -chap_secrets = sstp_cnf_dir + '/chap-secrets' -sstp_conf = sstp_cnf_dir + '/sstp.config' - -# config path creation -if not os.path.exists(sstp_cnf_dir): - os.makedirs(sstp_cnf_dir) - -def chk_con(): - cnt = 0 - s = socket(AF_INET, SOCK_STREAM) - while True: - try: - s.connect(("127.0.0.1", 2005)) - s.close() - break - except ConnectionRefusedError: - sleep(0.5) - cnt += 1 - if cnt == 100: - raise("failed to start sstp server") - break - - -def _accel_cmd(command): - return run(f'/usr/bin/accel-cmd -p 2005 {command}') +from vyos.util import call, run +from vyos.template import render + + +sstp_conf = '/run/accel-pppd/sstp.conf' +sstp_chap_secrets = '/run/accel-pppd/sstp.chap-secrets' default_config_data = { 'local_users' : [], 'auth_mode' : 'local', - 'auth_proto' : [], + 'auth_proto' : ['auth_mschap_v2'], + 'chap_secrets_file': sstp_chap_secrets, # used in Jinja2 template + 'client_gateway': '', 'radius_server' : [], 'radius_acct_tmo' : '3', 'radius_max_try' : '3', @@ -77,11 +52,11 @@ default_config_data = { 'client_ip_pool' : [], 'dnsv4' : [], 'mtu' : '', - 'ppp_mppe' : '', + 'ppp_mppe' : 'prefer', 'ppp_echo_failure' : '', 'ppp_echo_interval' : '', 'ppp_echo_timeout' : '', - 'thread_cnt' : '' + 'thread_cnt' : 1 } def get_config(): @@ -93,10 +68,9 @@ def get_config(): conf.set_level(base_path) - cpu = int(os.cpu_count()/2) - if cpu < 1: - cpu = 1 - sstp['thread_cnt'] = cpu + cpu = os.cpu_count() + if cpu > 1: + sstp['thread_cnt'] = int(cpu/2) if conf.exists(['authentication', 'mode']): sstp['auth_mode'] = conf.return_value(['authentication', 'mode']) @@ -214,6 +188,8 @@ def get_config(): # authentication protocols conf.set_level(base_path + ['authentication']) if conf.exists(['protocols']): + # clear default list content, now populate with actual CLI values + sstp['auth_proto'] = [] auth_mods = { 'pap': 'auth_pap', 'chap': 'auth_chap_md5', @@ -224,9 +200,6 @@ def get_config(): for proto in conf.return_values(['protocols']): sstp['auth_proto'].append(auth_mods[proto]) - else: - sstp['auth_proto'] = ['auth_mschap_v2'] - # # read in SSL certs conf.set_level(base_path + ['ssl']) @@ -262,7 +235,7 @@ def get_config(): # read in PPP stuff conf.set_level(base_path + ['ppp-settings']) if conf.exists('mppe'): - sstp['ppp_mppe'] = conf.return_value('ppp-settings mppe') + sstp['ppp_mppe'] = conf.return_value(['ppp-settings', 'mppe']) if conf.exists(['lcp-echo-failure']): sstp['ppp_echo_failure'] = conf.return_value(['lcp-echo-failure']) @@ -283,7 +256,7 @@ def verify(sstp): # vertify auth settings if sstp['auth_mode'] == 'local': if not sstp['local_users']: - raise ConfigError('sstp-server authentication local-users required') + raise ConfigError('SSTP local auth mode requires local users to be configured!') for user in sstp['local_users']: if not user['password']: @@ -303,7 +276,7 @@ def verify(sstp): raise ConfigError("Client gateway IP address required") if len(sstp['dnsv4']) > 2: - raise ConfigError("Only 2 DNS name-servers can be configured") + raise ConfigError('Not more then two IPv4 DNS name-servers can be configured') if not sstp['ssl_ca'] or not sstp['ssl_cert'] or not sstp['ssl_key']: raise ConfigError('One or more SSL certificates missing') @@ -326,69 +299,38 @@ def verify(sstp): raise ConfigError(f"Missing RADIUS secret for server {{ radius['key'] }}") def generate(sstp): - if sstp is None: + if not sstp: return None - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'sstp') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader, trim_blocks=True) + dirname = os.path.dirname(sstp_conf) + if not os.path.exists(dirname): + os.mkdir(dirname) # accel-cmd reload doesn't work so any change results in a restart of the daemon - tmpl = env.get_template('sstp.config.tmpl') - config_text = tmpl.render(sstp) - with open(sstp_conf, 'w') as f: - f.write(config_text) + render(sstp_conf, 'sstp/sstp.config.tmpl', sstp, trim_blocks=True) if sstp['local_users']: - tmpl = env.get_template('chap-secrets.tmpl') - config_text = tmpl.render(sstp) - with open(chap_secrets, 'w') as f: - f.write(config_text) - - os.chmod(chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP) + render(sstp_chap_secrets, 'sstp/chap-secrets.tmpl', sstp, trim_blocks=True) + os.chmod(sstp_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP) else: - if os.path.exists(chap_secrets): - os.unlink(chap_secrets) + if os.path.exists(sstp_chap_secrets): + os.unlink(sstp_chap_secrets) return sstp def apply(sstp): - if sstp is None: - if process_running(pidfile): - command = 'start-stop-daemon' - command += ' --stop ' - command += ' --quiet' - command += ' --oknodo' - command += ' --pidfile ' + pidfile - cmd(command) + if not sstp: + call('systemctl stop accel-ppp@sstp.service') - if os.path.exists(pidfile): - os.remove(pidfile) + if os.path.exists(sstp_conf): + os.unlink(sstp_conf) - return None + if os.path.exists(sstp_chap_secrets): + os.unlink(sstp_chap_secrets) - if not process_running(pidfile): - if os.path.exists(pidfile): - os.remove(pidfile) - - command = 'start-stop-daemon' - command += ' --start ' - command += ' --quiet' - command += ' --oknodo' - command += ' --pidfile ' + pidfile - command += ' --exec /usr/sbin/accel-pppd' - # now pass arguments to accel-pppd binary - command += ' --' - command += ' -c ' + sstp_conf - command += ' -p ' + pidfile - command += ' -d' - cmd(command) - - chk_con() + return None - else: - _accel_cmd('restart') + call('systemctl restart accel-ppp@sstp.service') if __name__ == '__main__': diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 07466f3aa..eb73293a9 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -18,15 +18,15 @@ import os from sys import exit from copy import deepcopy -from jinja2 import FileSystemLoader, Environment from json import loads from vyos.config import Config from vyos.configdict import list_diff -from vyos.defaults import directories as vyos_data_dir from vyos.ifconfig import Interface from vyos.util import read_file, cmd from vyos import ConfigError +from vyos.template import render + config_file = r'/etc/iproute2/rt_tables.d/vyos-vrf.conf' @@ -178,16 +178,7 @@ def verify(vrf_config): return None def generate(vrf_config): - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'vrf') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - - tmpl = env.get_template('vrf.conf.tmpl') - config_text = tmpl.render(vrf_config) - with open(config_file, 'w') as f: - f.write(config_text) - + render(config_file, 'vrf/vrf.conf.tmpl', vrf_config) return None def apply(vrf_config): @@ -195,6 +186,7 @@ def apply(vrf_config): # # - https://github.com/torvalds/linux/blob/master/Documentation/networking/vrf.txt # - https://github.com/Mellanox/mlxsw/wiki/Virtual-Routing-and-Forwarding-(VRF) + # - https://github.com/Mellanox/mlxsw/wiki/L3-Tunneling # - https://netdevconf.info/1.1/proceedings/slides/ahern-vrf-tutorial.pdf # - https://netdevconf.info/1.2/slides/oct6/02_ahern_what_is_l3mdev_slides.pdf diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py index d3e3710d1..b9b0405e2 100755 --- a/src/conf_mode/vrrp.py +++ b/src/conf_mode/vrrp.py @@ -18,16 +18,16 @@ import os from sys import exit from ipaddress import ip_address, ip_interface, IPv4Interface, IPv6Interface, IPv4Address, IPv6Address -from jinja2 import FileSystemLoader, Environment from json import dumps from pathlib import Path import vyos.config import vyos.keepalived -from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError -from vyos.util import run +from vyos.util import call +from vyos.template import render + daemon_file = "/etc/default/keepalived" config_file = "/etc/keepalived/keepalived.conf" @@ -201,11 +201,6 @@ def verify(data): def generate(data): - # Prepare Jinja2 template loader from files - tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'vrrp') - fs_loader = FileSystemLoader(tmpl_path) - env = Environment(loader=fs_loader) - vrrp_groups, sync_groups = data # Remove disabled groups from the sync group member lists @@ -217,16 +212,9 @@ def generate(data): # Filter out disabled groups vrrp_groups = list(filter(lambda x: x["disable"] is not True, vrrp_groups)) - tmpl = env.get_template('keepalived.conf.tmpl') - config_text = tmpl.render({"groups": vrrp_groups, "sync_groups": sync_groups}) - with open(config_file, 'w') as f: - f.write(config_text) - - tmpl = env.get_template('daemon.tmpl') - config_text = tmpl.render() - with open(daemon_file, 'w') as f: - f.write(config_text) - + render(config_file, 'vrrp/keepalived.conf.tmpl', + {"groups": vrrp_groups, "sync_groups": sync_groups}) + render(daemon_file, 'vrrp/daemon.tmpl', {}) return None @@ -242,17 +230,17 @@ def apply(data): if not vyos.keepalived.vrrp_running(): print("Starting the VRRP process") - ret = run("sudo systemctl restart keepalived.service") + ret = call("sudo systemctl restart keepalived.service") else: print("Reloading the VRRP process") - ret = run("sudo systemctl reload keepalived.service") + ret = call("sudo systemctl reload keepalived.service") if ret != 0: raise ConfigError("keepalived failed to start") else: # VRRP is removed in the commit print("Stopping the VRRP process") - run("sudo systemctl stop keepalived.service") + call("sudo systemctl stop keepalived.service") os.unlink(config_file) return None diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook b/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook index dcd06644f..eeb8b0782 100644 --- a/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook +++ b/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook @@ -22,8 +22,13 @@ # To enable this script set the following variable to "yes" RUN="yes" +proto="" +if [[ $reason =~ (REBOOT6|INIT6|EXPIRE6|RELEASE6|STOP6|INFORM6|BOUND6|REBIND6|DELEGATED6) ]]; then + proto="v6" +fi + if [ "$RUN" = "yes" ]; then - LOG=/var/lib/dhcp/dhclient_"$interface"_lease + LOG=/var/lib/dhcp/dhclient_"$interface"."$proto"lease echo `date` > $LOG for i in reason interface new_expiry new_dhcp_lease_time medium \ diff --git a/src/etc/init.d/isc-dhcpv4-server b/src/etc/init.d/isc-dhcpv4-server deleted file mode 100755 index 94a1020ac..000000000 --- a/src/etc/init.d/isc-dhcpv4-server +++ /dev/null @@ -1,113 +0,0 @@ -#!/bin/sh -# -# - -### BEGIN INIT INFO -# Provides: isc-dhcpv4-server -# Required-Start: $remote_fs $network $syslog -# Required-Stop: $remote_fs $network $syslog -# Should-Start: $local_fs slapd $named -# Should-Stop: $local_fs slapd -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: IPv4 DHCP server -# Description: Dynamic Host Configuration Protocol Server for IPv4 -### END INIT INFO - -PATH=/sbin:/bin:/usr/sbin:/usr/bin - -test -f /usr/sbin/dhcpd || exit 0 - -DHCPD_DEFAULT="${DHCPD_DEFAULT:-/etc/default/isc-dhcpv4-server}" - -# It is not safe to start if we don't have a default configuration... -if [ ! -f "$DHCPD_DEFAULT" ]; then - echo "$DHCPD_DEFAULT does not exist! - Aborting..." - exit 0 -fi - -. /lib/lsb/init-functions - -# Read init script configuration -[ -f "$DHCPD_DEFAULT" ] && . "$DHCPD_DEFAULT" - -NAME=dhcpd -DESC="ISC DHCP server" -# fallback to default config file -DHCPD_CONF=${DHCPD_CONF:-/etc/dhcp/dhcpd.conf} -# try to read pid file name from config file, with fallback to /var/run/dhcpd.pid -if [ -z "$DHCPD_PID" ]; then - DHCPD_PID=$(sed -n -e 's/^[ \t]*pid-file-name[ \t]*"(.*)"[ \t]*;.*$/\1/p' < "$DHCPD_CONF" 2>/dev/null | head -n 1) -fi -DHCPD_PID="${DHCPD_PID:-/var/run/dhcpd.pid}" - -test_config() -{ - if ! /usr/sbin/dhcpd -t $OPTIONS -q -cf "$DHCPD_CONF" > /dev/null 2>&1; then - echo "dhcpd self-test failed. Please fix $DHCPD_CONF." - echo "The error was: " - /usr/sbin/dhcpd -t $OPTIONS -cf "$DHCPD_CONF" - exit 1 - fi - touch /var/lib/dhcp/dhcpd.leases -} - -# single arg is -v for messages, -q for none -check_status() -{ - if [ ! -r "$DHCPD_PID" ]; then - test "$1" != -v || echo "$NAME is not running." - return 3 - fi - if read pid < "$DHCPD_PID" && ps -p "$pid" > /dev/null 2>&1; then - test "$1" != -v || echo "$NAME is running." - return 0 - else - test "$1" != -v || echo "$NAME is not running but $DHCPD_PID exists." - return 1 - fi -} - -case "$1" in - start) - test_config - log_daemon_msg "Starting $DESC" "$NAME" - start-stop-daemon --start --oknodo --quiet --pidfile "$DHCPD_PID" \ - --exec /usr/sbin/dhcpd -- \ - -q $OPTIONS -cf "$DHCPD_CONF" -pf "$DHCPD_PID" $INTERFACES - sleep 2 - - if check_status -q; then - log_end_msg 0 - else - log_failure_msg "check syslog for diagnostics." - log_end_msg 1 - exit 1 - fi - ;; - stop) - log_daemon_msg "Stopping $DESC" "$NAME" - start-stop-daemon --stop --oknodo --quiet --pidfile "$DHCPD_PID" - log_end_msg $? - rm -f "$DHCPD_PID" - ;; - restart | force-reload) - test_config - $0 stop - sleep 2 - $0 start - if [ "$?" != "0" ]; then - exit 1 - fi - ;; - status) - echo -n "Status of $DESC: " - check_status -v - exit "$?" - ;; - *) - echo "Usage: $0 {start|stop|restart|force-reload|status}" - exit 1 -esac - -exit 0 diff --git a/src/etc/init.d/isc-dhcpv6-relay b/src/etc/init.d/isc-dhcpv6-relay deleted file mode 100755 index e553eafd1..000000000 --- a/src/etc/init.d/isc-dhcpv6-relay +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/sh -# -# - -### BEGIN INIT INFO -# Provides: isc-dhcpv6-relay -# Required-Start: $remote_fs $network -# Required-Stop: $remote_fs $network -# Should-Start: $local_fs -# Should-Stop: $local_fs -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: IPv6 DHCP relay -# Description: Dynamic Host Configuration Protocol Relay for IPv6 -### END INIT INFO - -# It is not safe to start if we don't have a default configuration... -if [ ! -f /etc/default/isc-dhcpv6-relay ]; then - echo "/etc/default/isc-dhcpv6-relay does not exist! - Aborting..." - exit 1 -fi - -# Source init functions -. /lib/lsb/init-functions - -# Read init script configuration (interfaces the daemon should listen on -# and the DHCP server we should forward requests to.) -[ -f /etc/default/isc-dhcpv6-relay ] && . /etc/default/isc-dhcpv6-relay - -DHCRELAYPID=/var/run/dhcv6relay.pid - -case "$1" in - start) - start-stop-daemon --start --oknodo --quiet --pidfile $DHCRELAYPID \ - --exec /usr/sbin/dhcrelay -- -q $OPTIONS -pf $DHCRELAYPID - ;; - stop) - start-stop-daemon --stop --oknodo --quiet --pidfile $DHCRELAYPID - ;; - restart | force-reload) - $0 stop - sleep 2 - $0 start - ;; - *) - echo "Usage: /etc/init.d/isc-dhcpv6-relay {start|stop|restart|force-reload}" - exit 1 -esac - -exit 0 diff --git a/src/etc/init.d/isc-dhcpv6-server b/src/etc/init.d/isc-dhcpv6-server deleted file mode 100755 index f6b27cb4a..000000000 --- a/src/etc/init.d/isc-dhcpv6-server +++ /dev/null @@ -1,113 +0,0 @@ -#!/bin/sh -# -# - -### BEGIN INIT INFO -# Provides: isc-dhcpv6-server -# Required-Start: $remote_fs $network $syslog -# Required-Stop: $remote_fs $network $syslog -# Should-Start: $local_fs slapd $named -# Should-Stop: $local_fs slapd -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: IPv6 DHCP server -# Description: Dynamic Host Configuration Protocol Server for IPv6 -### END INIT INFO - -PATH=/sbin:/bin:/usr/sbin:/usr/bin - -test -f /usr/sbin/dhcpd || exit 0 - -DHCPD_DEFAULT="${DHCPD_DEFAULT:-/etc/default/isc-dhcpv6-server}" - -# It is not safe to start if we don't have a default configuration... -if [ ! -f "$DHCPD_DEFAULT" ]; then - echo "$DHCPD_DEFAULT does not exist! - Aborting..." - exit 0 -fi - -. /lib/lsb/init-functions - -# Read init script configuration -[ -f "$DHCPD_DEFAULT" ] && . "$DHCPD_DEFAULT" - -NAME=dhcpdv6 -DESC="ISC DHCP server IPv6" -# fallback to default config file -DHCPD_CONF=${DHCPD_CONF:-/etc/dhcp/dhcpdv6.conf} -# try to read pid file name from config file, with fallback to /var/run/dhcpdv6.pid -if [ -z "$DHCPD_PID" ]; then - DHCPD_PID=$(sed -n -e 's/^[ \t]*pid-file-name[ \t]*"(.*)"[ \t]*;.*$/\1/p' < "$DHCPD_CONF" 2>/dev/null | head -n 1) -fi -DHCPD_PID="${DHCPD_PID:-/var/run/dhcpdv6.pid}" - -test_config() -{ - if ! /usr/sbin/dhcpd -t $OPTIONS -q -cf "$DHCPD_CONF" > /dev/null 2>&1; then - echo "dhcpd self-test failed. Please fix $DHCPD_CONF." - echo "The error was: " - /usr/sbin/dhcpd -t $OPTIONS -cf "$DHCPD_CONF" - exit 1 - fi - touch /var/lib/dhcp/dhcpdv6.leases -} - -# single arg is -v for messages, -q for none -check_status() -{ - if [ ! -r "$DHCPD_PID" ]; then - test "$1" != -v || echo "$NAME is not running." - return 3 - fi - if read pid < "$DHCPD_PID" && ps -p "$pid" > /dev/null 2>&1; then - test "$1" != -v || echo "$NAME is running." - return 0 - else - test "$1" != -v || echo "$NAME is not running but $DHCPD_PID exists." - return 1 - fi -} - -case "$1" in - start) - test_config - log_daemon_msg "Starting $DESC" "$NAME" - start-stop-daemon --start --oknodo --quiet --pidfile "$DHCPD_PID" \ - --exec /usr/sbin/dhcpd -- \ - -q $OPTIONS -cf "$DHCPD_CONF" -pf "$DHCPD_PID" $INTERFACES - sleep 2 - - if check_status -q; then - log_end_msg 0 - else - log_failure_msg "check syslog for diagnostics." - log_end_msg 1 - exit 1 - fi - ;; - stop) - log_daemon_msg "Stopping $DESC" "$NAME" - start-stop-daemon --stop --oknodo --quiet --pidfile "$DHCPD_PID" - log_end_msg $? - rm -f "$DHCPD_PID" - ;; - restart | force-reload) - test_config - $0 stop - sleep 2 - $0 start - if [ "$?" != "0" ]; then - exit 1 - fi - ;; - status) - echo -n "Status of $DESC: " - check_status -v - exit "$?" - ;; - *) - echo "Usage: $0 {start|stop|restart|force-reload|status}" - exit 1 -esac - -exit 0 diff --git a/src/etc/systemd/system/hostapd@.service.d/override.conf b/src/etc/systemd/system/hostapd@.service.d/override.conf new file mode 100644 index 000000000..bb8e81d7a --- /dev/null +++ b/src/etc/systemd/system/hostapd@.service.d/override.conf @@ -0,0 +1,10 @@ +[Unit] +After= +After=vyos-router.service + +[Service] +WorkingDirectory=/run/hostapd +EnvironmentFile= +ExecStart= +ExecStart=/usr/sbin/hostapd -B -P /run/hostapd/%i.pid /run/hostapd/%i.conf +PIDFile=/run/hostapd/%i.pid diff --git a/src/etc/systemd/system/openvpn@.service.d/override.conf b/src/etc/systemd/system/openvpn@.service.d/override.conf new file mode 100644 index 000000000..7946484a3 --- /dev/null +++ b/src/etc/systemd/system/openvpn@.service.d/override.conf @@ -0,0 +1,9 @@ +[Unit] +After= +After=vyos-router.service + +[Service] +WorkingDirectory= +WorkingDirectory=/run/openvpn +ExecStart= +ExecStart=/usr/sbin/openvpn --daemon openvpn-%i --config %i.conf --status %i.status 30 --writepid %i.pid diff --git a/src/etc/systemd/system/pdns-recursor.service.d/override.conf b/src/etc/systemd/system/pdns-recursor.service.d/override.conf new file mode 100644 index 000000000..602d7b774 --- /dev/null +++ b/src/etc/systemd/system/pdns-recursor.service.d/override.conf @@ -0,0 +1,5 @@ +[Service] +WorkingDirectory= +WorkingDirectory=/run/powerdns +ExecStart= +ExecStart=/usr/sbin/pdns_recursor --daemon=no --write-pid=no --disable-syslog --log-timestamp=no --config-dir=/run/powerdns diff --git a/src/etc/systemd/system/wpa_supplicant@.service.d/override.conf b/src/etc/systemd/system/wpa_supplicant@.service.d/override.conf new file mode 100644 index 000000000..20b25b726 --- /dev/null +++ b/src/etc/systemd/system/wpa_supplicant@.service.d/override.conf @@ -0,0 +1,10 @@ +[Unit] +After= +After=vyos-router.service + +[Service] +WorkingDirectory= +WorkingDirectory=/run/wpa_supplicant +EnvironmentFile= +ExecStart= +ExecStart=/sbin/wpa_supplicant -c%I.conf -Dnl80211,wext -i%I diff --git a/src/helpers/validate-value.py b/src/helpers/validate-value.py index fab6ca81e..a58ba61d1 100755 --- a/src/helpers/validate-value.py +++ b/src/helpers/validate-value.py @@ -5,7 +5,7 @@ import os import sys import argparse -from vyos.util import run +from vyos.util import call parser = argparse.ArgumentParser() parser.add_argument('--regex', action='append') @@ -33,7 +33,7 @@ try: cmd = "{0} {1}".format(cmd, args.value) if debug: print(cmd) - res = run(cmd) + res = call(cmd) if res == 0: sys.exit(0) except Exception as exn: diff --git a/src/helpers/vyos-merge-config.py b/src/helpers/vyos-merge-config.py index 6546c03e3..14df2734b 100755 --- a/src/helpers/vyos-merge-config.py +++ b/src/helpers/vyos-merge-config.py @@ -23,7 +23,7 @@ import vyos.remote from vyos.config import Config from vyos.configtree import ConfigTree from vyos.migrator import Migrator, VirtualMigrator -from vyos.util import cmd +from vyos.util import cmd, DEVNULL if (len(sys.argv) < 2): @@ -99,9 +99,9 @@ if (len(sys.argv) > 2): if path: add_cmds = [ cmd for cmd in add_cmds if path in cmd ] -for cmd in add_cmds: +for add in add_cmds: try: - cmd(f'/opt/vyatta/sbin/my_{cmd}', message='Called process error') + cmd(f'/opt/vyatta/sbin/my_{add}', shell=True, stderr=DEVNULL) except OSError as err: print(err) diff --git a/src/migration-scripts/interfaces/7-to-8 b/src/migration-scripts/interfaces/7-to-8 index 78bd2781b..8830ffdc7 100755 --- a/src/migration-scripts/interfaces/7-to-8 +++ b/src/migration-scripts/interfaces/7-to-8 @@ -14,7 +14,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# Remove network provider name from CLI and rather use provider APN from CLI +# Split WireGuard endpoint into address / port nodes to make use of common +# validators from sys import exit, argv from vyos.configtree import ConfigTree diff --git a/src/migration-scripts/interfaces/8-to-9 b/src/migration-scripts/interfaces/8-to-9 new file mode 100755 index 000000000..e0b9dd375 --- /dev/null +++ b/src/migration-scripts/interfaces/8-to-9 @@ -0,0 +1,52 @@ +#!/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/>. + +# Rename link nodes to source-interface for the following interface types: +# - vxlan +# - pseudo ethernet + +from sys import exit, argv +from vyos.configtree import ConfigTree + +if __name__ == '__main__': + if (len(argv) < 1): + print("Must specify file name!") + exit(1) + + file_name = argv[1] + with open(file_name, 'r') as f: + config_file = f.read() + + config = ConfigTree(config_file) + + for if_type in ['vxlan', 'pseudo-ethernet']: + base = ['interfaces', if_type] + if not config.exists(base): + # Nothing to do + exit(0) + + # list all individual interface isntance + for i in config.list_nodes(base): + iface = base + [i] + if config.exists(iface + ['link']): + config.rename(iface + ['link'], 'source-interface') + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/l2tp/2-to-3 b/src/migration-scripts/l2tp/2-to-3 new file mode 100755 index 000000000..bd0839e03 --- /dev/null +++ b/src/migration-scripts/l2tp/2-to-3 @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# - remove primary/secondary identifier from nameserver +# - TODO: remove radius server req-limit + +import os +import sys + +from sys import argv, exit +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['vpn', 'l2tp', 'remote-access'] +if not config.exists(base): + # Nothing to do + exit(0) +else: + + # Migrate IPv4 DNS servers + dns_base = base + ['dns-servers'] + if config.exists(dns_base): + for server in ['server-1', 'server-2']: + if config.exists(dns_base + [server]): + dns = config.return_value(dns_base + [server]) + config.set(base + ['name-server'], value=dns, replace=False) + + config.delete(dns_base) + + # Migrate IPv6 DNS servers + dns_base = base + ['dnsv6-servers'] + if config.exists(dns_base): + for server in config.return_values(dns_base): + config.set(base + ['name-server'], value=server, replace=False) + + config.delete(dns_base) + + # Migrate IPv4 WINS servers + wins_base = base + ['wins-servers'] + if config.exists(wins_base): + for server in ['server-1', 'server-2']: + if config.exists(wins_base + [server]): + wins = config.return_value(wins_base + [server]) + config.set(base + ['wins-server'], value=wins, replace=False) + + config.delete(wins_base) + + + # Remove RADIUS server req-limit node + radius_base = base + ['authentication', 'radius'] + if config.exists(radius_base): + for server in config.list_nodes(radius_base + ['server']): + if config.exists(radius_base + ['server', server, 'req-limit']): + config.delete(radius_base + ['server', server, 'req-limit']) + + # Migrate IPv6 prefixes + ipv6_base = base + ['client-ipv6-pool'] + if config.exists(ipv6_base + ['prefix']): + prefix_old = config.return_values(ipv6_base + ['prefix']) + # delete old prefix CLI nodes + config.delete(ipv6_base + ['prefix']) + # create ned prefix tag node + config.set(ipv6_base + ['prefix']) + config.set_tag(ipv6_base + ['prefix']) + + for p in prefix_old: + prefix = p.split(',')[0] + mask = p.split(',')[1] + config.set(ipv6_base + ['prefix', prefix, 'mask'], value=mask) + + if config.exists(ipv6_base + ['delegate-prefix']): + prefix_old = config.return_values(ipv6_base + ['delegate-prefix']) + # delete old delegate prefix CLI nodes + config.delete(ipv6_base + ['delegate-prefix']) + # create ned delegation tag node + config.set(ipv6_base + ['delegate ']) + config.set_tag(ipv6_base + ['delegate ']) + + for p in prefix_old: + prefix = p.split(',')[0] + mask = p.split(',')[1] + config.set(ipv6_base + ['delegate', prefix, 'mask'], value=mask) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/op_mode/connect_disconnect.py b/src/op_mode/connect_disconnect.py index 192fd80ec..b191f630d 100755 --- a/src/op_mode/connect_disconnect.py +++ b/src/op_mode/connect_disconnect.py @@ -21,7 +21,7 @@ from sys import exit from psutil import process_iter from time import strftime, localtime, time -from vyos.util import run +from vyos.util import call PPP_LOGFILE = '/var/log/vyatta/ppp_{}.log' @@ -59,7 +59,7 @@ def connect(interface): tm = strftime("%a %d %b %Y %I:%M:%S %p %Z", localtime(time())) with open(PPP_LOGFILE.format(interface), 'a') as f: f.write('{}: user {} started PPP daemon for {} by connect command\n'.format(tm, user, interface)) - run('umask 0; setsid sh -c "nohup /usr/sbin/pppd call {0} > /tmp/{0}.log 2>&1 &"'.format(interface)) + call('umask 0; setsid sh -c "nohup /usr/sbin/pppd call {0} > /tmp/{0}.log 2>&1 &"'.format(interface)) def disconnect(interface): @@ -77,7 +77,7 @@ def disconnect(interface): tm = strftime("%a %d %b %Y %I:%M:%S %p %Z", localtime(time())) with open(PPP_LOGFILE.format(interface), 'a') as f: f.write('{}: user {} stopped PPP daemon for {} by disconnect command\n'.format(tm, user, interface)) - run('/usr/bin/poff "{}"'.format(interface)) + call('/usr/bin/poff "{}"'.format(interface)) def main(): parser = argparse.ArgumentParser() diff --git a/src/op_mode/dns_forwarding_reset.py b/src/op_mode/dns_forwarding_reset.py index 93c2444b9..8e2ee546c 100755 --- a/src/op_mode/dns_forwarding_reset.py +++ b/src/op_mode/dns_forwarding_reset.py @@ -21,12 +21,11 @@ import os -import sys import argparse -import vyos.config -from vyos.util import run - +from sys import exit +from vyos.config import Config +from vyos.util import call parser = argparse.ArgumentParser() parser.add_argument("-a", "--all", action="store_true", help="Reset all cache") @@ -36,16 +35,18 @@ if __name__ == '__main__': args = parser.parse_args() # Do nothing if service is not configured - c = vyos.config.Config() - if not c.exists_effective('service dns forwarding'): + c = Config() + if not c.exists_effective(['service', 'dns', 'forwarding']): print("DNS forwarding is not configured") - sys.exit(0) + exit(0) if args.all: - run("rec_control wipe-cache \'.$\'") - sys.exit(1) + call("rec_control wipe-cache \'.$\'") + exit(0) + elif args.domain: - run("rec_control wipe-cache \'{0}$\'".format(args.domain)) + call("rec_control wipe-cache \'{0}$\'".format(args.domain)) + else: parser.print_help() - sys.exit(1) + exit(1) diff --git a/src/op_mode/dns_forwarding_restart.sh b/src/op_mode/dns_forwarding_restart.sh index 8e556f2f0..64cc92115 100755 --- a/src/op_mode/dns_forwarding_restart.sh +++ b/src/op_mode/dns_forwarding_restart.sh @@ -2,7 +2,7 @@ if cli-shell-api existsEffective service dns forwarding; then echo "Restarting the DNS forwarding service" - systemctl restart pdns-recursor + systemctl restart pdns-recursor.service else echo "DNS forwarding is not configured" fi diff --git a/src/op_mode/dynamic_dns.py b/src/op_mode/dynamic_dns.py index d991848ad..e4e5043d5 100755 --- a/src/op_mode/dynamic_dns.py +++ b/src/op_mode/dynamic_dns.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -21,10 +21,9 @@ import sys import time from vyos.config import Config -from vyos.util import run +from vyos.util import call - -cache_file = r'/var/cache/ddclient/ddclient.cache' +cache_file = r'/run/ddclient/ddclient.cache' OUT_TMPL_SRC = """ {%- for entry in hosts -%} @@ -86,9 +85,9 @@ def show_status(): def update_ddns(): - run('systemctl stop ddclient') + call('systemctl stop ddclient.service') os.remove(cache_file) - run('systemctl start ddclient') + call('systemctl start ddclient.service') def main(): diff --git a/src/op_mode/flow_accounting_op.py b/src/op_mode/flow_accounting_op.py index 7f3ad7476..bf8c39fd6 100755 --- a/src/op_mode/flow_accounting_op.py +++ b/src/op_mode/flow_accounting_op.py @@ -70,13 +70,13 @@ def _is_host(host): # check if flow-accounting running def _uacctd_running(): - command = '/usr/bin/sudo /bin/systemctl status uacctd > /dev/null' + command = 'systemctl status uacctd.service > /dev/null' return run(command) == 0 # get list of interfaces def _get_ifaces_dict(): # run command to get ifaces list - out = cmd('/bin/ip link show', universal_newlines=True) + out = cmd('/bin/ip link show') # read output ifaces_out = out.splitlines() @@ -95,7 +95,6 @@ def _get_ifaces_dict(): def _get_flows_list(): # run command to get flows list out = cmd(f'/usr/bin/pmacct -s -O json -T flows -p {uacctd_pipefile}', - universal_newlines=True, message='Failed to get flows list') # read output @@ -196,7 +195,7 @@ if not _uacctd_running(): # restart pmacct daemon if cmd_args.action == 'restart': # run command to restart flow-accounting - cmd('/usr/bin/sudo /bin/systemctl restart uacctd', + cmd('systemctl restart uacctd.service', message='Failed to restart flow-accounting') # clear in-memory collected flows diff --git a/src/op_mode/format_disk.py b/src/op_mode/format_disk.py index 9d3797f17..df4486bce 100755 --- a/src/op_mode/format_disk.py +++ b/src/op_mode/format_disk.py @@ -22,7 +22,9 @@ from datetime import datetime from time import sleep from vyos.util import is_admin, ask_yes_no -from vyos.util import run, cmd, DEVNULL +from vyos.util import call +from vyos.util import cmd +from vyos.util import DEVNULL def list_disks(): disks = set() @@ -36,7 +38,7 @@ def list_disks(): def is_busy(disk: str): """Check if given disk device is busy by re-reading it's partition table""" - return run(f'sudo blockdev --rereadpt /dev/{disk}', stderr=DEVNULL) != 0 + return call(f'sudo blockdev --rereadpt /dev/{disk}', stderr=DEVNULL) != 0 def backup_partitions(disk: str): diff --git a/src/op_mode/generate_ssh_server_key.py b/src/op_mode/generate_ssh_server_key.py index f65d383c0..cbc9ef973 100755 --- a/src/op_mode/generate_ssh_server_key.py +++ b/src/op_mode/generate_ssh_server_key.py @@ -14,14 +14,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import sys - +from sys import exit from vyos.util import ask_yes_no from vyos.util import cmd if not ask_yes_no('Do you really want to remove the existing SSH host keys?'): - sys.exit(0) + exit(0) -cmd('sudo rm -v /etc/ssh/ssh_host_*') -cmd('sudo dpkg-reconfigure openssh-server') -cmd('sudo systemctl restart ssh') +cmd('rm -v /etc/ssh/ssh_host_*') +cmd('dpkg-reconfigure openssh-server') +cmd('systemctl restart ssh.service') diff --git a/src/op_mode/lldp_op.py b/src/op_mode/lldp_op.py index c8a5543b6..5d48e3210 100755 --- a/src/op_mode/lldp_op.py +++ b/src/op_mode/lldp_op.py @@ -23,6 +23,7 @@ from sys import exit from tabulate import tabulate from vyos.util import popen +from vyos.config import Config parser = argparse.ArgumentParser() parser.add_argument("-a", "--all", action="store_true", help="Show LLDP neighbors on all interfaces") @@ -141,6 +142,11 @@ if __name__ == '__main__': args = parser.parse_args() tmp = { 'neighbors' : [] } + c = Config() + if not c.exists_effective(['service', 'lldp']): + print('Service LLDP is not configured') + exit(0) + if args.all: neighbors = minidom.parseString(_get_neighbors()) for neighbor in neighbors.getElementsByTagName('interface'): diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py index 772bb8198..4ab91384b 100755 --- a/src/op_mode/powerctrl.py +++ b/src/op_mode/powerctrl.py @@ -21,7 +21,10 @@ import re from datetime import datetime, timedelta, time as type_time, date as type_date from vyos.util import ask_yes_no -from vyos.util import cmd, run +from vyos.util import cmd +from vyos.util import call +from vyos.util import run +from vyos.util import STDOUT systemd_sched_file = "/run/systemd/shutdown/scheduled" @@ -95,14 +98,14 @@ def execute_shutdown(time, reboot = True, ask=True): chk_vyatta_based_reboots() ### - out = cmd(f'/sbin/shutdown {action} now') + out = cmd(f'/sbin/shutdown {action} now', stderr=STDOUT) print(out.split(",",1)[0]) return elif len(time) == 1: # Assume the argument is just time ts = parse_time(time[0]) if ts: - cmd(f'/sbin/shutdown {action} {time[0]}') + cmd(f'/sbin/shutdown {action} {time[0]}', stderr=STDOUT) else: sys.exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0])) elif len(time) == 2: @@ -113,7 +116,7 @@ def execute_shutdown(time, reboot = True, ask=True): t = datetime.combine(ds, ts) td = t - datetime.now() t2 = 1 + int(td.total_seconds())//60 # Get total minutes - cmd('/sbin/shutdown {action} {t2}') + cmd('/sbin/shutdown {action} {t2}', stderr=STDOUT) else: if not ts: sys.exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0])) @@ -132,7 +135,7 @@ def chk_vyatta_based_reboots(): if os.path.exists(f): jid = open(f).read().strip() if jid != 0: - run(f'sudo atrm {jid}') + call(f'sudo atrm {jid}') os.remove(f) def main(): diff --git a/src/op_mode/reset_openvpn.py b/src/op_mode/reset_openvpn.py index 618cad5ea..dbd3eb4d1 100755 --- a/src/op_mode/reset_openvpn.py +++ b/src/op_mode/reset_openvpn.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,57 +14,18 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import sys import os - -from time import sleep -from netifaces import interfaces -from vyos.util import process_running, cmd - -def get_config_name(intf): - cfg_file = r'/opt/vyatta/etc/openvpn/openvpn-{}.conf'.format(intf) - return cfg_file - -def get_pid_file(intf): - pid_file = r'/var/run/openvpn/{}.pid'.format(intf) - return pid_file - +from sys import argv, exit +from vyos.util import call if __name__ == '__main__': - if (len(sys.argv) < 1): - print("Must specify OpenVPN interface name!") - sys.exit(1) - - interface = sys.argv[1] - if os.path.isfile(get_config_name(interface)): - pidfile = '/var/run/openvpn/{}.pid'.format(interface) - if process_running(pidfile): - command = 'start-stop-daemon' - command += ' --stop' - command += ' --oknodo' - command += ' --quiet' - command += ' --pidfile ' + pidfile - cmd(command) - - # When stopping OpenVPN we need to wait for the 'old' interface to - # vanish from the Kernel, if it is not gone, OpenVPN will report: - # ERROR: Cannot ioctl TUNSETIFF vtun10: Device or resource busy (errno=16) - while interface in interfaces(): - sleep(0.250) # 250ms - - # re-start OpenVPN process - command = 'start-stop-daemon' - command += ' --start' - command += ' --oknodo' - command += ' --quiet' - command += ' --pidfile ' + get_pid_file(interface) - command += ' --exec /usr/sbin/openvpn' - # now pass arguments to openvpn binary - command += ' --' - command += ' --daemon openvpn-' + interface - command += ' --config ' + get_config_name(interface) + if (len(argv) < 1): + print('Must specify OpenVPN interface name!') + exit(1) - cmd(command) + interface = argv[1] + if os.path.isfile(f'/run/openvpn/{interface}.conf'): + call(f'systemctl restart openvpn@{interface}.service') else: - print("OpenVPN interface {} does not exist!".format(interface)) - sys.exit(1) + print(f'OpenVPN interface "{interface}" does not exist!') + exit(1) diff --git a/src/op_mode/reset_vpn.py b/src/op_mode/reset_vpn.py index b47212f88..3a0ad941c 100755 --- a/src/op_mode/reset_vpn.py +++ b/src/op_mode/reset_vpn.py @@ -14,63 +14,49 @@ # 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 argparse -#import re -from vyos.util import run, DEVNULL +from vyos.util import run -pptp_base = '/usr/bin/accel-cmd -p 2003 terminate {} {}' -l2tp_base = '/usr/bin/accel-cmd -p 2004 terminate {} {}' +cmd_dict = { + 'cmd_base' : '/usr/bin/accel-cmd -p {} terminate {} {}', + 'vpn_types' : { + 'pptp' : 2003, + 'l2tp' : 2004, + 'sstp' : 2005 + } +} def terminate_sessions(username='', interface='', protocol=''): - if username: - if username == "all_users": - if protocol == "pptp": - pptp_cmd = pptp_base.format('all','') - run(pptp_cmd, stdout=DEVNULL, stderr=DEVNULL) - return - elif protocol == "l2tp": - l2tp_cmd = l2tp_base.format('all', '') - run(l2tp_cmd, stdout=DEVNULL, stderr=DEVNULL) - return - else: - pptp_cmd = pptp_base.format('all', '') - run(pptp_cmd, stdout=DEVNULL, stderr=DEVNULL) - l2tp_cmd = l2tp_base.format('all', '') - run(l2tp_cmd, stdout=DEVNULL, stderr=DEVNULL) - return - if protocol == "pptp": - pptp_cmd = pptp_base.format('username', username) - run(pptp_cmd, stdout=DEVNULL, stderr=DEVNULL) - return - elif protocol == "l2tp": - l2tp_cmd = l2tp_base.format('username', username) - run(l2tp_cmd, stdout=DEVNULL, stderr=DEVNULL) - return + # Reset vpn connections by username + if protocol in cmd_dict['vpn_types']: + if username == "all_users": + run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][protocol], 'all', '')) else: - pptp_cmd = pptp_base.format('username', username) - run(pptp_cmd, stdout=DEVNULL, stderr=DEVNULL) - l2tp_cmd.append("terminate username {0}".format(username)) - run(l2tp_cmd, stdout=DEVNULL, stderr=DEVNULL) - return + run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][protocol], 'username', username)) + + # Reset vpn connections by ifname + elif interface: + for proto in cmd_dict['vpn_types']: + run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][proto], 'if', interface)) - # rewrite `terminate by interface` if pptp will have pptp%d interface naming - if interface: - pptp_cmd = pptp_base.format('if', interface) - run(pptp_cmd, stdout=DEVNULL, stderr=DEVNULL) - l2tp_cmd = l2tp_base.format('if', interface) - run(l2tp_cmd, stdout=DEVNULL, stderr=DEVNULL) - + elif username: + # Reset all vpn connections + if username == "all_users": + for proto in cmd_dict['vpn_types']: + run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][proto], 'all', '')) + else: + for proto in cmd_dict['vpn_types']: + run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][proto], 'username', username)) def main(): #parese args parser = argparse.ArgumentParser() parser.add_argument('--username', help='Terminate by username (all_users used for disconnect all users)', required=False) parser.add_argument('--interface', help='Terminate by interface', required=False) - parser.add_argument('--protocol', help='Set protocol (pptp|l2tp)', required=False) + parser.add_argument('--protocol', help='Set protocol (pptp|l2tp|sstp)', required=False) args = parser.parse_args() if args.username or args.interface: diff --git a/src/op_mode/restart_dhcp_relay.py b/src/op_mode/restart_dhcp_relay.py index 057b4dcd8..af4fb2d15 100755 --- a/src/op_mode/restart_dhcp_relay.py +++ b/src/op_mode/restart_dhcp_relay.py @@ -23,7 +23,7 @@ import argparse import os import vyos.config -from vyos.util import run +from vyos.util import call parser = argparse.ArgumentParser() @@ -39,7 +39,7 @@ if __name__ == '__main__': if not c.exists_effective('service dhcp-relay'): print("DHCP relay service not configured") else: - run('sudo systemctl restart isc-dhcp-relay.service') + call('systemctl restart isc-dhcp-server.service') sys.exit(0) elif args.ipv6: @@ -47,7 +47,7 @@ if __name__ == '__main__': if not c.exists_effective('service dhcpv6-relay'): print("DHCPv6 relay service not configured") else: - run('sudo systemctl restart isc-dhcpv6-relay.service') + call('systemctl restart isc-dhcp-server6.service') sys.exit(0) else: diff --git a/src/op_mode/restart_frr.py b/src/op_mode/restart_frr.py index 6304e72db..d1b66b33f 100755 --- a/src/op_mode/restart_frr.py +++ b/src/op_mode/restart_frr.py @@ -22,7 +22,7 @@ from logging.handlers import SysLogHandler from pathlib import Path import psutil -from vyos.util import run +from vyos.util import call # some default values watchfrr = '/usr/lib/frr/watchfrr.sh' @@ -87,7 +87,7 @@ def _write_config(): Path(frrconfig_tmp).mkdir(parents=False, exist_ok=True) # save frr.conf to it command = "{} -n -w --config_dir {} 2> /dev/null".format(vtysh, frrconfig_tmp) - return_code = run(command) + return_code = call(command) if not return_code == 0: logger.error("Failed to save active config: \"{}\" returned exit code: {}".format(command, return_code)) return False @@ -109,7 +109,7 @@ def _cleanup(): # check if daemon is running def _daemon_check(daemon): command = "{} print_status {}".format(watchfrr, daemon) - return_code = run(command) + return_code = call(command) if not return_code == 0: logger.error("Daemon \"{}\" is not running".format(daemon)) return False @@ -120,7 +120,7 @@ def _daemon_check(daemon): # restart daemon def _daemon_restart(daemon): command = "{} restart {}".format(watchfrr, daemon) - return_code = run(command) + return_code = call(command) if not return_code == 0: logger.error("Failed to restart daemon \"{}\"".format(daemon)) return False @@ -136,7 +136,7 @@ def _reload_config(daemon): else: command = "{} -n -b --config_dir {} 2> /dev/null".format(vtysh, frrconfig_tmp) - return_code = run(command) + return_code = call(command) if not return_code == 0: logger.error("Failed to reinstall configuration") return False diff --git a/src/op_mode/show_acceleration.py b/src/op_mode/show_acceleration.py index 05d3d8906..6d44b0f66 100755 --- a/src/op_mode/show_acceleration.py +++ b/src/op_mode/show_acceleration.py @@ -21,7 +21,8 @@ import re import argparse from vyos.config import Config -from vyos.util import popen, run +from vyos.util import popen +from vyos.util import call def detect_qat_dev(): @@ -43,7 +44,7 @@ def show_qat_status(): sys.exit(1) # Show QAT service - run('sudo /etc/init.d/vyos-qat-utilities status') + call('sudo /etc/init.d/vyos-qat-utilities status') # Return QAT devices def get_qat_devices(): @@ -94,20 +95,20 @@ args = parser.parse_args() if args.hw: detect_qat_dev() # Show availible Intel QAT devices - run('sudo lspci -nn | egrep -e \'8086:37c8|8086:19e2|8086:0435|8086:6f54\'') + call('sudo lspci -nn | egrep -e \'8086:37c8|8086:19e2|8086:0435|8086:6f54\'') elif args.flow and args.dev: check_qat_if_conf() - run('sudo cat '+get_qat_proc_path(args.dev)+"fw_counters") + call('sudo cat '+get_qat_proc_path(args.dev)+"fw_counters") elif args.interrupts: check_qat_if_conf() # Delete _dev from args.dev - run('sudo cat /proc/interrupts | grep qat') + call('sudo cat /proc/interrupts | grep qat') elif args.status: check_qat_if_conf() show_qat_status() elif args.conf and args.dev: check_qat_if_conf() - run('sudo cat '+get_qat_proc_path(args.dev)+"dev_cfg") + call('sudo cat '+get_qat_proc_path(args.dev)+"dev_cfg") elif args.dev_list: get_qat_devices() else: diff --git a/src/op_mode/show_dhcp.py b/src/op_mode/show_dhcp.py index 4e3e08263..c49e604b7 100755 --- a/src/op_mode/show_dhcp.py +++ b/src/op_mode/show_dhcp.py @@ -27,7 +27,7 @@ from datetime import datetime from isc_dhcp_leases import Lease, IscDhcpLeases from vyos.config import Config -from vyos.util import run +from vyos.util import call lease_file = "/config/dhcpd.leases" @@ -193,7 +193,7 @@ if __name__ == '__main__': sys.exit(0) # if dhcp server is down, inactive leases may still be shown as active, so warn the user. - if run('systemctl -q is-active isc-dhcpv4-server.service') != 0: + if call('systemctl -q is-active isc-dhcp-server.service') != 0: print("WARNING: DHCP server is configured but not started. Data may be stale.") if args.leases: diff --git a/src/op_mode/show_dhcpv6.py b/src/op_mode/show_dhcpv6.py index 4ef4849ff..d686defc0 100755 --- a/src/op_mode/show_dhcpv6.py +++ b/src/op_mode/show_dhcpv6.py @@ -27,7 +27,7 @@ from datetime import datetime from isc_dhcp_leases import Lease, IscDhcpLeases from vyos.config import Config -from vyos.util import run +from vyos.util import call lease_file = "/config/dhcpdv6.leases" pool_key = "shared-networkname" @@ -179,7 +179,7 @@ if __name__ == '__main__': sys.exit(0) # if dhcp server is down, inactive leases may still be shown as active, so warn the user. - if run('systemctl -q is-active isc-dhcpv6-server.service') != 0: + if call('systemctl -q is-active isc-dhcp-server6.service') != 0: print("WARNING: DHCPv6 server is configured but not started. Data may be stale.") if args.leases: diff --git a/src/op_mode/snmp.py b/src/op_mode/snmp.py index b09eab97f..5fae67881 100755 --- a/src/op_mode/snmp.py +++ b/src/op_mode/snmp.py @@ -24,7 +24,7 @@ import sys import argparse from vyos.config import Config -from vyos.util import run +from vyos.util import call config_file_daemon = r'/etc/snmp/snmpd.conf' @@ -54,7 +54,7 @@ def show_all(): def show_community(c, h): print('Status of SNMP community {0} on {1}'.format(c, h), flush=True) - run('/usr/bin/snmpstatus -t1 -v1 -c {0} {1}'.format(c, h)) + call('/usr/bin/snmpstatus -t1 -v1 -c {0} {1}'.format(c, h)) if __name__ == '__main__': args = parser.parse_args() diff --git a/src/op_mode/version.py b/src/op_mode/version.py index 34eca44b1..8599c958f 100755 --- a/src/op_mode/version.py +++ b/src/op_mode/version.py @@ -30,7 +30,10 @@ import pystache import vyos.version import vyos.limericks -from vyos.util import cmd, run +from vyos.util import cmd +from vyos.util import call +from vyos.util import run +from vyos.util import DEVNULL parser = argparse.ArgumentParser() @@ -80,7 +83,7 @@ if __name__ == '__main__': # Get hypervisor name, if any system_type = "bare metal" try: - hypervisor = cmd('hvinfo 2>/dev/null') + hypervisor = cmd('hvinfo',stderr=DEVNULL) system_type = "{0} guest".format(hypervisor) except OSError: # hvinfo returns 1 if it cannot detect any hypervisor @@ -119,7 +122,7 @@ if __name__ == '__main__': if args.all: print("Package versions:") - run("dpkg -l") + call("dpkg -l") if args.funny: print(vyos.limericks.get_random()) diff --git a/src/op_mode/wireguard.py b/src/op_mode/wireguard.py index d940d79eb..1b90f4fa7 100755 --- a/src/op_mode/wireguard.py +++ b/src/op_mode/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,8 +13,6 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# import argparse import os @@ -27,7 +25,7 @@ from vyos.ifconfig import WireGuardIf from vyos import ConfigError from vyos.config import Config -from vyos.util import run +from vyos.util import cmd, run dir = r'/config/auth/wireguard' psk = dir + '/preshared.key' @@ -88,10 +86,11 @@ def genpsk(): it's stored only in the cli config """ - run('wg genpsk') + psk = cmd('wg genpsk') + print(psk) def list_key_dirs(): - """ lists all dirs under /config/auth/wireguard """ + """ lists all dirs under /config/auth/wireguard """ if os.path.exists(dir): nks = next(os.walk(dir))[1] for nk in nks: diff --git a/src/system/keepalived-fifo.py b/src/system/keepalived-fifo.py index 2778deaab..7e2076820 100755 --- a/src/system/keepalived-fifo.py +++ b/src/system/keepalived-fifo.py @@ -87,7 +87,7 @@ class KeepalivedFifo: def _run_command(self, command): logger.debug("Running the command: {}".format(command)) try: - cmd(command, universal_newlines=True) + cmd(command) except OSError as err: logger.error(f'Unable to execute command "{command}": {err}') diff --git a/src/systemd/accel-ppp@.service b/src/systemd/accel-ppp@.service new file mode 100644 index 000000000..256112769 --- /dev/null +++ b/src/systemd/accel-ppp@.service @@ -0,0 +1,16 @@ +[Unit] +Description=Accel-PPP - High performance VPN server application for Linux +RequiresMountsFor=/run +ConditionPathExists=/run/accel-pppd/%i.conf +After=vyos-router.service + +[Service] +WorkingDirectory=/run/accel-pppd +ExecStart=/usr/sbin/accel-pppd -d -p /run/accel-pppd/%i.pid -c /run/accel-pppd/%i.conf +ExecReload=/bin/kill -SIGUSR1 $MAINPID +PIDFile=/run/accel-pppd/%i.pid +Type=forking +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/ddclient.service b/src/systemd/ddclient.service new file mode 100644 index 000000000..a4d55827a --- /dev/null +++ b/src/systemd/ddclient.service @@ -0,0 +1,14 @@ +[Unit] +Description=Dynamic DNS Update Client +RequiresMountsFor=/run +ConditionPathExists=/run/ddclient/ddclient.conf +After=vyos-router.service + +[Service] +WorkingDirectory=/run/ddclient +Type=forking +PIDFile=/run/ddclient/ddclient.pid +ExecStart=/usr/sbin/ddclient -cache /run/ddclient/ddclient.cache -pid /run/ddclient/ddclient.pid -file /run/ddclient/ddclient.conf + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/isc-dhcp-relay.service b/src/systemd/isc-dhcp-relay.service new file mode 100644 index 000000000..ebf4d234e --- /dev/null +++ b/src/systemd/isc-dhcp-relay.service @@ -0,0 +1,14 @@ +[Unit] +Description=ISC DHCP IPv4 relay +Documentation=man:dhcrelay(8) +Wants=network-online.target +ConditionPathExists=/run/dhcp-relay/dhcp.conf +After=vyos-router.service + +[Service] +WorkingDirectory=/run/dhcp-relay +EnvironmentFile=/run/dhcp-relay/dhcp.conf +ExecStart=/usr/sbin/dhcrelay -d -4 $OPTIONS + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/isc-dhcp-relay6.service b/src/systemd/isc-dhcp-relay6.service new file mode 100644 index 000000000..a477618b1 --- /dev/null +++ b/src/systemd/isc-dhcp-relay6.service @@ -0,0 +1,14 @@ +[Unit] +Description=ISC DHCP IPv6 relay +Documentation=man:dhcrelay(8) +Wants=network-online.target +ConditionPathExists=/run/dhcp-relay/dhcpv6.conf +After=vyos-router.service + +[Service] +WorkingDirectory=/run/dhcp-relay +EnvironmentFile=/run/dhcp-relay/dhcpv6.conf +ExecStart=/usr/sbin/dhcrelay -d -6 $OPTIONS + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/isc-dhcp-server.service b/src/systemd/isc-dhcp-server.service new file mode 100644 index 000000000..d848e3df1 --- /dev/null +++ b/src/systemd/isc-dhcp-server.service @@ -0,0 +1,19 @@ +[Unit] +Description=ISC DHCP IPv4 server +Documentation=man:dhcpd(8) +RequiresMountsFor=/run +ConditionPathExists=/run/dhcp-server/dhcpd.conf +After=vyos-router.service + +[Service] +WorkingDirectory=/run/dhcp-server +# The leases files need to be root:vyattacfg even when dropping privileges +ExecStart=/bin/sh -ec '\ + CONFIG_FILE=/run/dhcp-server/dhcpd.conf; \ + [ -e /config/dhcpd.leases ] || touch /config/dhcpd.leases; \ + chown root:vyattacfg /config/dhcpd.leases; \ + chmod 664 /config/dhcpd.leases; \ + exec /usr/sbin/dhcpd -user nobody -group nogroup -f -4 -pf /run/dhcp-server/dhcpd.pid -cf $CONFIG_FILE -lf /config/dhcpd.leases' + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/isc-dhcp-server6.service b/src/systemd/isc-dhcp-server6.service new file mode 100644 index 000000000..743f16840 --- /dev/null +++ b/src/systemd/isc-dhcp-server6.service @@ -0,0 +1,18 @@ +[Unit] +Description=ISC DHCP IPv6 server +Documentation=man:dhcpd(8) +RequiresMountsFor=/run +ConditionPathExists=/run/dhcp-server/dhcpd.conf +After=vyos-router.service + +[Service] +WorkingDirectory=/run/dhcp-server +# The leases files need to be root:vyattacfg even when dropping privileges +ExecStart=/bin/sh -ec '\ + [ -e /config/dhcpdv6.leases ] || touch /config/dhcpdv6.leases; \ + chown root:vyattacfg /config/dhcpdv6.leases; \ + chmod 664 /config/dhcpdv6.leases; \ + exec /usr/sbin/dhcpd -user nobody -group nogroup -f -6 -pf /run/dhcp-server/dhcpdv6.pid -cf /run/dhcp-server/dhcpdv6.conf -lf /config/dhcpdv6.leases' + +[Install] +WantedBy=multi-user.target diff --git a/src/etc/systemd/system/ppp@.service b/src/systemd/ppp@.service index d271efb41..bb4622034 100644 --- a/src/etc/systemd/system/ppp@.service +++ b/src/systemd/ppp@.service @@ -1,6 +1,6 @@ [Unit] Description=Dialing PPP connection %I -After=network.target +After=vyos-router.service [Service] ExecStart=/usr/sbin/pppd call %I nodetach nolog diff --git a/src/systemd/tftpd@.service b/src/systemd/tftpd@.service index e5c289466..266bc0962 100644 --- a/src/systemd/tftpd@.service +++ b/src/systemd/tftpd@.service @@ -1,6 +1,6 @@ [Unit] Description=TFTP server -After=network.target +After=vyos-router.service RequiresMountsFor=/run [Service] |