summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/conf_mode/dhcp_relay.py5
-rwxr-xr-xsrc/conf_mode/dhcp_server.py5
-rwxr-xr-xsrc/conf_mode/dhcpv6_relay.py5
-rwxr-xr-xsrc/conf_mode/dhcpv6_server.py150
-rwxr-xr-xsrc/conf_mode/dns_forwarding.py4
-rwxr-xr-xsrc/conf_mode/dynamic_dns.py4
-rwxr-xr-xsrc/conf_mode/host_name.py15
-rwxr-xr-xsrc/conf_mode/interfaces-bonding.py182
-rwxr-xr-xsrc/conf_mode/interfaces-bridge.py55
-rwxr-xr-xsrc/conf_mode/interfaces-dummy.py17
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py148
-rwxr-xr-xsrc/conf_mode/interfaces-geneve.py17
-rwxr-xr-xsrc/conf_mode/interfaces-l2tpv3.py55
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py111
-rwxr-xr-xsrc/conf_mode/interfaces-pppoe.py24
-rwxr-xr-xsrc/conf_mode/interfaces-pseudo-ethernet.py216
-rwxr-xr-xsrc/conf_mode/interfaces-tunnel.py28
-rwxr-xr-xsrc/conf_mode/interfaces-vxlan.py38
-rwxr-xr-xsrc/conf_mode/interfaces-wireguard.py34
-rwxr-xr-xsrc/conf_mode/interfaces-wireless.py65
-rwxr-xr-xsrc/conf_mode/interfaces-wirelessmodem.py48
-rwxr-xr-xsrc/conf_mode/ipsec-settings.py48
-rwxr-xr-xsrc/conf_mode/protocols_static_multicast.py115
-rwxr-xr-xsrc/conf_mode/salt-minion.py103
-rwxr-xr-xsrc/conf_mode/service-ipoe.py284
-rwxr-xr-xsrc/conf_mode/service-pppoe.py428
-rwxr-xr-xsrc/conf_mode/service_ipoe-server.py300
-rwxr-xr-xsrc/conf_mode/service_pppoe-server.py463
-rwxr-xr-xsrc/conf_mode/service_router-advert.py (renamed from src/conf_mode/service-router-advert.py)0
-rwxr-xr-xsrc/conf_mode/system-login.py11
-rwxr-xr-xsrc/conf_mode/vpn-pptp.py257
-rwxr-xr-xsrc/conf_mode/vpn_l2tp.py29
-rwxr-xr-xsrc/conf_mode/vpn_pptp.py279
-rwxr-xr-xsrc/conf_mode/vpn_sstp.py50
-rw-r--r--src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper8
-rw-r--r--src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup64
-rw-r--r--src/etc/dhcp/dhclient-exit-hooks.d/02-vyos-dhcp-renew-rfc3442148
-rw-r--r--src/etc/sysctl.d/31-vyos-addr_gen_mode.conf14
-rwxr-xr-xsrc/migration-scripts/dhcpv6-server/0-to-161
-rwxr-xr-xsrc/migration-scripts/dns-forwarding/1-to-271
-rwxr-xr-xsrc/migration-scripts/ipoe-server/0-to-1133
-rwxr-xr-xsrc/migration-scripts/l2tp/2-to-36
-rwxr-xr-xsrc/migration-scripts/pppoe-server/2-to-3142
-rwxr-xr-xsrc/migration-scripts/pptp/1-to-271
-rwxr-xr-xsrc/migration-scripts/salt/0-to-158
-rwxr-xr-xsrc/op_mode/dns_forwarding_statistics.py2
-rwxr-xr-xsrc/op_mode/powerctrl.py291
-rwxr-xr-xsrc/op_mode/show_dhcp.py91
-rwxr-xr-xsrc/op_mode/show_dhcpv6.py74
-rwxr-xr-xsrc/op_mode/show_interfaces.py37
-rwxr-xr-xsrc/op_mode/version.py93
-rwxr-xr-xsrc/op_mode/vrrp.py1
-rwxr-xr-xsrc/op_mode/wireguard.py8
-rwxr-xr-xsrc/services/vyos-http-api-server42
-rw-r--r--src/systemd/dhclient6@.service18
-rw-r--r--src/systemd/dhclient@.service18
-rw-r--r--src/systemd/isc-dhcp-server6.service2
-rwxr-xr-xsrc/validators/numeric6
58 files changed, 2920 insertions, 2132 deletions
diff --git a/src/conf_mode/dhcp_relay.py b/src/conf_mode/dhcp_relay.py
index ce0e01308..d24a46220 100755
--- a/src/conf_mode/dhcp_relay.py
+++ b/src/conf_mode/dhcp_relay.py
@@ -98,11 +98,6 @@ def generate(relay):
if not relay:
return None
- # 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
diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py
index da01f16eb..1849ece0a 100755
--- a/src/conf_mode/dhcp_server.py
+++ b/src/conf_mode/dhcp_server.py
@@ -594,11 +594,6 @@ def generate(dhcp):
if not dhcp or dhcp['disabled']:
return None
- # Create configuration directory on demand
- dirname = os.path.dirname(config_file)
- if not os.path.isdir(dirname):
- os.mkdir(dirname)
-
# Please see: https://phabricator.vyos.net/T1129 for quoting of the raw parameters
# we can pass to ISC DHCPd
render(config_file, 'dhcp-server/dhcpd.conf.tmpl', dhcp,
diff --git a/src/conf_mode/dhcpv6_relay.py b/src/conf_mode/dhcpv6_relay.py
index cb5a4bbfb..ecc739063 100755
--- a/src/conf_mode/dhcpv6_relay.py
+++ b/src/conf_mode/dhcpv6_relay.py
@@ -84,11 +84,6 @@ def generate(relay):
if relay is None:
return None
- # 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
diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py
index 94a307826..159d16401 100755
--- a/src/conf_mode/dhcpv6_server.py
+++ b/src/conf_mode/dhcpv6_server.py
@@ -23,7 +23,7 @@ from copy import deepcopy
from vyos.config import Config
from vyos.template import render
from vyos.util import call
-from vyos.validate import is_subnet_connected
+from vyos.validate import is_subnet_connected, is_ipv6
from vyos import ConfigError
config_file = r'/run/dhcp-server/dhcpdv6.conf'
@@ -37,24 +37,25 @@ default_config_data = {
def get_config():
dhcpv6 = deepcopy(default_config_data)
conf = Config()
- if not conf.exists('service dhcpv6-server'):
+ base = ['service', 'dhcpv6-server']
+ if not conf.exists(base):
return None
else:
- conf.set_level('service dhcpv6-server')
+ conf.set_level(base)
# Check for global disable of DHCPv6 service
- if conf.exists('disable'):
+ if conf.exists(['disable']):
dhcpv6['disabled'] = True
return dhcpv6
# Preference of this DHCPv6 server compared with others
- if conf.exists('preference'):
- dhcpv6['preference'] = conf.return_value('preference')
+ if conf.exists(['preference']):
+ dhcpv6['preference'] = conf.return_value(['preference'])
# check for multiple, shared networks served with DHCPv6 addresses
- if conf.exists('shared-network-name'):
- for network in conf.list_nodes('shared-network-name'):
- conf.set_level('service dhcpv6-server shared-network-name {0}'.format(network))
+ if conf.exists(['shared-network-name']):
+ for network in conf.list_nodes(['shared-network-name']):
+ conf.set_level(base + ['shared-network-name', network])
config = {
'name': network,
'disabled': False,
@@ -62,13 +63,13 @@ def get_config():
}
# If disabled, the shared-network configuration becomes inactive
- if conf.exists('disable'):
+ if conf.exists(['disable']):
config['disabled'] = True
# check for multiple subnet configurations in a shared network
- if conf.exists('subnet'):
- for net in conf.list_nodes('subnet'):
- conf.set_level('service dhcpv6-server shared-network-name {0} subnet {1}'.format(network, net))
+ if conf.exists(['subnet']):
+ for net in conf.list_nodes(['subnet']):
+ conf.set_level(base + ['shared-network-name', network, 'subnet', net])
subnet = {
'network': net,
'range6_prefix': [],
@@ -94,25 +95,25 @@ def get_config():
# least one address range statement. The range statement gives the lowest and highest
# IP addresses in a range. All IP addresses in the range should be in the subnet in
# which the range statement is declared.
- if conf.exists('address-range prefix'):
- for prefix in conf.list_nodes('address-range prefix'):
+ if conf.exists(['address-range', 'prefix']):
+ for prefix in conf.list_nodes(['address-range', 'prefix']):
range = {
'prefix': prefix,
'temporary': False
}
# Address range will be used for temporary addresses
- if conf.exists('address-range prefix {0} temporary'.format(range['prefix'])):
+ if conf.exists(['address-range' 'prefix', prefix, 'temporary']):
range['temporary'] = True
# Append to subnet temporary range6 list
subnet['range6_prefix'].append(range)
- if conf.exists('address-range start'):
- for range in conf.list_nodes('address-range start'):
+ if conf.exists(['address-range', 'start']):
+ for range in conf.list_nodes(['address-range', 'start']):
range = {
'start': range,
- 'stop': conf.return_value('address-range start {0} stop'.format(range))
+ 'stop': conf.return_value(['address-range', 'start', range, 'stop'])
}
# Append to subnet range6 list
@@ -120,70 +121,68 @@ def get_config():
# The domain-search option specifies a 'search list' of Domain Names to be used
# by the client to locate not-fully-qualified domain names.
- if conf.exists('domain-search'):
- for domain in conf.return_values('domain-search'):
- subnet['domain_search'].append('"' + domain + '"')
+ if conf.exists(['domain-search']):
+ subnet['domain_search'] = conf.return_values(['domain-search'])
# IPv6 address valid lifetime
# (at the end the address is no longer usable by the client)
# (set to 30 days, the usual IPv6 default)
- if conf.exists('lease-time default'):
- subnet['lease_def'] = conf.return_value('lease-time default')
+ if conf.exists(['lease-time', 'default']):
+ subnet['lease_def'] = conf.return_value(['lease-time', 'default'])
# Time should be the maximum length in seconds that will be assigned to a lease.
# The only exception to this is that Dynamic BOOTP lease lengths, which are not
# specified by the client, are not limited by this maximum.
- if conf.exists('lease-time maximum'):
- subnet['lease_max'] = conf.return_value('lease-time maximum')
+ if conf.exists(['lease-time', 'maximum']):
+ subnet['lease_max'] = conf.return_value(['lease-time', 'maximum'])
# Time should be the minimum length in seconds that will be assigned to a lease
- if conf.exists('lease-time minimum'):
- subnet['lease_min'] = conf.return_value('lease-time minimum')
+ if conf.exists(['lease-time', 'minimum']):
+ subnet['lease_min'] = conf.return_value(['lease-time', 'minimum'])
# Specifies a list of Domain Name System name servers available to the client.
# Servers should be listed in order of preference.
- if conf.exists('name-server'):
- subnet['dns_server'] = conf.return_values('name-server')
+ if conf.exists(['name-server']):
+ subnet['dns_server'] = conf.return_values(['name-server'])
# Ancient NIS (Network Information Service) domain name
- if conf.exists('nis-domain'):
- subnet['nis_domain'] = conf.return_value('nis-domain')
+ if conf.exists(['nis-domain']):
+ subnet['nis_domain'] = conf.return_value(['nis-domain'])
# Ancient NIS (Network Information Service) servers
- if conf.exists('nis-server'):
- subnet['nis_server'] = conf.return_values('nis-server')
+ if conf.exists(['nis-server']):
+ subnet['nis_server'] = conf.return_values(['nis-server'])
# Ancient NIS+ (Network Information Service) domain name
- if conf.exists('nisplus-domain'):
- subnet['nisp_domain'] = conf.return_value('nisplus-domain')
+ if conf.exists(['nisplus-domain']):
+ subnet['nisp_domain'] = conf.return_value(['nisplus-domain'])
# Ancient NIS+ (Network Information Service) servers
- if conf.exists('nisplus-server'):
- subnet['nisp_server'] = conf.return_values('nisplus-server')
+ if conf.exists(['nisplus-server']):
+ subnet['nisp_server'] = conf.return_values(['nisplus-server'])
# Prefix Delegation (RFC 3633)
- if conf.exists('prefix-delegation'):
+ if conf.exists(['prefix-delegation']):
print('TODO: This option is actually not implemented right now!')
# Local SIP server that is to be used for all outbound SIP requests - IPv6 address
- if conf.exists('sip-server-address'):
- subnet['sip_address'] = conf.return_values('sip-server-address')
-
- # Local SIP server that is to be used for all outbound SIP requests - hostname
- if conf.exists('sip-server-name'):
- for hostname in conf.return_values('sip-server-name'):
- subnet['sip_hostname'].append('"' + hostname + '"')
+ if conf.exists(['sip-server']):
+ for value in conf.return_values(['sip-server']):
+ if is_ipv6(value):
+ subnet['sip_address'].append(value)
+ else:
+ subnet['sip_hostname'].append(value)
# List of local SNTP servers available for the client to synchronize their clocks
- if conf.exists('sntp-server'):
- subnet['sntp_server'] = conf.return_values('sntp-server')
+ if conf.exists(['sntp-server']):
+ subnet['sntp_server'] = conf.return_values(['sntp-server'])
#
# Static DHCP v6 leases
#
- if conf.exists('static-mapping'):
- for mapping in conf.list_nodes('static-mapping'):
- conf.set_level('service dhcpv6-server shared-network-name {0} subnet {1} static-mapping {2}'.format(network, net, mapping))
+ if conf.exists(['static-mapping']):
+ for mapping in conf.list_nodes(['static-mapping']):
+ conf.set_level(base + ['shared-network-name', network, 'subnet', net, 'static-mapping', mapping])
mapping = {
'name': mapping,
'disabled': False,
@@ -192,16 +191,16 @@ def get_config():
}
# This static lease is disabled
- if conf.exists('disable'):
+ if conf.exists(['disable']):
mapping['disabled'] = True
# IPv6 address used for this DHCP client
- if conf.exists('ipv6-address'):
- mapping['ipv6_address'] = conf.return_value('ipv6-address')
+ if conf.exists(['ipv6-address']):
+ mapping['ipv6_address'] = conf.return_value(['ipv6-address'])
# This option specifies the client’s DUID identifier. DUIDs are similar but different from DHCPv4 client identifiers
- if conf.exists('identifier'):
- mapping['client_identifier'] = conf.return_value('identifier')
+ if conf.exists(['identifier']):
+ mapping['client_identifier'] = conf.return_value(['identifier'])
# append static mapping configuration tu subnet list
subnet['static_mapping'].append(mapping)
@@ -209,10 +208,13 @@ def get_config():
# append subnet configuration to shared network subnet list
config['subnet'].append(subnet)
-
# append shared network configuration to config dictionary
dhcpv6['shared_network'].append(config)
+ # If all shared-networks are disabled, there's nothing to do.
+ if all(net['disabled'] for net in dhcpv6['shared_network']):
+ return None
+
return dhcpv6
def verify(dhcpv6):
@@ -302,22 +304,22 @@ def verify(dhcpv6):
else:
subnets.append(subnet['network'])
- # DHCPv6 requires at least one configured address range or one static mapping
- # (FIXME: is not actually checked right now?)
+ # DHCPv6 requires at least one configured address range or one static mapping
+ # (FIXME: is not actually checked right now?)
- # There must be one subnet connected to a listen interface if network is not disabled.
- if not network['disabled']:
- if is_subnet_connected(subnet['network']):
- listen_ok = True
+ # There must be one subnet connected to a listen interface if network is not disabled.
+ if not network['disabled']:
+ if is_subnet_connected(subnet['network']):
+ listen_ok = True
- # DHCPv6 subnet must not overlap. ISC DHCP also complains about overlapping
- # subnets: "Warning: subnet 2001:db8::/32 overlaps subnet 2001:db8:1::/32"
- net = ipaddress.ip_network(subnet['network'])
- for n in subnets:
- net2 = ipaddress.ip_network(n)
- if (net != net2):
- if net.overlaps(net2):
- raise ConfigError('DHCPv6 conflicting subnet ranges: {0} overlaps {1}'.format(net, net2))
+ # DHCPv6 subnet must not overlap. ISC DHCP also complains about overlapping
+ # subnets: "Warning: subnet 2001:db8::/32 overlaps subnet 2001:db8:1::/32"
+ net = ipaddress.ip_network(subnet['network'])
+ for n in subnets:
+ net2 = ipaddress.ip_network(n)
+ if (net != net2):
+ if net.overlaps(net2):
+ raise ConfigError('DHCPv6 conflicting subnet ranges: {0} overlaps {1}'.format(net, net2))
if not listen_ok:
raise ConfigError('None of the DHCPv6 subnets are connected to a subnet6 on\n' \
@@ -331,11 +333,6 @@ def generate(dhcpv6):
if not dhcpv6 or dhcpv6['disabled']:
return None
- # 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
@@ -346,6 +343,7 @@ def apply(dhcpv6):
if os.path.exists(config_file):
os.unlink(config_file)
+ else:
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 567dfa4b3..7f7417b00 100755
--- a/src/conf_mode/dns_forwarding.py
+++ b/src/conf_mode/dns_forwarding.py
@@ -152,10 +152,6 @@ def generate(dns):
if dns is None:
return None
- dirname = os.path.dirname(config_file)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
-
render(config_file, 'dns-forwarding/recursor.conf.tmpl', dns, trim_blocks=True)
return None
diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py
index 038f77cf9..3386324ae 100755
--- a/src/conf_mode/dynamic_dns.py
+++ b/src/conf_mode/dynamic_dns.py
@@ -217,10 +217,6 @@ def generate(dyndns):
if dyndns['deleted']:
return None
- dirname = os.path.dirname(config_file)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
-
render(config_file, 'dynamic-dns/ddclient.conf.tmpl', dyndns)
# Config file must be accessible only by its owner
diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py
index a669580ae..f181a7b35 100755
--- a/src/conf_mode/host_name.py
+++ b/src/conf_mode/host_name.py
@@ -164,10 +164,17 @@ def apply(config):
if process_named_running('snmpd'):
call('systemctl restart snmpd.service')
- # restart pdns if it is used
- ret = run('/usr/bin/rec_control --socket-dir=/run/powerdns ping')
- if ret == 0:
- call('systemctl restart pdns-recursor.service')
+ # restart pdns if it is used - we check for the control dir to not raise
+ # an exception on system startup
+ #
+ # File "/usr/lib/python3/dist-packages/vyos/configsession.py", line 128, in __run_command
+ # raise ConfigSessionError(output)
+ # vyos.configsession.ConfigSessionError: [ system domain-name vyos.io ]
+ # Fatal: Unable to generate local temporary file in directory '/run/powerdns': No such file or directory
+ if os.path.isdir('/run/powerdns'):
+ ret = run('/usr/bin/rec_control --socket-dir=/run/powerdns ping')
+ if ret == 0:
+ call('systemctl restart pdns-recursor.service')
return None
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
index fd1f218d1..a174e33e4 100755
--- a/src/conf_mode/interfaces-bonding.py
+++ b/src/conf_mode/interfaces-bonding.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
@@ -20,12 +20,12 @@ from copy import deepcopy
from sys import exit
from netifaces import interfaces
-from vyos.ifconfig import BondIf
+from vyos.ifconfig import BondIf, Section
from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
-from vyos.configdict import list_diff, vlan_to_dict
+from vyos.configdict import list_diff, intf_to_dict, add_to_dict
from vyos.config import Config
-from vyos.util import is_bridge_member
-from vyos.util import call
+from vyos.util import call, cmd
+from vyos.validate import is_bridge_member
from vyos import ConfigError
default_config_data = {
@@ -43,6 +43,7 @@ default_config_data = {
'disable': False,
'disable_link_detect': 1,
'hash_policy': 'layer2',
+ 'intf': '',
'ip_arp_cache_tmo': 30,
'ip_disable_arp_filter': 1,
'ip_enable_arp_accept': 0,
@@ -51,10 +52,11 @@ default_config_data = {
'ip_proxy_arp': 0,
'ip_proxy_arp_pvlan': 0,
'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
+ 'ipv6_eui64_prefix': [],
+ 'ipv6_eui64_prefix_remove': [],
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
- 'intf': '',
+ 'is_bridge_member': False,
'mac': '',
'mode': '802.3ad',
'member': [],
@@ -88,6 +90,13 @@ def get_bond_mode(mode):
raise ConfigError('invalid bond mode "{}"'.format(mode))
def get_config():
+ # determine tagNode instance
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+ conf = Config()
+
# initialize kernel module if not loaded
if not os.path.isfile('/sys/class/net/bonding_masters'):
import syslog
@@ -96,32 +105,21 @@ def get_config():
syslog.syslog(syslog.LOG_NOTICE, "failed loading bonding kernel module")
raise ConfigError("failed loading bonding kernel module")
- bond = deepcopy(default_config_data)
- conf = Config()
-
- # determine tagNode instance
- if 'VYOS_TAGNODE_VALUE' not in os.environ:
- raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
-
- bond['intf'] = os.environ['VYOS_TAGNODE_VALUE']
-
# check if bond has been removed
- cfg_base = 'interfaces bonding ' + bond['intf']
+ cfg_base = 'interfaces bonding ' + ifname
if not conf.exists(cfg_base):
+ bond = deepcopy(default_config_data)
+ bond['intf'] = ifname
bond['deleted'] = True
+ # check if interface is member if a bridge
+ bond['is_bridge_member'] = is_bridge_member(conf, ifname)
return bond
# set new configuration level
conf.set_level(cfg_base)
- # retrieve configured interface addresses
- if conf.exists('address'):
- bond['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')
- bond['address_remove'] = list_diff(eff_addr, bond['address'])
+ bond, disabled = intf_to_dict(conf, default_config_data)
+ bond['intf'] = ifname
# ARP link monitoring frequency in milliseconds
if conf.exists('arp-monitor interval'):
@@ -131,38 +129,6 @@ def get_config():
if conf.exists('arp-monitor target'):
bond['arp_mon_tgt'] = conf.return_values('arp-monitor target')
- # retrieve interface description
- if conf.exists('description'):
- bond['description'] = conf.return_value('description')
-
- # get DHCP client identifier
- if conf.exists('dhcp-options client-id'):
- bond['dhcp_client_id'] = conf.return_value('dhcp-options client-id')
-
- # DHCP client host name (overrides the system host name)
- if conf.exists('dhcp-options host-name'):
- bond['dhcp_hostname'] = conf.return_value('dhcp-options host-name')
-
- # DHCP client vendor identifier
- if conf.exists('dhcp-options vendor-class-id'):
- bond['dhcp_vendor_class_id'] = conf.return_value('dhcp-options vendor-class-id')
-
- # DHCPv6 only acquire config parameters, no address
- if conf.exists('dhcpv6-options parameters-only'):
- bond['dhcpv6_prm_only'] = True
-
- # DHCPv6 temporary IPv6 address
- if conf.exists('dhcpv6-options temporary'):
- bond['dhcpv6_temporary'] = True
-
- # ignore link state changes
- if conf.exists('disable-link-detect'):
- bond['disable_link_detect'] = 2
-
- # disable bond interface
- if conf.exists('disable'):
- bond['disable'] = True
-
# Bonding transmit hash policy
if conf.exists('hash-policy'):
bond['hash_policy'] = conf.return_value('hash-policy')
@@ -171,50 +137,10 @@ def get_config():
if conf.exists('ip arp-cache-timeout'):
bond['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout'))
- # ARP filter configuration
- if conf.exists('ip disable-arp-filter'):
- bond['ip_disable_arp_filter'] = 0
-
- # ARP enable accept
- if conf.exists('ip enable-arp-accept'):
- bond['ip_enable_arp_accept'] = 1
-
- # ARP enable announce
- if conf.exists('ip enable-arp-announce'):
- bond['ip_enable_arp_announce'] = 1
-
- # ARP enable ignore
- if conf.exists('ip enable-arp-ignore'):
- bond['ip_enable_arp_ignore'] = 1
-
- # Enable proxy-arp on this interface
- if conf.exists('ip enable-proxy-arp'):
- bond['ip_proxy_arp'] = 1
-
# Enable private VLAN proxy ARP on this interface
if conf.exists('ip proxy-arp-pvlan'):
bond['ip_proxy_arp_pvlan'] = 1
- # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
- if conf.exists('ipv6 address autoconf'):
- bond['ipv6_autoconf'] = 1
-
- # Get prefix for IPv6 addressing based on MAC address (EUI-64)
- if conf.exists('ipv6 address eui64'):
- bond['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
-
- # Disable IPv6 forwarding on this interface
- if conf.exists('ipv6 disable-forwarding'):
- bond['ipv6_forwarding'] = 0
-
- # IPv6 Duplicate Address Detection (DAD) tries
- if conf.exists('ipv6 dup-addr-detect-transmits'):
- bond['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
-
- # Media Access Control (MAC) address
- if conf.exists('mac'):
- bond['mac'] = conf.return_value('mac')
-
# Bonding mode
if conf.exists('mode'):
act_mode = conf.return_value('mode')
@@ -224,10 +150,6 @@ def get_config():
bond['mode'] = get_bond_mode(act_mode)
- # Maximum Transmission Unit (MTU)
- if conf.exists('mtu'):
- bond['mtu'] = int(conf.return_value('mtu'))
-
# determine bond member interfaces (currently configured)
if conf.exists('member interface'):
bond['member'] = conf.return_values('member interface')
@@ -244,48 +166,18 @@ def get_config():
if conf.exists('primary'):
bond['primary'] = conf.return_value('primary')
- # retrieve VRF instance
- if conf.exists('vrf'):
- bond['vrf'] = conf.return_value('vrf')
-
- # get vif-s interfaces (currently effective) - to determine which vif-s
- # interface is no longer present and needs to be removed
- eff_intf = conf.list_effective_nodes('vif-s')
- act_intf = conf.list_nodes('vif-s')
- bond['vif_s_remove'] = list_diff(eff_intf, act_intf)
-
- if conf.exists('vif-s'):
- for vif_s in conf.list_nodes('vif-s'):
- # set config level to vif-s interface
- conf.set_level(cfg_base + ' vif-s ' + vif_s)
- bond['vif_s'].append(vlan_to_dict(conf))
-
- # re-set configuration level to parse new nodes
- conf.set_level(cfg_base)
- # Determine vif interfaces (currently effective) - to determine which
- # vif interface is no longer present and needs to be removed
- eff_intf = conf.list_effective_nodes('vif')
- act_intf = conf.list_nodes('vif')
- bond['vif_remove'] = list_diff(eff_intf, act_intf)
-
- if conf.exists('vif'):
- for vif in conf.list_nodes('vif'):
- # set config level to vif interface
- conf.set_level(cfg_base + ' vif ' + vif)
- bond['vif'].append(vlan_to_dict(conf))
+ add_to_dict(conf, disabled, bond, 'vif', 'vif')
+ add_to_dict(conf, disabled, bond, 'vif-s', 'vif_s')
return bond
def verify(bond):
if bond['deleted']:
- interface = bond['intf']
- is_member, bridge = is_bridge_member(interface)
- if is_member:
- # can not use a f'' formatted-string here as bridge would not get
- # expanded in the print statement
- raise ConfigError('Can not delete interface "{0}" as it ' \
- 'is a member of bridge "{1}"!'.format(interface, bridge))
+ if bond['is_bridge_member']:
+ interface = bond['intf']
+ bridge = bond['is_bridge_member']
+ raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
return None
if len (bond['arp_mon_tgt']) > 16:
@@ -434,17 +326,23 @@ def apply(bond):
b.set_proxy_arp_pvlan(bond['ip_proxy_arp_pvlan'])
# IPv6 address autoconfiguration
b.set_ipv6_autoconf(bond['ipv6_autoconf'])
- # IPv6 EUI-based address
- b.set_ipv6_eui64_address(bond['ipv6_eui64_prefix'])
# IPv6 forwarding
b.set_ipv6_forwarding(bond['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
b.set_ipv6_dad_messages(bond['ipv6_dup_addr_detect'])
+ # Delete old IPv6 EUI64 addresses before changing MAC
+ for addr in bond['ipv6_eui64_prefix_remove']:
+ b.del_ipv6_eui64_address(addr)
+
# Change interface MAC address
if bond['mac']:
b.set_mac(bond['mac'])
+ # Add IPv6 EUI-based addresses
+ for addr in bond['ipv6_eui64_prefix']:
+ b.add_ipv6_eui64_address(addr)
+
# Maximum Transmission Unit (MTU)
b.set_mtu(bond['mtu'])
@@ -467,6 +365,12 @@ def apply(bond):
# Add (enslave) interfaces to bond
for intf in bond['member']:
+ # flushes only children of Interfaces class (e.g. vlan are not)
+ if intf in Section.interfaces():
+ klass = Section.klass(intf, vlan=False)
+ klass(intf, create=False).flush_addrs()
+ # flushes also vlan interfaces
+ call(f'ip addr flush dev "{intf}"')
b.add_port(intf)
# As the bond interface is always disabled first when changing
diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py
index 93c6db97e..9d638653c 100755
--- a/src/conf_mode/interfaces-bridge.py
+++ b/src/conf_mode/interfaces-bridge.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
@@ -20,10 +20,11 @@ from copy import deepcopy
from sys import exit
from netifaces import interfaces
-from vyos.ifconfig import BridgeIf
+from vyos.ifconfig import BridgeIf, Section
from vyos.ifconfig.stp import STP
from vyos.configdict import list_diff
from vyos.config import Config
+from vyos.util import cmd
from vyos import ConfigError
default_config_data = {
@@ -47,7 +48,8 @@ default_config_data = {
'ip_enable_arp_announce': 0,
'ip_enable_arp_ignore': 0,
'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
+ 'ipv6_eui64_prefix': [],
+ 'ipv6_eui64_prefix_remove': [],
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
'igmp_querier': 0,
@@ -160,9 +162,21 @@ def get_config():
if conf.exists('ipv6 address autoconf'):
bridge['ipv6_autoconf'] = 1
- # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ # Get prefixes for IPv6 addressing based on MAC address (EUI-64)
if conf.exists('ipv6 address eui64'):
- bridge['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+ bridge['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64')
+
+ # Determine currently effective EUI64 addresses - to determine which
+ # address is no longer valid and needs to be removed
+ eff_addr = conf.return_effective_values('ipv6 address eui64')
+ bridge['ipv6_eui64_prefix_remove'] = list_diff(eff_addr, bridge['ipv6_eui64_prefix'])
+
+ # Remove the default link-local address if set.
+ if conf.exists('ipv6 address no-default-link-local'):
+ bridge['ipv6_eui64_prefix_remove'].append('fe80::/64')
+ else:
+ # add the link-local by default to make IPv6 work
+ bridge['ipv6_eui64_prefix'].append('fe80::/64')
# Disable IPv6 forwarding on this interface
if conf.exists('ipv6 disable-forwarding'):
@@ -176,6 +190,12 @@ def get_config():
if conf.exists('mac'):
bridge['mac'] = conf.return_value('mac')
+ # Find out if MAC has changed - if so, we need to delete all IPv6 EUI64 addresses
+ # before re-adding them
+ if ( bridge['mac'] and bridge['intf'] in Section.interfaces(section='bridge')
+ and bridge['mac'] != BridgeIf(bridge['intf'], create=False).get_mac() ):
+ bridge['ipv6_eui64_prefix_remove'] += bridge['ipv6_eui64_prefix']
+
# Interval at which neighbor bridges are removed
if conf.exists('max-age'):
bridge['max_age'] = int(conf.return_value('max-age'))
@@ -283,8 +303,6 @@ def apply(bridge):
br.set_arp_ignore(bridge['ip_enable_arp_ignore'])
# IPv6 address autoconfiguration
br.set_ipv6_autoconf(bridge['ipv6_autoconf'])
- # IPv6 EUI-based address
- br.set_ipv6_eui64_address(bridge['ipv6_eui64_prefix'])
# IPv6 forwarding
br.set_ipv6_forwarding(bridge['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
@@ -318,9 +336,10 @@ def apply(bridge):
# assign/remove VRF
br.set_vrf(bridge['vrf'])
- # Change interface MAC address
- if bridge['mac']:
- br.set_mac(bridge['mac'])
+ # Delete old IPv6 EUI64 addresses before changing MAC
+ # (adding members to a fresh bridge changes its MAC too)
+ for addr in bridge['ipv6_eui64_prefix_remove']:
+ br.del_ipv6_eui64_address(addr)
# remove interface from bridge
for intf in bridge['member_remove']:
@@ -328,8 +347,24 @@ def apply(bridge):
# add interfaces to bridge
for member in bridge['member']:
+ # flushes address of only children of Interfaces class
+ # (e.g. vlan are not)
+ if member['name'] in Section.interfaces():
+ klass = Section.klass(member['name'], vlan=False)
+ klass(member['name'], create=False).flush_addrs()
+ # flushes all interfaces
+ cmd(f'ip addr flush dev "{member["name"]}"')
br.add_port(member['name'])
+ # Change interface MAC address
+ if bridge['mac']:
+ br.set_mac(bridge['mac'])
+
+ # Add IPv6 EUI-based addresses (must be done after adding the
+ # 1st bridge member or setting its MAC)
+ for addr in bridge['ipv6_eui64_prefix']:
+ br.add_ipv6_eui64_address(addr)
+
# up/down interface
if bridge['disable']:
br.set_admin_state('down')
diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py
index a256103af..23eaa4ecb 100755
--- a/src/conf_mode/interfaces-dummy.py
+++ b/src/conf_mode/interfaces-dummy.py
@@ -23,7 +23,7 @@ from netifaces import interfaces
from vyos.ifconfig import DummyIf
from vyos.configdict import list_diff
from vyos.config import Config
-from vyos.util import is_bridge_member
+from vyos.validate import is_bridge_member
from vyos import ConfigError
default_config_data = {
@@ -33,6 +33,7 @@ default_config_data = {
'description': '',
'disable': False,
'intf': '',
+ 'is_bridge_member': False,
'vrf': ''
}
@@ -49,6 +50,8 @@ def get_config():
# Check if interface has been removed
if not conf.exists('interfaces dummy ' + dummy['intf']):
dummy['deleted'] = True
+ # check if interface is member if a bridge
+ dummy['is_bridge_member'] = is_bridge_member(conf, dummy['intf'])
return dummy
# set new configuration level
@@ -80,13 +83,11 @@ def get_config():
def verify(dummy):
if dummy['deleted']:
- interface = dummy['intf']
- is_member, bridge = is_bridge_member(interface)
- if is_member:
- # can not use a f'' formatted-string here as bridge would not get
- # expanded in the print statement
- raise ConfigError('Can not delete interface "{0}" as it ' \
- 'is a member of bridge "{1}"!'.format(interface, bridge))
+ if dummy['is_bridge_member']:
+ interface = dummy['intf']
+ bridge = dummy['is_bridge_member']
+ raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+
return None
vrf_name = dummy['vrf']
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index 5a977d797..3ddd394d7 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.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
@@ -20,9 +20,9 @@ from sys import exit
from copy import deepcopy
from netifaces import interfaces
-from vyos.ifconfig import EthernetIf
+from vyos.ifconfig import EthernetIf, Section
from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
-from vyos.configdict import list_diff, vlan_to_dict
+from vyos.configdict import list_diff, intf_to_dict, add_to_dict
from vyos.config import Config
from vyos import ConfigError
@@ -49,7 +49,8 @@ default_config_data = {
'ip_proxy_arp': 0,
'ip_proxy_arp_pvlan': 0,
'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
+ 'ipv6_eui64_prefix': [],
+ 'ipv6_eui64_prefix_remove': [],
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
'intf': '',
@@ -69,18 +70,18 @@ default_config_data = {
}
def get_config():
- eth = deepcopy(default_config_data)
- conf = Config()
-
# determine tagNode instance
if 'VYOS_TAGNODE_VALUE' not in os.environ:
raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
- eth['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+ conf = Config()
# check if ethernet interface has been removed
- cfg_base = ['interfaces', 'ethernet', eth['intf']]
+ cfg_base = ['interfaces', 'ethernet', ifname]
if not conf.exists(cfg_base):
+ eth = deepcopy(default_config_data)
+ eth['intf'] = ifname
eth['deleted'] = True
# we can not bail out early as ethernet interface can not be removed
# Kernel will complain with: RTNETLINK answers: Operation not supported.
@@ -90,42 +91,8 @@ def get_config():
# set new configuration level
conf.set_level(cfg_base)
- # retrieve configured interface addresses
- if conf.exists('address'):
- eth['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')
- eth['address_remove'] = list_diff(eff_addr, eth['address'])
-
- # retrieve interface description
- if conf.exists('description'):
- eth['description'] = conf.return_value('description')
-
- # get DHCP client identifier
- if conf.exists('dhcp-options client-id'):
- eth['dhcp_client_id'] = conf.return_value('dhcp-options client-id')
-
- # DHCP client host name (overrides the system host name)
- if conf.exists('dhcp-options host-name'):
- eth['dhcp_hostname'] = conf.return_value('dhcp-options host-name')
-
- # DHCP client vendor identifier
- if conf.exists('dhcp-options vendor-class-id'):
- eth['dhcp_vendor_class_id'] = conf.return_value('dhcp-options vendor-class-id')
-
- # DHCPv6 only acquire config parameters, no address
- if conf.exists('dhcpv6-options parameters-only'):
- eth['dhcpv6_prm_only'] = True
-
- # DHCPv6 temporary IPv6 address
- if conf.exists('dhcpv6-options temporary'):
- eth['dhcpv6_temporary'] = True
-
- # ignore link state changes
- if conf.exists('disable-link-detect'):
- eth['disable_link_detect'] = 2
+ eth, disabled = intf_to_dict(conf, default_config_data)
+ eth['intf'] = ifname
# disable ethernet flow control (pause frames)
if conf.exists('disable-flow-control'):
@@ -135,10 +102,6 @@ def get_config():
if conf.exists('hw-id'):
eth['hw_id'] = conf.return_value('hw-id')
- # disable interface
- if conf.exists('disable'):
- eth['disable'] = True
-
# interface duplex
if conf.exists('duplex'):
eth['duplex'] = conf.return_value('duplex')
@@ -147,54 +110,10 @@ def get_config():
if conf.exists('ip arp-cache-timeout'):
eth['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout'))
- # ARP filter configuration
- if conf.exists('ip disable-arp-filter'):
- eth['ip_disable_arp_filter'] = 0
-
- # ARP enable accept
- if conf.exists('ip enable-arp-accept'):
- eth['ip_enable_arp_accept'] = 1
-
- # ARP enable announce
- if conf.exists('ip enable-arp-announce'):
- eth['ip_enable_arp_announce'] = 1
-
- # ARP enable ignore
- if conf.exists('ip enable-arp-ignore'):
- eth['ip_enable_arp_ignore'] = 1
-
- # Enable proxy-arp on this interface
- if conf.exists('ip enable-proxy-arp'):
- eth['ip_proxy_arp'] = 1
-
# Enable private VLAN proxy ARP on this interface
if conf.exists('ip proxy-arp-pvlan'):
eth['ip_proxy_arp_pvlan'] = 1
- # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
- if conf.exists('ipv6 address autoconf'):
- eth['ipv6_autoconf'] = 1
-
- # Get prefix for IPv6 addressing based on MAC address (EUI-64)
- if conf.exists('ipv6 address eui64'):
- eth['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
-
- # Disable IPv6 forwarding on this interface
- if conf.exists('ipv6 disable-forwarding'):
- eth['ipv6_forwarding'] = 0
-
- # IPv6 Duplicate Address Detection (DAD) tries
- if conf.exists('ipv6 dup-addr-detect-transmits'):
- eth['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
-
- # Media Access Control (MAC) address
- if conf.exists('mac'):
- eth['mac'] = conf.return_value('mac')
-
- # Maximum Transmission Unit (MTU)
- if conf.exists('mtu'):
- eth['mtu'] = int(conf.return_value('mtu'))
-
# GRO (generic receive offload)
if conf.exists('offload-options generic-receive'):
eth['offload_gro'] = conf.return_value('offload-options generic-receive')
@@ -219,37 +138,8 @@ def get_config():
if conf.exists('speed'):
eth['speed'] = conf.return_value('speed')
- # retrieve VRF instance
- if conf.exists('vrf'):
- eth['vrf'] = conf.return_value('vrf')
-
- # re-set configuration level to parse new nodes
- conf.set_level(cfg_base)
- # get vif-s interfaces (currently effective) - to determine which vif-s
- # interface is no longer present and needs to be removed
- eff_intf = conf.list_effective_nodes('vif-s')
- act_intf = conf.list_nodes('vif-s')
- eth['vif_s_remove'] = list_diff(eff_intf, act_intf)
-
- if conf.exists('vif-s'):
- for vif_s in conf.list_nodes('vif-s'):
- # set config level to vif-s interface
- conf.set_level(cfg_base + ['vif-s', vif_s])
- eth['vif_s'].append(vlan_to_dict(conf))
-
- # re-set configuration level to parse new nodes
- conf.set_level(cfg_base)
- # Determine vif interfaces (currently effective) - to determine which
- # vif interface is no longer present and needs to be removed
- eff_intf = conf.list_effective_nodes('vif')
- act_intf = conf.list_nodes('vif')
- eth['vif_remove'] = list_diff(eff_intf, act_intf)
-
- if conf.exists('vif'):
- for vif in conf.list_nodes('vif'):
- # set config level to vif interface
- conf.set_level(cfg_base + ['vif', vif])
- eth['vif'].append(vlan_to_dict(conf))
+ add_to_dict(conf, disabled, eth, 'vif', 'vif')
+ add_to_dict(conf, disabled, eth, 'vif-s', 'vif_s')
return eth
@@ -336,13 +226,15 @@ def apply(eth):
e.set_proxy_arp_pvlan(eth['ip_proxy_arp_pvlan'])
# IPv6 address autoconfiguration
e.set_ipv6_autoconf(eth['ipv6_autoconf'])
- # IPv6 EUI-based address
- e.set_ipv6_eui64_address(eth['ipv6_eui64_prefix'])
# IPv6 forwarding
e.set_ipv6_forwarding(eth['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
e.set_ipv6_dad_messages(eth['ipv6_dup_addr_detect'])
+ # Delete old IPv6 EUI64 addresses before changing MAC
+ for addr in eth['ipv6_eui64_prefix_remove']:
+ e.del_ipv6_eui64_address(addr)
+
# Change interface MAC address - re-set to real hardware address (hw-id)
# if custom mac is removed
if eth['mac']:
@@ -350,6 +242,10 @@ def apply(eth):
elif eth['hw_id']:
e.set_mac(eth['hw_id'])
+ # Add IPv6 EUI-based addresses
+ for addr in eth['ipv6_eui64_prefix']:
+ e.add_ipv6_eui64_address(addr)
+
# Maximum Transmission Unit (MTU)
e.set_mtu(eth['mtu'])
diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py
index e47473d76..708a64474 100755
--- a/src/conf_mode/interfaces-geneve.py
+++ b/src/conf_mode/interfaces-geneve.py
@@ -22,7 +22,7 @@ from netifaces import interfaces
from vyos.config import Config
from vyos.ifconfig import GeneveIf
-from vyos.util import is_bridge_member
+from vyos.validate import is_bridge_member
from vyos import ConfigError
default_config_data = {
@@ -33,6 +33,7 @@ default_config_data = {
'intf': '',
'ip_arp_cache_tmo': 30,
'ip_proxy_arp': 0,
+ 'is_bridge_member': False,
'mtu': 1500,
'remote': '',
'vni': ''
@@ -51,6 +52,8 @@ def get_config():
# Check if interface has been removed
if not conf.exists('interfaces geneve ' + geneve['intf']):
geneve['deleted'] = True
+ # check if interface is member if a bridge
+ geneve['is_bridge_member'] = is_bridge_member(conf, geneve['intf'])
return geneve
# set new configuration level
@@ -93,13 +96,11 @@ def get_config():
def verify(geneve):
if geneve['deleted']:
- interface = geneve['intf']
- is_member, bridge = is_bridge_member(interface)
- if is_member:
- # can not use a f'' formatted-string here as bridge would not get
- # expanded in the print statement
- raise ConfigError('Can not delete interface "{0}" as it ' \
- 'is a member of bridge "{1}"!'.format(interface, bridge))
+ if geneve['is_bridge_member']:
+ interface = geneve['intf']
+ bridge = geneve['is_bridge_member']
+ raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+
return None
if not geneve['remote']:
diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py
index 11ba9acdd..33cf62f70 100755
--- a/src/conf_mode/interfaces-l2tpv3.py
+++ b/src/conf_mode/interfaces-l2tpv3.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
@@ -18,13 +18,13 @@ import os
from sys import exit
from copy import deepcopy
+from netifaces import interfaces
from vyos.config import Config
from vyos.ifconfig import L2TPv3If, Interface
from vyos import ConfigError
from vyos.util import call
-from vyos.util import is_bridge_member
-from netifaces import interfaces
+from vyos.validate import is_bridge_member, is_addr_assigned
default_config_data = {
'address': [],
@@ -36,9 +36,10 @@ default_config_data = {
'local_port': 5000,
'intf': '',
'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
+ 'ipv6_eui64_prefix': [],
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
+ 'is_bridge_member': False,
'mtu': 1488,
'peer_session_id': '',
'peer_tunnel_id': '',
@@ -68,15 +69,16 @@ def get_config():
# Check if interface has been removed
if not conf.exists('interfaces l2tpv3 ' + l2tpv3['intf']):
l2tpv3['deleted'] = True
- # to delete the l2tpv3 interface we need to current
- # tunnel_id and session_id
- if conf.exists_effective('interfaces l2tpv3 {} tunnel-id'.format(l2tpv3['intf'])):
- l2tpv3['tunnel_id'] = conf.return_effective_value(
- 'interfaces l2tpv3 {} tunnel-id'.format(l2tpv3['intf']))
+ interface = l2tpv3['intf']
+ # check if interface is member if a bridge
+ l2tpv3['is_bridge_member'] = is_bridge_member(conf, interface)
- if conf.exists_effective('interfaces l2tpv3 {} session-id'.format(l2tpv3['intf'])):
- l2tpv3['session_id'] = conf.return_effective_value(
- 'interfaces l2tpv3 {} session-id'.format(l2tpv3['intf']))
+ # to delete the l2tpv3 interface we need the current tunnel_id and session_id
+ if conf.exists_effective(f'interfaces l2tpv3 {interface} tunnel-id'):
+ l2tpv3['tunnel_id'] = conf.return_effective_value(f'interfaces l2tpv3 {interface} tunnel-id')
+
+ if conf.exists_effective(f'interfaces l2tpv3 {interface} session-id'):
+ l2tpv3['session_id'] = conf.return_effective_value(f'interfaces l2tpv3 {interface} session-id')
return l2tpv3
@@ -111,9 +113,14 @@ def get_config():
if conf.exists('ipv6 address autoconf'):
l2tpv3['ipv6_autoconf'] = 1
- # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ # Get prefixes for IPv6 addressing based on MAC address (EUI-64)
if conf.exists('ipv6 address eui64'):
- l2tpv3['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+ l2tpv3['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64')
+
+ # Remove the default link-local address if set.
+ if not conf.exists('ipv6 address no-default-link-local'):
+ # add the link-local by default to make IPv6 work
+ l2tpv3['ipv6_eui64_prefix'].append('fe80::/64')
# Disable IPv6 forwarding on this interface
if conf.exists('ipv6 disable-forwarding'):
@@ -158,17 +165,19 @@ def verify(l2tpv3):
interface = l2tpv3['intf']
if l2tpv3['deleted']:
- is_member, bridge = is_bridge_member(interface)
- if is_member:
- # can not use a f'' formatted-string here as bridge would not get
- # expanded in the print statement
- raise ConfigError('Can not delete interface "{0}" as it ' \
- 'is a member of bridge "{1}"!'.format(interface, bridge))
+ if l2tpv3['is_bridge_member']:
+ interface = l2tpv3['intf']
+ bridge = l2tpv3['is_bridge_member']
+ raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+
return None
if not l2tpv3['local_address']:
raise ConfigError(f'Must configure the l2tpv3 local-ip for {interface}')
+ if not is_addr_assigned(l2tpv3['local_address']):
+ raise ConfigError(f'Must use a configured IP on l2tpv3 local-ip for {interface}')
+
if not l2tpv3['remote_address']:
raise ConfigError(f'Must configure the l2tpv3 remote-ip for {interface}')
@@ -224,8 +233,6 @@ def apply(l2tpv3):
l.set_mtu(l2tpv3['mtu'])
# IPv6 address autoconfiguration
l.set_ipv6_autoconf(l2tpv3['ipv6_autoconf'])
- # IPv6 EUI-based address
- l.set_ipv6_eui64_address(l2tpv3['ipv6_eui64_prefix'])
# IPv6 forwarding
l.set_ipv6_forwarding(l2tpv3['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
@@ -237,6 +244,10 @@ def apply(l2tpv3):
for addr in l2tpv3['address']:
l.add_addr(addr)
+ # IPv6 EUI-based addresses
+ for addr in l2tpv3['ipv6_eui64_prefix']:
+ l.add_ipv6_eui64_address(addr)
+
# As the interface is always disabled first when changing parameters
# we will only re-enable the interface if it is not administratively
# disabled
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 836deb64b..029bc1d69 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -25,12 +25,12 @@ from time import sleep
from shutil import rmtree
from vyos.config import Config
+from vyos.configdict import list_diff
from vyos.ifconfig import VTunIf
-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
-
+from vyos.util import call, chown, chmod_600, chmod_755
+from vyos.validate import is_addr_assigned, is_bridge_member, is_ipv4
+from vyos import ConfigError
user = 'openvpn'
group = 'openvpn'
@@ -39,6 +39,7 @@ default_config_data = {
'address': [],
'auth_user': '',
'auth_pass': '',
+ 'auth_user_pass_file': '',
'auth': False,
'bridge_member': [],
'compress_lzo': False,
@@ -50,11 +51,13 @@ default_config_data = {
'hash': '',
'intf': '',
'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
+ 'ipv6_eui64_prefix': [],
+ 'ipv6_eui64_prefix_remove': [],
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
'ipv6_local_address': [],
'ipv6_remote_address': [],
+ 'is_bridge_member': False,
'ping_restart': '60',
'ping_interval': '10',
'local_address': [],
@@ -66,6 +69,7 @@ default_config_data = {
'options': [],
'persistent_tunnel': False,
'protocol': 'udp',
+ 'protocol_real': '',
'redirect_gateway': '',
'remote_address': [],
'remote_host': [],
@@ -193,10 +197,13 @@ def get_config():
raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
openvpn['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ openvpn['auth_user_pass_file'] = f"/run/openvpn/{openvpn['intf']}.pw"
# Check if interface instance has been removed
if not conf.exists('interfaces openvpn ' + openvpn['intf']):
openvpn['deleted'] = True
+ # check if interface is member if a bridge
+ openvpn['is_bridge_member'] = is_bridge_member(conf, openvpn['intf'])
return openvpn
# Check if we belong to any bridge interface
@@ -309,9 +316,21 @@ def get_config():
if conf.exists('ipv6 address autoconf'):
openvpn['ipv6_autoconf'] = 1
- # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ # Get prefixes for IPv6 addressing based on MAC address (EUI-64)
if conf.exists('ipv6 address eui64'):
- openvpn['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+ openvpn['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64')
+
+ # Determine currently effective EUI64 addresses - to determine which
+ # address is no longer valid and needs to be removed
+ eff_addr = conf.return_effective_values('ipv6 address eui64')
+ openvpn['ipv6_eui64_prefix_remove'] = list_diff(eff_addr, openvpn['ipv6_eui64_prefix'])
+
+ # Remove the default link-local address if set.
+ if conf.exists('ipv6 address no-default-link-local'):
+ openvpn['ipv6_eui64_prefix_remove'].append('fe80::/64')
+ else:
+ # add the link-local by default to make IPv6 work
+ openvpn['ipv6_eui64_prefix'].append('fe80::/64')
# Disable IPv6 forwarding on this interface
if conf.exists('ipv6 disable-forwarding'):
@@ -553,6 +572,23 @@ def get_config():
if openvpn['mode'] == 'server' and not openvpn['server_topology']:
openvpn['server_topology'] = 'net30'
+ # Convert protocol to real protocol used by openvpn.
+ # To make openvpn listen on both IPv4 and IPv6 we must use *6 protocols
+ # (https://community.openvpn.net/openvpn/ticket/360), unless local is IPv4
+ # in which case it must use the standard protocols.
+ # Note: this will break openvpn if IPv6 is disabled on the system.
+ # This currently isn't supported, a check can be added in the future.
+ if openvpn['protocol'] == 'tcp-active':
+ openvpn['protocol_real'] = 'tcp6-client'
+ elif openvpn['protocol'] == 'tcp-passive':
+ openvpn['protocol_real'] = 'tcp6-server'
+ else:
+ openvpn['protocol_real'] = 'udp6'
+
+ if is_ipv4(openvpn['local_host']):
+ # takes out the '6'
+ openvpn['protocol_real'] = openvpn['protocol_real'][:3] + openvpn['protocol_real'][4:]
+
# Set defaults where necessary.
# If any of the input parameters are wrong,
# this will return False and no defaults will be set.
@@ -598,13 +634,11 @@ def get_config():
def verify(openvpn):
if openvpn['deleted']:
- interface = openvpn['intf']
- is_member, bridge = is_bridge_member(interface)
- if is_member:
- # can not use a f'' formatted-string here as bridge would not get
- # expanded in the print statement
- raise ConfigError('Can not delete interface "{0}" as it ' \
- 'is a member of bridge "{1}"!'.format(interface, bridge))
+ if openvpn['is_bridge_member']:
+ interface = openvpn['intf']
+ bridge = openvpn['is_bridge_member']
+ raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+
return None
@@ -917,18 +951,18 @@ def verify(openvpn):
return None
def generate(openvpn):
- if openvpn['deleted'] or openvpn['disable']:
- return None
-
interface = openvpn['intf']
directory = os.path.dirname(get_config_name(interface))
- # we can't know in advance which clients have been,
- # remove all client configs
+ # we can't know in advance which clients have been removed,
+ # thus all client configs will be removed and re-added on demand
ccd_dir = os.path.join(directory, 'ccd', interface)
if os.path.isdir(ccd_dir):
rmtree(ccd_dir, ignore_errors=True)
+ if openvpn['deleted'] or openvpn['disable']:
+ return None
+
# create config directory on demand
directories = []
directories.append(f'{directory}/status')
@@ -944,17 +978,16 @@ def generate(openvpn):
fix_permissions.append(openvpn['tls_key'])
# Generate User/Password authentication file
- user_auth_file = f'/tmp/openvpn-{interface}-pw'
if openvpn['auth']:
- with open(user_auth_file, 'w') as f:
+ with open(openvpn['auth_user_pass_file'], 'w') as f:
f.write('{}\n{}'.format(openvpn['auth_user'], openvpn['auth_pass']))
# also change permission on auth file
- fix_permissions.append(user_auth_file)
+ fix_permissions.append(openvpn['auth_user_pass_file'])
else:
# delete old auth file if present
- if os.path.isfile(user_auth_file):
- os.remove(user_auth_file)
+ if os.path.isfile(openvpn['auth_user_pass_file']):
+ os.remove(openvpn['auth_user_pass_file'])
# Generate client specific configuration
for client in openvpn['client']:
@@ -980,15 +1013,14 @@ def apply(openvpn):
# 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(interface)):
- os.remove(get_config_name(interface))
+ # cleanup old configuration files
+ cleanup = []
+ cleanup.append(get_config_name(interface))
+ cleanup.append(openvpn['auth_user_pass_file'])
- # cleanup client config dir
- 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)
+ for file in cleanup:
+ if os.path.isfile(file):
+ os.unlink(file)
return None
@@ -1025,13 +1057,24 @@ def apply(openvpn):
o.set_alias(openvpn['description'])
# IPv6 address autoconfiguration
o.set_ipv6_autoconf(openvpn['ipv6_autoconf'])
- # IPv6 EUI-based address
- o.set_ipv6_eui64_address(openvpn['ipv6_eui64_prefix'])
# IPv6 forwarding
o.set_ipv6_forwarding(openvpn['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
o.set_ipv6_dad_messages(openvpn['ipv6_dup_addr_detect'])
+ # IPv6 EUI-based addresses - only in TAP mode (TUN's have no MAC)
+ # If MAC has changed, old EUI64 addresses won't get deleted,
+ # but this isn't easy to solve, so leave them.
+ # This is even more difficult as openvpn uses a random MAC for the
+ # initial interface creation, unless set by 'lladdr'.
+ # NOTE: right now the interface is always deleted. For future
+ # compatibility when tap's are not deleted, leave the del_ in
+ if openvpn['mode'] == 'tap':
+ for addr in openvpn['ipv6_eui64_prefix_remove']:
+ o.del_ipv6_eui64_address(addr)
+ for addr in openvpn['ipv6_eui64_prefix']:
+ o.add_ipv6_eui64_address(addr)
+
except:
pass
diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py
index f942b7d2f..e72540f66 100755
--- a/src/conf_mode/interfaces-pppoe.py
+++ b/src/conf_mode/interfaces-pppoe.py
@@ -173,12 +173,6 @@ def generate(pppoe):
config_files = [config_pppoe, script_pppoe_pre_up, script_pppoe_ip_up,
script_pppoe_ip_down, script_pppoe_ipv6_up]
- # Ensure directories for config files exist - otherwise create them on demand
- for file in config_files:
- dirname = os.path.dirname(file)
- if not os.path.isdir(dirname):
- os.mkdir(dirname)
-
# Always hang-up PPPoE connection prior generating new configuration file
cmd(f'systemctl stop ppp@{intf}.service')
@@ -189,27 +183,23 @@ def generate(pppoe):
os.unlink(file)
else:
+ # generated script must be executable
+
# Create PPP configuration files
render(config_pppoe, 'pppoe/peer.tmpl',
- pppoe, trim_blocks=True)
+ pppoe, trim_blocks=True, permission=0o755)
# Create script for ip-pre-up.d
render(script_pppoe_pre_up, 'pppoe/ip-pre-up.script.tmpl',
- pppoe, trim_blocks=True)
+ pppoe, trim_blocks=True, permission=0o755)
# Create script for ip-up.d
render(script_pppoe_ip_up, 'pppoe/ip-up.script.tmpl',
- pppoe, trim_blocks=True)
+ pppoe, trim_blocks=True, permission=0o755)
# Create script for ip-down.d
render(script_pppoe_ip_down, 'pppoe/ip-down.script.tmpl',
- pppoe, trim_blocks=True)
+ pppoe, trim_blocks=True, permission=0o755)
# Create script for ipv6-up.d
render(script_pppoe_ipv6_up, 'pppoe/ipv6-up.script.tmpl',
- pppoe, trim_blocks=True)
-
- # make generated script file executable
- chmod_755(script_pppoe_pre_up)
- chmod_755(script_pppoe_ip_up)
- chmod_755(script_pppoe_ip_down)
- chmod_755(script_pppoe_ipv6_up)
+ pppoe, trim_blocks=True, permission=0o755)
return None
diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py
index 655006146..f0f893b44 100755
--- a/src/conf_mode/interfaces-pseudo-ethernet.py
+++ b/src/conf_mode/interfaces-pseudo-ethernet.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
@@ -20,11 +20,11 @@ from copy import deepcopy
from sys import exit
from netifaces import interfaces
-from vyos.ifconfig import MACVLANIf
-from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
-from vyos.configdict import list_diff, vlan_to_dict
from vyos.config import Config
-from vyos.util import is_bridge_member
+from vyos.configdict import list_diff, vlan_to_dict, intf_to_dict, add_to_dict
+from vyos.ifconfig import MACVLANIf, Section
+from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
+from vyos.validate import is_bridge_member
from vyos import ConfigError
default_config_data = {
@@ -39,6 +39,7 @@ default_config_data = {
'dhcpv6_temporary': False,
'disable': False,
'disable_link_detect': 1,
+ 'intf': '',
'ip_arp_cache_tmo': 30,
'ip_disable_arp_filter': 1,
'ip_enable_arp_accept': 0,
@@ -47,10 +48,11 @@ default_config_data = {
'ip_proxy_arp': 0,
'ip_proxy_arp_pvlan': 0,
'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
+ 'ipv6_eui64_prefix': [],
+ 'ipv6_eui64_prefix_remove': [],
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
- 'intf': '',
+ 'is_bridge_member': False,
'source_interface': '',
'source_interface_changed': False,
'mac': '',
@@ -63,109 +65,36 @@ default_config_data = {
}
def get_config():
- peth = deepcopy(default_config_data)
- conf = Config()
-
# determine tagNode instance
if 'VYOS_TAGNODE_VALUE' not in os.environ:
raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
- peth['intf'] = os.environ['VYOS_TAGNODE_VALUE']
- cfg_base = ['interfaces', 'pseudo-ethernet', peth['intf']]
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+ conf = Config()
# Check if interface has been removed
+ cfg_base = ['interfaces', 'pseudo-ethernet', ifname]
if not conf.exists(cfg_base):
+ peth = deepcopy(default_config_data)
peth['deleted'] = True
+ # check if interface is member if a bridge
+ peth['is_bridge_member'] = is_bridge_member(conf, ifname)
return peth
# set new configuration level
conf.set_level(cfg_base)
- # retrieve configured interface addresses
- if conf.exists(['address']):
- peth['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'])
- peth['address_remove'] = list_diff(eff_addr, peth['address'])
-
- # retrieve interface description
- if conf.exists(['description']):
- peth['description'] = conf.return_value(['description'])
-
- # get DHCP client identifier
- if conf.exists(['dhcp-options', 'client-id']):
- peth['dhcp_client_id'] = conf.return_value(['dhcp-options', 'client-id'])
-
- # DHCP client host name (overrides the system host name)
- if conf.exists(['dhcp-options', 'host-name']):
- peth['dhcp_hostname'] = conf.return_value(['dhcp-options', 'host-name'])
-
- # DHCP client vendor identifier
- if conf.exists(['dhcp-options', 'vendor-class-id']):
- peth['dhcp_vendor_class_id'] = conf.return_value(['dhcp-options', 'vendor-class-id'])
-
- # DHCPv6 only acquire config parameters, no address
- if conf.exists(['dhcpv6-options parameters-only']):
- peth['dhcpv6_prm_only'] = True
-
- # DHCPv6 temporary IPv6 address
- if conf.exists(['dhcpv6-options temporary']):
- peth['dhcpv6_temporary'] = True
-
- # disable interface
- if conf.exists(['disable']):
- peth['disable'] = True
-
- # ignore link state changes
- if conf.exists(['disable-link-detect']):
- peth['disable_link_detect'] = 2
+ peth, disabled = intf_to_dict(conf, default_config_data)
+ peth['intf'] = ifname
# ARP cache entry timeout in seconds
if conf.exists(['ip', 'arp-cache-timeout']):
peth['ip_arp_cache_tmo'] = int(conf.return_value(['ip', 'arp-cache-timeout']))
- # ARP filter configuration
- if conf.exists(['ip', 'disable-arp-filter']):
- peth['ip_disable_arp_filter'] = 0
-
- # ARP enable accept
- if conf.exists(['ip', 'enable-arp-accept']):
- peth['ip_enable_arp_accept'] = 1
-
- # ARP enable announce
- if conf.exists(['ip', 'enable-arp-announce']):
- peth['ip_enable_arp_announce'] = 1
-
- # ARP enable ignore
- if conf.exists(['ip', 'enable-arp-ignore']):
- peth['ip_enable_arp_ignore'] = 1
-
- # Enable proxy-arp on this interface
- if conf.exists(['ip', 'enable-proxy-arp']):
- peth['ip_proxy_arp'] = 1
-
# Enable private VLAN proxy ARP on this interface
if conf.exists(['ip', 'proxy-arp-pvlan']):
peth['ip_proxy_arp_pvlan'] = 1
- # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
- if conf.exists('ipv6 address autoconf'):
- peth['ipv6_autoconf'] = 1
-
- # Get prefix for IPv6 addressing based on MAC address (EUI-64)
- if conf.exists('ipv6 address eui64'):
- peth['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
-
- # Disable IPv6 forwarding on this interface
- if conf.exists('ipv6 disable-forwarding'):
- peth['ipv6_forwarding'] = 0
-
- # IPv6 Duplicate Address Detection (DAD) tries
- if conf.exists('ipv6 dup-addr-detect-transmits'):
- peth['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
-
# Physical interface
if conf.exists(['source-interface']):
peth['source_interface'] = conf.return_value(['source-interface'])
@@ -173,65 +102,29 @@ def get_config():
if tmp != peth['source_interface']:
peth['source_interface_changed'] = True
- # Media Access Control (MAC) address
- if conf.exists(['mac']):
- peth['mac'] = conf.return_value(['mac'])
-
# MACvlan mode
if conf.exists(['mode']):
peth['mode'] = conf.return_value(['mode'])
- # retrieve VRF instance
- if conf.exists('vrf'):
- peth['vrf'] = conf.return_value('vrf')
-
- # re-set configuration level to parse new nodes
- conf.set_level(cfg_base)
- # get vif-s interfaces (currently effective) - to determine which vif-s
- # interface is no longer present and needs to be removed
- eff_intf = conf.list_effective_nodes('vif-s')
- act_intf = conf.list_nodes('vif-s')
- peth['vif_s_remove'] = list_diff(eff_intf, act_intf)
-
- if conf.exists('vif-s'):
- for vif_s in conf.list_nodes('vif-s'):
- # set config level to vif-s interface
- conf.set_level(cfg_base + ['vif-s', vif_s])
- peth['vif_s'].append(vlan_to_dict(conf))
-
- # re-set configuration level to parse new nodes
- conf.set_level(cfg_base)
- # Determine vif interfaces (currently effective) - to determine which
- # vif interface is no longer present and needs to be removed
- eff_intf = conf.list_effective_nodes('vif')
- act_intf = conf.list_nodes('vif')
- peth['vif_remove'] = list_diff(eff_intf, act_intf)
-
- if conf.exists('vif'):
- for vif in conf.list_nodes('vif'):
- # set config level to vif interface
- conf.set_level(cfg_base + ['vif', vif])
- peth['vif'].append(vlan_to_dict(conf))
-
+ add_to_dict(conf, disabled, peth, 'vif', 'vif')
+ add_to_dict(conf, disabled, peth, 'vif-s', 'vif_s')
return peth
def verify(peth):
if peth['deleted']:
- interface = peth['intf']
- is_member, bridge = is_bridge_member(interface)
- if is_member:
- # can not use a f'' formatted-string here as bridge would not get
- # expanded in the print statement
- raise ConfigError('Can not delete interface "{0}" as it ' \
- 'is a member of bridge "{1}"!'.format(interface, bridge))
+ if peth['is_bridge_member']:
+ interface = peth['intf']
+ bridge = peth['is_bridge_member']
+ raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+
return None
if not peth['source_interface']:
- raise ConfigError('Link device must be set for virtual ethernet {}'.format(peth['intf']))
+ raise ConfigError('source-interface must be set for virtual ethernet {}'.format(peth['intf']))
if not peth['source_interface'] in interfaces():
- raise ConfigError('Pseudo-ethernet source interface does not exist')
+ raise ConfigError('Pseudo-ethernet source-interface does not exist')
vrf_name = peth['vrf']
if vrf_name and vrf_name not in interfaces():
@@ -245,38 +138,29 @@ def generate(peth):
return None
def apply(peth):
-
- p = ''
if peth['deleted']:
# delete interface
- p = MACVLANIf(peth['intf'])
- p.remove()
+ MACVLANIf(peth['intf']).remove()
return None
- elif peth['source_interface_changed']:
- # Check if MACVLAN interface already exists. Parameters like the
- # underlaying source-interface device can not be changed on the fly
- # and the interface needs to be recreated from the bottom.
- #
- # 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'])
- p.remove()
-
- # MACVLAN interface needs to be created on-block instead of passing a ton
- # of arguments, I just use a dict that is managed by vyos.ifconfig
- conf = deepcopy(MACVLANIf.get_config())
-
- # Assign MACVLAN instance configuration parameters to config dict
- conf['source_interface'] = peth['source_interface']
- conf['mode'] = peth['mode']
-
- # It is safe to "re-create" the interface always, there is a sanity check
- # that the interface will only be create if its non existent
- p = MACVLANIf(peth['intf'], **conf)
- else:
- p = MACVLANIf(peth['intf'])
+ # Check if MACVLAN interface already exists. Parameters like the underlaying
+ # source-interface device can not be changed on the fly and the interface
+ # needs to be recreated from the bottom.
+ if peth['intf'] in interfaces():
+ if peth['source_interface_changed']:
+ MACVLANIf(peth['intf']).remove()
+
+ # MACVLAN interface needs to be created on-block instead of passing a ton
+ # of arguments, I just use a dict that is managed by vyos.ifconfig
+ conf = deepcopy(MACVLANIf.get_config())
+
+ # Assign MACVLAN instance configuration parameters to config dict
+ conf['source_interface'] = peth['source_interface']
+ conf['mode'] = peth['mode']
+
+ # It is safe to "re-create" the interface always, there is a sanity check
+ # that the interface will only be create if its non existent
+ p = MACVLANIf(peth['intf'], **conf)
# update interface description used e.g. within SNMP
p.set_alias(peth['description'])
@@ -314,8 +198,6 @@ def apply(peth):
p.set_proxy_arp_pvlan(peth['ip_proxy_arp_pvlan'])
# IPv6 address autoconfiguration
p.set_ipv6_autoconf(peth['ipv6_autoconf'])
- # IPv6 EUI-based address
- p.set_ipv6_eui64_address(peth['ipv6_eui64_prefix'])
# IPv6 forwarding
p.set_ipv6_forwarding(peth['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
@@ -324,10 +206,18 @@ def apply(peth):
# assign/remove VRF
p.set_vrf(peth['vrf'])
+ # Delete old IPv6 EUI64 addresses before changing MAC
+ for addr in peth['ipv6_eui64_prefix_remove']:
+ p.del_ipv6_eui64_address(addr)
+
# Change interface MAC address
if peth['mac']:
p.set_mac(peth['mac'])
+ # Add IPv6 EUI-based addresses
+ for addr in peth['ipv6_eui64_prefix']:
+ p.add_ipv6_eui64_address(addr)
+
# Change interface mode
p.set_mode(peth['mode'])
diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
index 06c2ea29b..fc084814a 100755
--- a/src/conf_mode/interfaces-tunnel.py
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -25,7 +25,7 @@ from vyos.config import Config
from vyos.ifconfig import Interface, GREIf, GRETapIf, IPIPIf, IP6GREIf, IPIP6If, IP6IP6If, SitIf, Sit6RDIf
from vyos.ifconfig.afi import IP4, IP6
from vyos.configdict import list_diff
-from vyos.validate import is_ipv4, is_ipv6
+from vyos.validate import is_ipv4, is_ipv6, is_bridge_member
from vyos import ConfigError
from vyos.dicts import FixedDict
@@ -255,7 +255,9 @@ default_config_data = {
'ipv6_forwarding': 1,
'ipv6_dad_transmits': 1,
# internal
+ 'interfaces': [],
'tunnel': {},
+ 'bridge': '',
# the following names are exactly matching the name
# for the ip command and must not be changed
'ifname': '',
@@ -264,6 +266,7 @@ default_config_data = {
'mtu': '1476',
'local': '',
'remote': '',
+ 'dev': '',
'multicast': 'disable',
'allmulticast': 'disable',
'ttl': '255',
@@ -285,6 +288,7 @@ mapping = {
'local': ('local-ip', False, None),
'remote': ('remote-ip', False, None),
'multicast': ('multicast', False, None),
+ 'dev': ('source-interface', False, None),
'ttl': ('parameters ip ttl', False, None),
'tos': ('parameters ip tos', False, None),
'key': ('parameters ip key', False, None),
@@ -405,6 +409,10 @@ def get_config():
ct = conf.get_config_dict()['tunnel']
options['tunnel'] = {}
+ # check for bridges
+ options['bridge'] = is_bridge_member(conf, ifname)
+ options['interfaces'] = interfaces()
+
for name in ct:
tunnel = ct[name]
encap = tunnel.get('encapsulation', '')
@@ -429,6 +437,11 @@ def verify(conf):
if changes['section'] == 'delete':
if ifname in options['nhrp']:
raise ConfigError(f'Can not delete interface tunnel {iftype} {ifname}, it is used by nhrp')
+
+ bridge = options['bridge']
+ if bridge:
+ raise ConfigError(f'Interface "{ifname}" can not be deleted as it belongs to bridge "{bridge}"!')
+
# done, bail out early
return None
@@ -448,7 +461,7 @@ def verify(conf):
# what are the tunnel options we can set / modified / deleted
kls = get_class(options)
- valid = kls.updates + ['alias', 'addresses-add', 'addresses-del', 'vrf']
+ valid = kls.updates + ['alias', 'addresses-add', 'addresses-del', 'vrf', 'state']
if changes['section'] == 'create':
valid.extend(['type',])
@@ -474,6 +487,7 @@ def verify(conf):
afi_remote = get_afi(tun_remote)
tun_ismgre = iftype == 'gre' and not options['remote']
tun_is6rd = iftype == 'sit' and options['6rd-prefix']
+ tun_dev = options['dev']
# incompatible options
@@ -483,6 +497,9 @@ def verify(conf):
if tun_local and options['dhcp-interface']:
raise ConfigError(f'Must configure only one of local-ip or dhcp-interface for tunnel {iftype} {ifname}')
+ if tun_dev and iftype in ('gre-bridge', 'sit'):
+ raise ConfigError(f'source interface can not be used with {iftype} {ifname}')
+
# tunnel endpoint
if afi_local != afi_remote:
@@ -510,9 +527,14 @@ def verify(conf):
# vrf check
vrf = options['vrf']
- if vrf and vrf not in interfaces():
+ if vrf and vrf not in options['interfaces']:
raise ConfigError(f'VRF "{vrf}" does not exist')
+ # source-interface check
+
+ if tun_dev and tun_dev not in options['interfaces']:
+ raise ConfigError(f'device "{dev}" does not exist')
+
# tunnel encapsulation check
convert = {
diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index 6639a9b0d..74eae4281 100755
--- a/src/conf_mode/interfaces-vxlan.py
+++ b/src/conf_mode/interfaces-vxlan.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
@@ -22,7 +22,7 @@ from netifaces import interfaces
from vyos.config import Config
from vyos.ifconfig import VXLANIf, Interface
-from vyos.util import is_bridge_member
+from vyos.validate import is_bridge_member
from vyos import ConfigError
default_config_data = {
@@ -39,9 +39,10 @@ default_config_data = {
'ip_enable_arp_ignore': 0,
'ip_proxy_arp': 0,
'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
+ 'ipv6_eui64_prefix': [],
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
+ 'is_bridge_member': False,
'source_address': '',
'source_interface': '',
'mtu': 1450,
@@ -64,6 +65,8 @@ def get_config():
# Check if interface has been removed
if not conf.exists('interfaces vxlan ' + vxlan['intf']):
vxlan['deleted'] = True
+ # check if interface is member if a bridge
+ vxlan['is_bridge_member'] = is_bridge_member(conf, vxlan['intf'])
return vxlan
# set new configuration level
@@ -113,9 +116,14 @@ def get_config():
if conf.exists('ipv6 address autoconf'):
vxlan['ipv6_autoconf'] = 1
- # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ # Get prefixes for IPv6 addressing based on MAC address (EUI-64)
if conf.exists('ipv6 address eui64'):
- vxlan['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+ vxlan['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64')
+
+ # Remove the default link-local address if set.
+ if not conf.exists('ipv6 address no-default-link-local'):
+ # add the link-local by default to make IPv6 work
+ vxlan['ipv6_eui64_prefix'].append('fe80::/64')
# Disable IPv6 forwarding on this interface
if conf.exists('ipv6 disable-forwarding'):
@@ -154,13 +162,11 @@ def get_config():
def verify(vxlan):
if vxlan['deleted']:
- interface = vxlan['intf']
- is_member, bridge = is_bridge_member(interface)
- if is_member:
- # can not use a f'' formatted-string here as bridge would not get
- # expanded in the print statement
- raise ConfigError('Can not delete interface "{0}" as it ' \
- 'is a member of bridge "{1}"!'.format(interface, bridge))
+ if vxlan['is_bridge_member']:
+ interface = vxlan['intf']
+ bridge = vxlan['is_bridge_member']
+ raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+
return None
if vxlan['mtu'] < 1500:
@@ -237,8 +243,6 @@ def apply(vxlan):
v.set_proxy_arp(vxlan['ip_proxy_arp'])
# IPv6 address autoconfiguration
v.set_ipv6_autoconf(vxlan['ipv6_autoconf'])
- # IPv6 EUI-based address
- v.set_ipv6_eui64_address(vxlan['ipv6_eui64_prefix'])
# IPv6 forwarding
v.set_ipv6_forwarding(vxlan['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
@@ -250,7 +254,11 @@ def apply(vxlan):
for addr in vxlan['address']:
v.add_addr(addr)
- # As the bond interface is always disabled first when changing
+ # IPv6 EUI-based addresses
+ for addr in vxlan['ipv6_eui64_prefix']:
+ v.add_ipv6_eui64_address(addr)
+
+ # As the VXLAN interface is always disabled first when changing
# parameters we will only re-enable the interface if it is not
# administratively disabled
if not vxlan['disable']:
diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py
index 8bf81c747..01f84260d 100755
--- a/src/conf_mode/interfaces-wireguard.py
+++ b/src/conf_mode/interfaces-wireguard.py
@@ -24,8 +24,8 @@ from netifaces import interfaces
from vyos.config import Config
from vyos.configdict import list_diff
from vyos.ifconfig import WireGuardIf
-from vyos.util import chown, is_bridge_member, chmod_750
-from vyos.util import call
+from vyos.util import chown, chmod_750, call
+from vyos.validate import is_bridge_member
from vyos import ConfigError
kdir = r'/config/auth/wireguard'
@@ -35,10 +35,11 @@ default_config_data = {
'address': [],
'address_remove': [],
'description': '',
- 'lport': None,
+ 'listen_port': '',
'deleted': False,
'disable': False,
- 'fwmark': 0x00,
+ 'is_bridge_member': False,
+ 'fwmark': 0,
'mtu': 1420,
'peer': [],
'peer_remove': [], # stores public keys of peers to remove
@@ -80,6 +81,8 @@ def get_config():
# Check if interface has been removed
if not conf.exists(base + [wg['intf']]):
wg['deleted'] = True
+ # check if interface is member if a bridge
+ wg['is_bridge_member'] = is_bridge_member(conf, wg['intf'])
return wg
conf.set_level(base + [wg['intf']])
@@ -103,7 +106,7 @@ def get_config():
# local port to listen on
if conf.exists(['port']):
- wg['lport'] = conf.return_value(['port'])
+ wg['listen_port'] = conf.return_value(['port'])
# fwmark value
if conf.exists(['fwmark']):
@@ -189,12 +192,11 @@ def verify(wg):
interface = wg['intf']
if wg['deleted']:
- is_member, bridge = is_bridge_member(interface)
- if is_member:
- # can not use a f'' formatted-string here as bridge would not get
- # expanded in the print statement
- raise ConfigError('Can not delete interface "{0}" as it ' \
- 'is a member of bridge "{1}"!'.format(interface, bridge))
+ if wg['is_bridge_member']:
+ interface = wg['intf']
+ bridge = wg['is_bridge_member']
+ raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+
return None
vrf_name = wg['vrf']
@@ -220,6 +222,12 @@ def verify(wg):
if not peer['pubkey']:
raise ConfigError(f'Peer public-key required for peer "{peer_name}"!')
+ if peer['address'] and not peer['port']:
+ raise ConfigError(f'Peer "{peer_name}" port must be defined if address is defined!')
+
+ if not peer['address'] and peer['port']:
+ raise ConfigError(f'Peer "{peer_name}" address must be defined if port is defined!')
+
def apply(wg):
# init wg class
@@ -261,8 +269,8 @@ def apply(wg):
# peer allowed-ips
w.config['allowed-ips'] = peer['allowed-ips']
# local listen port
- if wg['lport']:
- w.config['port'] = wg['lport']
+ if wg['listen_port']:
+ w.config['port'] = wg['listen_port']
# fwmark
if c['fwmark']:
w.config['fwmark'] = wg['fwmark']
diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py
index 498c24df0..148a7f6e0 100755
--- a/src/conf_mode/interfaces-wireless.py
+++ b/src/conf_mode/interfaces-wireless.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
@@ -25,12 +25,12 @@ from netaddr import EUI, mac_unix_expanded
from vyos.config import Config
from vyos.configdict import list_diff, vlan_to_dict
-from vyos.ifconfig import WiFiIf
+from vyos.ifconfig import WiFiIf, Section
from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
-from vyos.util import chown, is_bridge_member, call
-from vyos import ConfigError
from vyos.template import render
-
+from vyos.util import chown, call
+from vyos.validate import is_bridge_member
+from vyos import ConfigError
default_config_data = {
'address': [],
@@ -88,9 +88,11 @@ default_config_data = {
'ip_enable_arp_announce': 0,
'ip_enable_arp_ignore': 0,
'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
+ 'ipv6_eui64_prefix': [],
+ 'ipv6_eui64_prefix_remove': [],
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
+ 'is_bridge_member': False,
'mac' : '',
'max_stations' : '',
'mgmt_frame_protection' : 'disabled',
@@ -136,6 +138,8 @@ def get_config():
cfg_base = 'interfaces wireless ' + wifi['intf']
if not conf.exists(cfg_base):
wifi['deleted'] = True
+ # check if interface is member if a bridge
+ wifi['is_bridge_member'] = is_bridge_member(conf, wifi['intf'])
# we can not bail out early as wireless interface can not be removed
# Kernel will complain with: RTNETLINK answers: Operation not supported.
# Thus we need to remove individual settings
@@ -365,9 +369,21 @@ def get_config():
if conf.exists('ipv6 address autoconf'):
wifi['ipv6_autoconf'] = 1
- # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ # Get prefixes for IPv6 addressing based on MAC address (EUI-64)
if conf.exists('ipv6 address eui64'):
- wifi['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+ wifi['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64')
+
+ # Determine currently effective EUI64 addresses - to determine which
+ # address is no longer valid and needs to be removed
+ eff_addr = conf.return_effective_values('ipv6 address eui64')
+ wifi['ipv6_eui64_prefix_remove'] = list_diff(eff_addr, wifi['ipv6_eui64_prefix'])
+
+ # Remove the default link-local address if set.
+ if conf.exists('ipv6 address no-default-link-local'):
+ wifi['ipv6_eui64_prefix_remove'].append('fe80::/64')
+ else:
+ # add the link-local by default to make IPv6 work
+ wifi['ipv6_eui64_prefix'].append('fe80::/64')
# ARP enable ignore
if conf.exists('ip enable-arp-ignore'):
@@ -389,6 +405,12 @@ def get_config():
if conf.exists('mac'):
wifi['mac'] = conf.return_value('mac')
+ # Find out if MAC has changed - if so, we need to delete all IPv6 EUI64 addresses
+ # before re-adding them
+ if ( wifi['mac'] and wifi['intf'] in Section.interfaces(section='wireless')
+ and wifi['mac'] != WiFiIf(wifi['intf'], create=False).get_mac() ):
+ wifi['ipv6_eui64_prefix_remove'] += wifi['ipv6_eui64_prefix']
+
# Maximum number of wireless radio stations
if conf.exists('max-stations'):
wifi['max_stations'] = conf.return_value('max-stations')
@@ -442,6 +464,10 @@ def get_config():
wifi['sec_wpa_cipher'].append('CCMP')
wifi['sec_wpa_cipher'].append('TKIP')
+ # WPA Group Cipher suite
+ if conf.exists('security wpa group-cipher'):
+ wifi['sec_wpa_group_cipher'] = conf.return_values('security wpa group-cipher')
+
# WPA personal shared pass phrase
if conf.exists('security wpa passphrase'):
wifi['sec_wpa_passphrase'] = conf.return_value('security wpa passphrase')
@@ -524,15 +550,12 @@ def get_config():
def verify(wifi):
if wifi['deleted']:
- interface = wifi['intf']
- is_member, bridge = is_bridge_member(interface)
- if is_member:
- # can not use a f'' formatted-string here as bridge would not get
- # expanded in the print statement
- raise ConfigError('Can not delete interface "{0}" as it ' \
- 'is a member of bridge "{1}"!'.format(interface, bridge))
- return None
+ if wifi['is_bridge_member']:
+ interface = wifi['intf']
+ bridge = wifi['is_bridge_member']
+ raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+ return None
if wifi['op_mode'] != 'monitor' and not wifi['ssid']:
raise ConfigError('SSID must be set for {}'.format(wifi['intf']))
@@ -692,6 +715,10 @@ def apply(wifi):
# ignore link state changes
w.set_link_detect(wifi['disable_link_detect'])
+ # Delete old IPv6 EUI64 addresses before changing MAC
+ for addr in wifi['ipv6_eui64_prefix_remove']:
+ w.del_ipv6_eui64_address(addr)
+
# Change interface MAC address - re-set to real hardware address (hw-id)
# if custom mac is removed
if wifi['mac']:
@@ -699,6 +726,10 @@ def apply(wifi):
elif wifi['hw_id']:
w.set_mac(wifi['hw_id'])
+ # Add IPv6 EUI-based addresses
+ for addr in wifi['ipv6_eui64_prefix']:
+ w.add_ipv6_eui64_address(addr)
+
# configure ARP filter configuration
w.set_arp_filter(wifi['ip_disable_arp_filter'])
# configure ARP accept
@@ -709,8 +740,6 @@ def apply(wifi):
w.set_arp_ignore(wifi['ip_enable_arp_ignore'])
# IPv6 address autoconfiguration
w.set_ipv6_autoconf(wifi['ipv6_autoconf'])
- # IPv6 EUI-based address
- w.set_ipv6_eui64_address(wifi['ipv6_eui64_prefix'])
# IPv6 forwarding
w.set_ipv6_forwarding(wifi['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
diff --git a/src/conf_mode/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py
index da1855cd9..a3a2a2648 100755
--- a/src/conf_mode/interfaces-wirelessmodem.py
+++ b/src/conf_mode/interfaces-wirelessmodem.py
@@ -21,14 +21,10 @@ from copy import deepcopy
from netifaces import interfaces
from vyos.config import Config
-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
-
+from vyos.util import chown, chmod_755, cmd, call
+from vyos.validate import is_bridge_member
+from vyos import ConfigError
default_config_data = {
'address': [],
@@ -44,6 +40,7 @@ default_config_data = {
'metric': '10',
'mtu': '1500',
'name_server': True,
+ 'is_bridge_member': False,
'intf': '',
'vrf': ''
}
@@ -70,6 +67,8 @@ def get_config():
# Check if interface has been removed
if not conf.exists('interfaces wirelessmodem ' + wwan['intf']):
wwan['deleted'] = True
+ # check if interface is member if a bridge
+ wwan['is_bridge_member'] = is_bridge_member(conf, wwan['intf'])
return wwan
# set new configuration level
@@ -119,13 +118,11 @@ def get_config():
def verify(wwan):
if wwan['deleted']:
- interface = wwan['intf']
- is_member, bridge = is_bridge_member(interface)
- if is_member:
- # can not use a f'' formatted-string here as bridge would not get
- # expanded in the print statement
- raise ConfigError('Can not delete interface "{0}" as it ' \
- 'is a member of bridge "{1}"!'.format(interface, bridge))
+ if wwan['is_bridge_member']:
+ interface = wwan['intf']
+ bridge = wwan['is_bridge_member']
+ raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+
return None
if not wwan['apn']:
@@ -155,12 +152,6 @@ def generate(wwan):
config_files = [config_wwan, config_wwan_chat, script_wwan_pre_up,
script_wwan_ip_up, script_wwan_ip_down]
- # Ensure directories for config files exist - otherwise create them on demand
- for file in config_files:
- dirname = os.path.dirname(file)
- if not os.path.isdir(dirname):
- os.mkdir(dirname)
-
# Always hang-up WWAN connection prior generating new configuration file
cmd(f'systemctl stop ppp@{intf}.service')
@@ -175,17 +166,18 @@ def generate(wwan):
render(config_wwan, 'wwan/peer.tmpl', wwan)
# Create PPP chat script
render(config_wwan_chat, 'wwan/chat.tmpl', wwan)
+
+ # generated script file must be executable
+
# Create script for ip-pre-up.d
- render(script_wwan_pre_up, 'wwan/ip-pre-up.script.tmpl', wwan)
+ render(script_wwan_pre_up, 'wwan/ip-pre-up.script.tmpl',
+ wwan, permission=0o755)
# Create script for ip-up.d
- render(script_wwan_ip_up, 'wwan/ip-up.script.tmpl', wwan)
+ render(script_wwan_ip_up, 'wwan/ip-up.script.tmpl',
+ wwan, permission=0o755)
# Create script for ip-down.d
- render(script_wwan_ip_down, 'wwan/ip-down.script.tmpl', wwan)
-
- # make generated script file executable
- chmod_755(script_wwan_pre_up)
- chmod_755(script_wwan_ip_up)
- chmod_755(script_wwan_ip_down)
+ render(script_wwan_ip_down, 'wwan/ip-down.script.tmpl',
+ wwan, permission=0o755)
return None
diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py
index 4fffa11ee..3398bcdf2 100755
--- a/src/conf_mode/ipsec-settings.py
+++ b/src/conf_mode/ipsec-settings.py
@@ -28,10 +28,10 @@ from vyos.template import render
ra_conn_name = "remote-access"
charon_conf_file = "/etc/strongswan.d/charon.conf"
-ipsec_secrets_flie = "/etc/ipsec.secrets"
+ipsec_secrets_file = "/etc/ipsec.secrets"
ipsec_ra_conn_dir = "/etc/ipsec.d/tunnels/"
ipsec_ra_conn_file = ipsec_ra_conn_dir + ra_conn_name
-ipsec_conf_flie = "/etc/ipsec.conf"
+ipsec_conf_file = "/etc/ipsec.conf"
ca_cert_path = "/etc/ipsec.d/cacerts"
server_cert_path = "/etc/ipsec.d/certs"
server_key_path = "/etc/ipsec.d/private"
@@ -96,6 +96,24 @@ def get_config():
return data
+def write_ipsec_secrets(c):
+ if c.get("ipsec_l2tp_auth_mode") == "pre-shared-secret":
+ secret_txt = "{0}\n{1} %any : PSK \"{2}\"\n{3}\n".format(delim_ipsec_l2tp_begin, c['outside_addr'], c['ipsec_l2tp_secret'], delim_ipsec_l2tp_end)
+ elif data.get("ipsec_l2tp_auth_mode") == "x509":
+ secret_txt = "{0}\n: RSA {1}\n{2}\n".format(delim_ipsec_l2tp_begin, c['server_key_file_copied'], delim_ipsec_l2tp_end)
+
+ old_umask = os.umask(0o077)
+ with open(ipsec_secrets_file, 'a+') as f:
+ f.write(secret_txt)
+ os.umask(old_umask)
+
+def write_ipsec_conf(c):
+ ipsec_confg_txt = "{0}\ninclude {1}\n{2}\n".format(delim_ipsec_l2tp_begin, ipsec_ra_conn_file, delim_ipsec_l2tp_end)
+
+ old_umask = os.umask(0o077)
+ with open(ipsec_conf_file, 'a+') as f:
+ f.write(ipsec_confg_txt)
+ os.umask(old_umask)
### Remove config from file by delimiter
def remove_confs(delim_begin, delim_end, conf_file):
@@ -150,11 +168,12 @@ def generate(data):
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)
-
- old_umask = os.umask(0o077)
- render(ipsec_secrets_flie, 'ipsec/ipsec.secrets.tmpl', c, trim_blocks=True)
- os.umask(old_umask)
+ remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_file)
+ # old_umask = os.umask(0o077)
+ # render(ipsec_secrets_file, 'ipsec/ipsec.secrets.tmpl', data, trim_blocks=True)
+ # os.umask(old_umask)
+ ## Use this method while IPSec CLI handler won't be overwritten to python
+ write_ipsec_secrets(data)
old_umask = os.umask(0o077)
@@ -162,18 +181,21 @@ def generate(data):
if not os.path.exists(ipsec_ra_conn_dir):
os.makedirs(ipsec_ra_conn_dir)
- render(ipsec_ra_conn_file, 'ipsec/remote-access.tmpl', c, trim_blocks=True)
+ render(ipsec_ra_conn_file, 'ipsec/remote-access.tmpl', data, trim_blocks=True)
os.umask(old_umask)
- old_umask = os.umask(0o077)
- render(ipsec_conf_flie, 'ipsec/ipsec.conf.tmpl', c, trim_blocks=True)
- os.umask(old_umask)
+ remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_file)
+ # old_umask = os.umask(0o077)
+ # render(ipsec_conf_file, 'ipsec/ipsec.conf.tmpl', data, trim_blocks=True)
+ # os.umask(old_umask)
+ ## Use this method while IPSec CLI handler won't be overwritten to python
+ write_ipsec_conf(data)
else:
if os.path.exists(ipsec_ra_conn_file):
remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_ra_conn_file)
- remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_flie)
- remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_flie)
+ remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_file)
+ remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_file)
def restart_ipsec():
call('ipsec restart >&/dev/null')
diff --git a/src/conf_mode/protocols_static_multicast.py b/src/conf_mode/protocols_static_multicast.py
new file mode 100755
index 000000000..411a130ec
--- /dev/null
+++ b/src/conf_mode/protocols_static_multicast.py
@@ -0,0 +1,115 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from ipaddress import IPv4Address
+from sys import exit
+
+from vyos import ConfigError
+from vyos.config import Config
+from vyos.util import call
+from vyos.template import render
+
+
+config_file = r'/tmp/static_mcast.frr'
+
+# Get configuration for static multicast route
+def get_config():
+ conf = Config()
+ mroute = {
+ 'old_mroute' : {},
+ 'mroute' : {}
+ }
+
+ base_path = "protocols static multicast"
+
+ if not (conf.exists(base_path) or conf.exists_effective(base_path)):
+ return None
+
+ conf.set_level(base_path)
+
+ # Get multicast effective routes
+ for route in conf.list_effective_nodes('route'):
+ mroute['old_mroute'][route] = {}
+ for next_hop in conf.list_effective_nodes('route {0} next-hop'.format(route)):
+ mroute['old_mroute'][route].update({
+ next_hop : conf.return_value('route {0} next-hop {1} distance'.format(route, next_hop))
+ })
+
+ # Get multicast effective interface-routes
+ for route in conf.list_effective_nodes('interface-route'):
+ if not route in mroute['old_mroute']:
+ mroute['old_mroute'][route] = {}
+ for next_hop in conf.list_effective_nodes('interface-route {0} next-hop-interface'.format(route)):
+ mroute['old_mroute'][route].update({
+ next_hop : conf.return_value('interface-route {0} next-hop-interface {1} distance'.format(route, next_hop))
+ })
+
+ # Get multicast routes
+ for route in conf.list_nodes('route'):
+ mroute['mroute'][route] = {}
+ for next_hop in conf.list_nodes('route {0} next-hop'.format(route)):
+ mroute['mroute'][route].update({
+ next_hop : conf.return_value('route {0} next-hop {1} distance'.format(route, next_hop))
+ })
+
+ # Get multicast interface-routes
+ for route in conf.list_nodes('interface-route'):
+ if not route in mroute['mroute']:
+ mroute['mroute'][route] = {}
+ for next_hop in conf.list_nodes('interface-route {0} next-hop-interface'.format(route)):
+ mroute['mroute'][route].update({
+ next_hop : conf.return_value('interface-route {0} next-hop-interface {1} distance'.format(route, next_hop))
+ })
+
+ return mroute
+
+def verify(mroute):
+ if mroute is None:
+ return None
+
+ for route in mroute['mroute']:
+ route = route.split('/')
+ if IPv4Address(route[0]) < IPv4Address('224.0.0.0'):
+ raise ConfigError(route + " not a multicast network")
+
+def generate(mroute):
+ if mroute is None:
+ return None
+
+ render(config_file, 'frr-mcast/static_mcast.frr.tmpl', mroute)
+ return None
+
+def apply(mroute):
+ if mroute is None:
+ return None
+
+ if os.path.exists(config_file):
+ call("sudo vtysh -d staticd -f " + config_file)
+ os.remove(config_file)
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/salt-minion.py b/src/conf_mode/salt-minion.py
index 236480854..8bc35bb45 100755
--- a/src/conf_mode/salt-minion.py
+++ b/src/conf_mode/salt-minion.py
@@ -17,117 +17,102 @@
import os
from copy import deepcopy
-from pwd import getpwnam
from socket import gethostname
from sys import exit
from urllib3 import PoolManager
from vyos.config import Config
-from vyos import ConfigError
-from vyos.util import call
from vyos.template import render
-
+from vyos.util import call, chown
+from vyos import ConfigError
config_file = r'/etc/salt/minion'
+master_keyfile = r'/opt/vyatta/etc/config/salt/pki/minion/master_sign.pub'
default_config_data = {
- 'hash_type': 'sha256',
- 'log_file': '/var/log/salt/minion',
+ 'hash': 'sha256',
'log_level': 'warning',
'master' : 'salt',
'user': 'minion',
+ 'group': 'vyattacfg',
'salt_id': gethostname(),
'mine_interval': '60',
- 'verify_master_pubkey_sign': 'false'
+ 'verify_master_pubkey_sign': 'false',
+ 'master_key': ''
}
def get_config():
salt = deepcopy(default_config_data)
conf = Config()
- if not conf.exists('service salt-minion'):
+ base = ['service', 'salt-minion']
+
+ if not conf.exists(base):
return None
else:
- conf.set_level('service salt-minion')
-
- if conf.exists('hash_type'):
- salt['hash_type'] = conf.return_value('hash_type')
-
- if conf.exists('log_file'):
- salt['log_file'] = conf.return_value('log_file')
+ conf.set_level(base)
- if conf.exists('log_level'):
- salt['log_level'] = conf.return_value('log_level')
+ if conf.exists(['hash']):
+ salt['hash'] = conf.return_value(['hash'])
- if conf.exists('master'):
- master = conf.return_values('master')
- salt['master'] = master
+ if conf.exists(['master']):
+ salt['master'] = conf.return_values(['master'])
- if conf.exists('id'):
- salt['salt_id'] = conf.return_value('id')
+ if conf.exists(['id']):
+ salt['salt_id'] = conf.return_value(['id'])
- if conf.exists('user'):
- salt['user'] = conf.return_value('user')
+ if conf.exists(['user']):
+ salt['user'] = conf.return_value(['user'])
- if conf.exists('mine_interval'):
- salt['mine_interval'] = conf.return_value('mine_interval')
+ if conf.exists(['interval']):
+ salt['interval'] = conf.return_value(['interval'])
- salt['master-key'] = None
- if conf.exists('master-key'):
- salt['master-key'] = conf.return_value('master-key')
+ if conf.exists(['master-key']):
+ salt['master_key'] = conf.return_value(['master-key'])
salt['verify_master_pubkey_sign'] = 'true'
return salt
-def generate(salt):
- paths = ['/etc/salt/','/var/run/salt','/opt/vyatta/etc/config/salt/']
- directory = '/opt/vyatta/etc/config/salt/pki/minion'
- uid = getpwnam(salt['user']).pw_uid
- http = PoolManager()
+def verify(salt):
+ return None
- if salt is None:
+def generate(salt):
+ if not salt:
return None
- if not os.path.exists(directory):
- os.makedirs(directory)
-
- render(config_file, 'salt-minion/minion.tmpl', salt)
-
- path = "/etc/salt/"
- for path in paths:
- for root, dirs, files in os.walk(path):
- for usgr in dirs:
- os.chown(os.path.join(root, usgr), uid, 100)
- for usgr in files:
- os.chown(os.path.join(root, usgr), uid, 100)
+ render(config_file, 'salt-minion/minion.tmpl', salt,
+ user=salt['user'], group=salt['group'])
- if not os.path.exists('/opt/vyatta/etc/config/salt/pki/minion/master_sign.pub'):
- if not salt['master-key'] is None:
- r = http.request('GET', salt['master-key'], preload_content=False)
+ if not os.path.exists(master_keyfile):
+ if salt['master_key']:
+ req = PoolManager().request('GET', salt['master_key'], preload_content=False)
- with open('/opt/vyatta/etc/config/salt/pki/minion/master_sign.pub', 'wb') as out:
+ with open(master_keyfile, 'wb') as f:
while True:
- data = r.read(1024)
+ data = req.read(1024)
if not data:
break
- out.write(data)
+ f.write(data)
- r.release_conn()
+ req.release_conn()
+ chown(master_keyfile, salt['user'], salt['group'])
return None
def apply(salt):
- if salt is not None:
- call("sudo systemctl restart salt-minion")
+ if not salt:
+ # Salt removed from running config
+ call('systemctl stop salt-minion.service')
+ if os.path.exists(config_file):
+ os.unlink(config_file)
else:
- # Salt access is removed in the commit
- call("sudo systemctl stop salt-minion")
- os.unlink(config_file)
+ call('systemctl restart salt-minion.service')
return None
if __name__ == '__main__':
try:
c = get_config()
+ verify(c)
generate(c)
apply(c)
except ConfigError as e:
diff --git a/src/conf_mode/service-ipoe.py b/src/conf_mode/service-ipoe.py
deleted file mode 100755
index 76aa80a10..000000000
--- a/src/conf_mode/service-ipoe.py
+++ /dev/null
@@ -1,284 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-import os
-import re
-
-from sys import exit
-from time import sleep
-
-from stat import S_IRUSR, S_IWUSR, S_IRGRP
-from vyos.config import Config
-from vyos import ConfigError
-from vyos.util import call
-from vyos.template import render
-
-
-ipoe_conf = '/run/accel-pppd/ipoe.conf'
-ipoe_chap_secrets = '/run/accel-pppd/ipoe.chap-secrets'
-
-
-def _get_cpu():
- cpu_cnt = 1
- if os.cpu_count() == 1:
- cpu_cnt = 1
- else:
- cpu_cnt = int(os.cpu_count()/2)
- return cpu_cnt
-
-
-def get_config():
- c = Config()
- if not c.exists(['service', 'ipoe-server']):
- return None
-
- config_data = {
- 'chap_secrets_file' : ipoe_chap_secrets
- }
-
- c.set_level(['service', 'ipoe-server'])
- config_data['interfaces'] = {}
- for intfc in c.list_nodes(['interface']):
- config_data['interfaces'][intfc] = {
- 'mode': 'L2',
- 'shared': '1',
- # may need a conifg option, can be dhcpv4 or up for unclassified pkts
- 'sess_start': 'dhcpv4',
- 'range': None,
- 'ifcfg': '1',
- 'vlan_mon': []
- }
- config_data['dns'] = {
- 'server1': None,
- 'server2': None
- }
- config_data['dnsv6'] = {
- 'server1': None,
- 'server2': None,
- 'server3': None
- }
- config_data['ipv6'] = {
- 'prfx': [],
- 'pd': [],
- }
- config_data['auth'] = {
- 'auth_if': {},
- 'mech': 'noauth',
- 'radius': {},
- 'radsettings': {
- 'dae-server': {}
- }
- }
-
- if c.exists(['interface', intfc, 'network-mode']):
- config_data['interfaces'][intfc]['mode'] = c.return_value(
- ['interface', intfc, 'network-mode'])
- if c.return_value(['interface', intfc, 'network']) == 'vlan':
- config_data['interfaces'][intfc]['shared'] = '0'
- if c.exists(['interface', intfc, 'vlan-id']):
- config_data['interfaces'][intfc]['vlan_mon'] += c.return_values(
- ['interface', intfc, 'vlan-id'])
- if c.exists(['interface', intfc, 'vlan-range']):
- config_data['interfaces'][intfc]['vlan_mon'] += c.return_values(
- ['interface', intfc, 'vlan-range'])
- if c.exists(['interface', intfc, 'client-subnet']):
- config_data['interfaces'][intfc]['range'] = c.return_value(
- ['interface', intfc, 'client-subnet'])
- if c.exists(['dns-server', 'server-1']):
- config_data['dns']['server1'] = c.return_value(
- ['dns-server', 'server-1'])
- if c.exists(['dns-server', 'server-2']):
- config_data['dns']['server2'] = c.return_value(
- ['dns-server', 'server-2'])
- if c.exists(['dnsv6-server', 'server-1']):
- config_data['dnsv6']['server1'] = c.return_value(
- ['dnsv6-server', 'server-1'])
- if c.exists(['dnsv6-server', 'server-2']):
- config_data['dnsv6']['server2'] = c.return_value(
- ['dnsv6-server', 'server-2'])
- if c.exists(['dnsv6-server', 'server-3']):
- config_data['dnsv6']['server3'] = c.return_value(
- ['dnsv6-server', 'server-3'])
- if not c.exists(['authentication', 'mode', 'noauth']):
- config_data['auth']['mech'] = c.return_value(
- ['authentication', 'mode'])
- if c.exists(['authentication', 'mode', 'local']):
- for auth_int in c.list_nodes(['authentication', 'interface']):
- for mac in c.list_nodes(['authentication', 'interface', auth_int, 'mac-address']):
- config_data['auth']['auth_if'][auth_int] = {}
- if c.exists(['authentication', 'interface', auth_int, 'mac-address', mac, 'rate-limit']):
- config_data['auth']['auth_if'][auth_int][mac] = {}
- config_data['auth']['auth_if'][auth_int][mac]['up'] = c.return_value(
- ['authentication', 'interface', auth_int, 'mac-address', mac, 'rate-limit upload'])
- config_data['auth']['auth_if'][auth_int][mac]['down'] = c.return_value(
- ['authentication', 'interface', auth_int, 'mac-address', 'mac', 'rate-limit download'])
- else:
- config_data['auth']['auth_if'][auth_int][mac] = {}
- config_data['auth']['auth_if'][auth_int][mac]['up'] = None
- config_data['auth']['auth_if'][auth_int][mac]['down'] = None
- # client vlan-id
- if c.exists(['authentication', 'interface', auth_int, 'mac-address', mac, 'vlan-id']):
- config_data['auth']['auth_if'][auth_int][mac]['vlan'] = c.return_value(
- ['authentication', 'interface', auth_int, 'mac-address', mac, 'vlan-id'])
- if c.exists(['authentication', 'mode', 'radius']):
- for rsrv in c.list_nodes(['authentication', 'radius-server']):
- config_data['auth']['radius'][rsrv] = {}
- if c.exists(['authentication', 'radius-server', rsrv, 'secret']):
- config_data['auth']['radius'][rsrv]['secret'] = c.return_value(
- ['authentication', 'radius-server', rsrv, 'secret'])
- else:
- config_data['auth']['radius'][rsrv]['secret'] = None
- if c.exists(['authentication', 'radius-server', rsrv, 'fail-time']):
- config_data['auth']['radius'][rsrv]['fail-time'] = c.return_value(
- ['authentication', 'radius-server', rsrv, 'fail-time'])
- else:
- config_data['auth']['radius'][rsrv]['fail-time'] = '0'
- if c.exists(['authentication', 'radius-server', rsrv, 'req-limit']):
- config_data['auth']['radius'][rsrv]['req-limit'] = c.return_value(
- ['authentication', 'radius-server', rsrv, 'req-limit'])
- else:
- config_data['auth']['radius'][rsrv]['req-limit'] = '0'
- if c.exists(['authentication', 'radius-settings']):
- if c.exists(['authentication', 'radius-settings', 'timeout']):
- config_data['auth']['radsettings']['timeout'] = c.return_value(
- ['authentication', 'radius-settings', 'timeout'])
- if c.exists(['authentication', 'radius-settings', 'nas-ip-address']):
- config_data['auth']['radsettings']['nas-ip-address'] = c.return_value(
- ['authentication', 'radius-settings', 'nas-ip-address'])
- if c.exists(['authentication', 'radius-settings', 'nas-identifier']):
- config_data['auth']['radsettings']['nas-identifier'] = c.return_value(
- ['authentication', 'radius-settings', 'nas-identifier'])
- if c.exists(['authentication', 'radius-settings', 'max-try']):
- config_data['auth']['radsettings']['max-try'] = c.return_value(
- ['authentication', 'radius-settings', 'max-try'])
- if c.exists(['authentication', 'radius-settings', 'acct-timeout']):
- config_data['auth']['radsettings']['acct-timeout'] = c.return_value(
- ['authentication', 'radius-settings', 'acct-timeout'])
- if c.exists(['authentication', 'radius-settings', 'dae-server', 'ip-address']):
- config_data['auth']['radsettings']['dae-server']['ip-address'] = c.return_value(
- ['authentication', 'radius-settings', 'dae-server', 'ip-address'])
- if c.exists(['authentication', 'radius-settings', 'dae-server', 'port']):
- config_data['auth']['radsettings']['dae-server']['port'] = c.return_value(
- ['authentication', 'radius-settings', 'dae-server', 'port'])
- if c.exists(['authentication', 'radius-settings', 'dae-server', 'secret']):
- config_data['auth']['radsettings']['dae-server']['secret'] = c.return_value(
- ['authentication', 'radius-settings', 'dae-server', 'secret'])
-
- if c.exists(['client-ipv6-pool', 'prefix']):
- config_data['ipv6']['prfx'] = c.return_values(
- ['client-ipv6-pool', 'prefix'])
- if c.exists(['client-ipv6-pool', 'delegate-prefix']):
- config_data['ipv6']['pd'] = c.return_values(
- ['client-ipv6-pool', 'delegate-prefix'])
-
- return config_data
-
-
-def generate(ipoe):
- if not ipoe:
- return None
-
- dirname = os.path.dirname(ipoe_conf)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
-
- ipoe['thread_cnt'] = _get_cpu()
- render(ipoe_conf, 'ipoe-server/ipoe.config.tmpl', ipoe, trim_blocks=True)
-
- if ipoe['auth']['mech'] == 'local':
- render(ipoe_chap_secrets, 'ipoe-server/chap-secrets.tmpl', ipoe)
- os.chmod(ipoe_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP)
-
- else:
- if os.path.exists(ipoe_chap_secrets):
- os.unlink(ipoe_chap_secrets)
-
- return None
-
-
-def verify(c):
- if c == None or not c:
- return None
-
- if not c['interfaces']:
- raise ConfigError("service ipoe-server interface requires a value")
-
- for intfc in c['interfaces']:
- if not c['interfaces'][intfc]['range']:
- raise ConfigError("service ipoe-server interface " +
- intfc + " client-subnet needs a value")
-
- if c['auth']['mech'] == 'radius':
- if not c['auth']['radius']:
- raise ConfigError(
- "service ipoe-server authentication radius-server requires a value for authentication mode radius")
- else:
- for radsrv in c['auth']['radius']:
- if not c['auth']['radius'][radsrv]['secret']:
- raise ConfigError(
- "service ipoe-server authentication radius-server " + radsrv + " secret requires a value")
-
- if c['auth']['radsettings']['dae-server']:
- try:
- if c['auth']['radsettings']['dae-server']['ip-address']:
- pass
- except:
- raise ConfigError(
- "service ipoe-server authentication radius-settings dae-server ip-address value required")
- try:
- if c['auth']['radsettings']['dae-server']['secret']:
- pass
- except:
- raise ConfigError(
- "service ipoe-server authentication radius-settings dae-server secret value required")
- try:
- if c['auth']['radsettings']['dae-server']['port']:
- pass
- except:
- raise ConfigError(
- "service ipoe-server authentication radius-settings dae-server port value required")
-
- if len(c['ipv6']['pd']) != 0 and len(c['ipv6']['prfx']) == 0:
- raise ConfigError(
- "service ipoe-server client-ipv6-pool prefix needs a value")
-
- return c
-
-
-def apply(ipoe):
- if ipoe == None:
- call('systemctl stop accel-ppp@ipoe.service')
-
- if os.path.exists(ipoe_conf):
- os.unlink(ipoe_conf)
-
- if os.path.exists(ipoe_chap_secrets):
- os.unlink(ipoe_chap_secrets)
-
- return None
-
- call('systemctl restart accel-ppp@ipoe.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/service-pppoe.py b/src/conf_mode/service-pppoe.py
deleted file mode 100755
index a96249199..000000000
--- a/src/conf_mode/service-pppoe.py
+++ /dev/null
@@ -1,428 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-import os
-import re
-
-from socket import socket, AF_INET, SOCK_STREAM
-from sys import exit
-from time import sleep
-
-from vyos.config import Config
-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'
-chap_secrets = pppoe_cnf_dir + '/chap-secrets'
-pppoe_conf = pppoe_cnf_dir + '/pppoe.config'
-# accel-pppd -d -c /etc/accel-ppp/pppoe/pppoe.config -p
-# /var/run/accel_pppoe.pid
-
-# config path creation
-if not os.path.exists(pppoe_cnf_dir):
- os.makedirs(pppoe_cnf_dir)
-
-#
-# 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(AF_INET, SOCK_STREAM)
- while True:
- try:
- s.connect(("127.0.0.1", 2001))
- break
- except ConnectionRefusedError:
- sleep(0.5)
- cnt += 1
- if cnt == 100:
- raise("failed to start pppoe server")
-
-
-def _accel_cmd(command):
- return run(f'/usr/bin/accel-cmd {command}')
-
-
-def get_config():
- c = Config()
- if not c.exists('service pppoe-server'):
- return None
-
- config_data = {
- 'concentrator': 'vyos-ac',
- 'authentication': {
- 'local-users': {
- },
- 'mode': 'local',
- 'radiussrv': {},
- 'radiusopt': {}
- },
- 'client_ip_pool': '',
- 'client_ip_subnets': [],
- 'client_ipv6_pool': {},
- 'interface': {},
- 'ppp_gw': '',
- 'svc_name': [],
- 'dns': [],
- 'dnsv6': [],
- 'wins': [],
- 'mtu': '1492',
- 'ppp_options': {},
- 'limits': {},
- 'snmp': 'disable',
- 'sesscrtl': 'replace',
- 'pado_delay': ''
- }
-
- c.set_level(['service', 'pppoe-server'])
- # general options
- if c.exists(['access-concentrator']):
- config_data['concentrator'] = c.return_value(['access-concentrator'])
- if c.exists(['service-name']):
- config_data['svc_name'] = c.return_values(['service-name'])
- if c.exists(['interface']):
- for intfc in c.list_nodes(['interface']):
- config_data['interface'][intfc] = {'vlans': []}
- if c.exists(['interface', intfc, 'vlan-id']):
- config_data['interface'][intfc]['vlans'] += c.return_values(
- ['interface', intfc, 'vlan-id'])
- if c.exists(['interface', intfc, 'vlan-range']):
- config_data['interface'][intfc]['vlans'] += c.return_values(
- ['interface', intfc, 'vlan-range'])
- if c.exists(['local-ip']):
- config_data['ppp_gw'] = c.return_value(['local-ip'])
- if c.exists(['dns-servers']):
- if c.return_value(['dns-servers', 'server-1']):
- config_data['dns'].append(
- c.return_value(['dns-servers', 'server-1']))
- if c.return_value(['dns-servers', 'server-2']):
- config_data['dns'].append(
- c.return_value(['dns-servers', 'server-2']))
- if c.exists(['dnsv6-servers']):
- if c.return_value(['dnsv6-servers', 'server-1']):
- config_data['dnsv6'].append(
- c.return_value(['dnsv6-servers', 'server-1']))
- if c.return_value(['dnsv6-servers', 'server-2']):
- config_data['dnsv6'].append(
- c.return_value(['dnsv6-servers', 'server-2']))
- if c.return_value(['dnsv6-servers', 'server-3']):
- config_data['dnsv6'].append(
- c.return_value(['dnsv6-servers', 'server-3']))
- if c.exists(['wins-servers']):
- if c.return_value(['wins-servers', 'server-1']):
- config_data['wins'].append(
- c.return_value(['wins-servers', 'server-1']))
- if c.return_value(['wins-servers', 'server-2']):
- config_data['wins'].append(
- c.return_value(['wins-servers', 'server-2']))
- if c.exists(['client-ip-pool']):
- if c.exists(['client-ip-pool', 'start']):
- config_data['client_ip_pool'] = c.return_value(
- ['client-ip-pool start'])
- if c.exists(['client-ip-pool stop']):
- config_data['client_ip_pool'] += '-' + re.search(
- '[0-9]+$', c.return_value(['client-ip-pool', 'stop'])).group(0)
- else:
- raise ConfigError('client ip pool stop required')
- 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'])
- if c.exists(['client-ipv6-pool', 'delegate-prefix']):
- config_data['client_ipv6_pool']['delegate-prefix'] = c.return_values(
- ['client-ipv6-pool', 'delegate-prefix'])
- if c.exists(['limits']):
- if c.exists(['limits', 'burst']):
- config_data['limits']['burst'] = str(
- c.return_value(['limits', 'burst']))
- if c.exists(['limits', 'timeout']):
- config_data['limits']['timeout'] = str(
- c.return_value(['limits', 'timeout']))
- if c.exists(['limits', 'connection-limit']):
- config_data['limits']['conn-limit'] = str(
- c.return_value(['limits', 'connection-limit']))
- if c.exists(['snmp']):
- config_data['snmp'] = 'enable'
- if c.exists(['snmp', 'master-agent']):
- config_data['snmp'] = 'enable-ma'
-
- # authentication mode local
- if not c.exists(['authentication', 'mode']):
- raise ConfigError('pppoe-server authentication mode required')
-
- 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': None,
- 'state': 'enabled',
- 'ip': '*',
- 'upload': None,
- 'download': None
- }
- }
- )
- if c.exists(['authentication', 'local-users', 'username', usr, 'password']):
- config_data['authentication']['local-users'][usr]['passwd'] = c.return_value(
- ['authentication', 'local-users', 'username', usr, 'password'])
- if c.exists(['authentication', 'local-users', 'username', usr, 'disable']):
- config_data['authentication'][
- 'local-users'][usr]['state'] = 'disable'
- if c.exists(['authentication', 'local-users', 'username', usr, 'static-ip']):
- config_data['authentication']['local-users'][usr]['ip'] = c.return_value(
- ['authentication', 'local-users', 'username', usr, 'static-ip'])
- if c.exists(['authentication', 'local-users', 'username', usr, 'rate-limit', 'download']):
- config_data['authentication']['local-users'][usr]['download'] = c.return_value(
- ['authentication', 'local-users', 'username', usr, 'rate-limit', 'download'])
- if c.exists(['authentication', 'local-users', 'username', usr, 'rate-limit', 'upload']):
- config_data['authentication']['local-users'][usr]['upload'] = c.return_value(
- ['authentication', 'local-users', 'username', usr, 'rate-limit', 'upload'])
-
- # 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, 'secret']),
- 'fail-time': ftime,
- 'req-limit': reql
- }
- }
- )
-
- # advanced radius-setting
- if c.exists(['authentication', 'radius-settings']):
- if c.exists(['authentication', 'radius-settings', 'acct-timeout']):
- config_data['authentication']['radiusopt']['acct-timeout'] = c.return_value(
- ['authentication', 'radius-settings', 'acct-timeout'])
- if c.exists(['authentication', 'radius-settings', 'max-try']):
- config_data['authentication']['radiusopt'][
- 'max-try'] = c.return_value(['authentication', 'radius-settings', 'max-try'])
- if c.exists(['authentication', 'radius-settings', 'timeout']):
- config_data['authentication']['radiusopt'][
- 'timeout'] = c.return_value(['authentication', 'radius-settings', 'timeout'])
- if c.exists(['authentication', 'radius-settings', 'nas-identifier']):
- config_data['authentication']['radiusopt']['nas-id'] = c.return_value(
- ['authentication', 'radius-settings', 'nas-identifier'])
- if c.exists(['authentication', 'radius-settings', 'nas-ip-address']):
- config_data['authentication']['radiusopt']['nas-ip'] = c.return_value(
- ['authentication', 'radius-settings', 'nas-ip-address'])
- if c.exists(['authentication', 'radius-settings', 'dae-server']):
- config_data['authentication']['radiusopt'].update(
- {
- 'dae-srv': {
- 'ip-addr': c.return_value(['authentication', 'radius-settings', 'dae-server', 'ip-address']),
- 'port': c.return_value(['authentication', 'radius-settings', 'dae-server', 'port']),
- 'secret': str(c.return_value(['authentication', 'radius-settings', 'dae-server', 'secret']))
- }
- }
- )
- # 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-settings', 'rate-limit', 'enable']):
- if not c.exists(['authentication', 'radius-settings', 'rate-limit', 'attribute']):
- config_data['authentication']['radiusopt']['shaper'] = {
- 'attr': 'Filter-Id'
- }
- else:
- config_data['authentication']['radiusopt']['shaper'] = {
- 'attr': c.return_value(['authentication', 'radius-settings', 'rate-limit', 'attribute'])
- }
- if c.exists(['authentication', 'radius-settings', 'rate-limit', 'vendor']):
- config_data['authentication']['radiusopt']['shaper'][
- 'vendor'] = c.return_value(['authentication', 'radius-settings', 'rate-limit', 'vendor'])
-
- if c.exists(['mtu']):
- config_data['mtu'] = c.return_value(['mtu'])
-
- # ppp_options
- ppp_options = {}
- if c.exists(['ppp-options']):
- if c.exists(['ppp-options', 'ccp']):
- ppp_options['ccp'] = c.return_value(['ppp-options', 'ccp'])
- if c.exists(['ppp-options', 'min-mtu']):
- ppp_options['min-mtu'] = c.return_value(['ppp-options', 'min-mtu'])
- if c.exists(['ppp-options', 'mru']):
- ppp_options['mru'] = c.return_value(['ppp-options', 'mru'])
- if c.exists(['ppp-options', 'mppe deny']):
- ppp_options['mppe'] = 'deny'
- if c.exists(['ppp-options', 'mppe', 'require']):
- ppp_options['mppe'] = 'require'
- if c.exists(['ppp-options', 'mppe', 'prefer']):
- ppp_options['mppe'] = 'prefer'
- 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 c.exists(['ppp-options', 'ipv4']):
- ppp_options['ipv4'] = c.return_value(['ppp-options', 'ipv4'])
- if c.exists(['ppp-options', 'ipv6']):
- ppp_options['ipv6'] = c.return_value(['ppp-options', 'ipv6'])
- if c.exists(['ppp-options', 'ipv6-accept-peer-intf-id']):
- ppp_options['ipv6-accept-peer-intf-id'] = 1
- if c.exists(['ppp-options', 'ipv6-intf-id']):
- ppp_options['ipv6-intf-id'] = c.return_value(
- ['ppp-options', 'ipv6-intf-id'])
- if c.exists(['ppp-options', 'ipv6-peer-intf-id']):
- ppp_options['ipv6-peer-intf-id'] = c.return_value(
- ['ppp-options', 'ipv6-peer-intf-id'])
- if c.exists(['ppp-options', 'lcp-echo-timeout']):
- ppp_options['lcp-echo-timeout'] = c.return_value(
- ['ppp-options', 'lcp-echo-timeout'])
-
- if len(ppp_options) != 0:
- config_data['ppp_options'] = ppp_options
-
- if c.exists(['session-control']):
- config_data['sesscrtl'] = c.return_value(['session-control'])
-
- if c.exists(['pado-delay']):
- config_data['pado_delay'] = '0'
- a = {}
- for id in c.list_nodes(['pado-delay']):
- if not c.return_value(['pado-delay', id, 'sessions']):
- a[id] = 0
- else:
- a[id] = c.return_value(['pado-delay', id, 'sessions'])
-
- for k in sorted(a.keys()):
- if k != sorted(a.keys())[-1]:
- config_data['pado_delay'] += ",{0}:{1}".format(k, a[k])
- else:
- config_data['pado_delay'] += ",{0}:{1}".format('-1', a[k])
-
- return config_data
-
-
-def verify(c):
- if c == None:
- return None
- # vertify auth settings
- if c['authentication']['mode'] == 'local':
- if not c['authentication']['local-users']:
- raise ConfigError(
- 'pppoe-server authentication local-users required')
-
- for usr in c['authentication']['local-users']:
- if not c['authentication']['local-users'][usr]['passwd']:
- raise ConfigError('user ' + usr + ' requires a password')
- # if up/download is set, check that both have a value
- if c['authentication']['local-users'][usr]['upload']:
- if not c['authentication']['local-users'][usr]['download']:
- raise ConfigError(
- 'user ' + usr + ' requires download speed value')
- if c['authentication']['local-users'][usr]['download']:
- if not c['authentication']['local-users'][usr]['upload']:
- raise ConfigError(
- 'user ' + usr + ' requires upload speed value')
-
- if 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')
-
- # local ippool and gateway settings config checks
-
- if c['client_ip_subnets'] or c['client_ip_pool']:
- if not c['ppp_gw']:
- raise ConfigError('pppoe-server local-ip required')
-
- if c['ppp_gw'] and not c['client_ip_subnets'] and not c['client_ip_pool']:
- print ("Warning: No pppoe client IPv4 pool defined")
-
-
-def generate(c):
- if c == None:
- return None
-
- # accel-cmd reload doesn't work so any change results in a restart of the
- # daemon
- try:
- if os.cpu_count() == 1:
- c['thread_cnt'] = 1
- else:
- c['thread_cnt'] = int(os.cpu_count() / 2)
- except KeyError:
- if os.cpu_count() == 1:
- c['thread_cnt'] = 1
- else:
- c['thread_cnt'] = int(os.cpu_count() / 2)
-
- render(pppoe_conf, 'pppoe-server/pppoe.config.tmpl', c, trim_blocks=True)
-
- if c['authentication']['local-users']:
- old_umask = os.umask(0o077)
- render(chap_secrets, 'pppoe-server/chap-secrets.tmpl', c, trim_blocks=True)
- 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 {pppoe_conf} -p {pidfile} -d')
- _chk_con()
- if ret != 0 and os.path.exists(pidfile):
- os.remove(pidfile)
- raise ConfigError('accel-pppd failed to start')
- else:
- _accel_cmd('restart')
-
-
-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/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py
new file mode 100755
index 000000000..b53692d37
--- /dev/null
+++ b/src/conf_mode/service_ipoe-server.py
@@ -0,0 +1,300 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import re
+
+from copy import deepcopy
+from stat import S_IRUSR, S_IWUSR, S_IRGRP
+from sys import exit
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.util import call, get_half_cpus
+from vyos.validate import is_ipv4
+from vyos import ConfigError
+
+ipoe_conf = '/run/accel-pppd/ipoe.conf'
+ipoe_chap_secrets = '/run/accel-pppd/ipoe.chap-secrets'
+
+default_config_data = {
+ 'auth_mode': 'local',
+ 'auth_interfaces': [],
+ 'chap_secrets_file': ipoe_chap_secrets, # used in Jinja2 template
+ 'interfaces': [],
+ 'dnsv4': [],
+ 'dnsv6': [],
+ 'client_ipv6_pool': [],
+ 'client_ipv6_delegate_prefix': [],
+ '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': '',
+ 'thread_cnt': get_half_cpus()
+}
+
+def get_config():
+ conf = Config()
+ base_path = ['service', 'ipoe-server']
+ if not conf.exists(base_path):
+ return None
+
+ conf.set_level(base_path)
+ ipoe = deepcopy(default_config_data)
+
+ for interface in conf.list_nodes(['interface']):
+ tmp = {
+ 'mode': 'L2',
+ 'name': interface,
+ 'shared': '1',
+ # may need a config option, can be dhcpv4 or up for unclassified pkts
+ 'sess_start': 'dhcpv4',
+ 'range': None,
+ 'ifcfg': '1',
+ 'vlan_mon': []
+ }
+
+ conf.set_level(base_path + ['interface', interface])
+
+ if conf.exists(['network-mode']):
+ tmp['mode'] = conf.return_value(['network-mode'])
+
+ if conf.exists(['network']):
+ mode = conf.return_value(['network'])
+ if mode == 'vlan':
+ tmp['shared'] = '0'
+
+ if conf.exists(['vlan-id']):
+ tmp['vlan_mon'] += conf.return_values(['vlan-id'])
+
+ if conf.exists(['vlan-range']):
+ tmp['vlan_mon'] += conf.return_values(['vlan-range'])
+
+ if conf.exists(['client-subnet']):
+ tmp['range'] = conf.return_value(['client-subnet'])
+
+ ipoe['interfaces'].append(tmp)
+
+ conf.set_level(base_path)
+
+ if conf.exists(['name-server']):
+ for name_server in conf.return_values(['name-server']):
+ if is_ipv4(name_server):
+ ipoe['dnsv4'].append(name_server)
+ else:
+ ipoe['dnsv6'].append(name_server)
+
+ if conf.exists(['authentication', 'mode']):
+ ipoe['auth_mode'] = conf.return_value(['authentication', 'mode'])
+
+ if conf.exists(['authentication', 'interface']):
+ for interface in conf.list_nodes(['authentication', 'interface']):
+ tmp = {
+ 'name': interface,
+ 'mac': []
+ }
+ for client in conf.list_nodes(base_path + ['authentication', 'interface', interface, 'mac-address']):
+ mac = {
+ 'address': mac,
+ 'rate_download': '',
+ 'rate_upload': '',
+ 'vlan_id': ''
+ }
+ conf.set_level(base_path + ['authentication', 'interface', interface, 'mac-address', client])
+
+ if conf.exists(['rate-limit', 'download']):
+ mac['rate_download'] = conf.return_value(['rate-limit', 'download'])
+
+ if conf.exists(['rate-limit', 'upload']):
+ mac['rate_upload'] = conf.return_value(['rate-limit', 'upload'])
+
+ if conf.exists(['vlan-id']):
+ mac['vlan'] = conf.return_value(['vlan-id'])
+
+ tmp['mac'].append(mac)
+
+ ipoe['auth_interfaces'].append(tmp)
+
+ #
+ # authentication mode radius servers and settings
+ if conf.exists(['authentication', 'mode', 'radius']):
+ for server in conf.list_nodes(['authentication', 'radius', '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']):
+ ipoe['radius_server'].append(radius)
+
+ #
+ # advanced radius-setting
+ conf.set_level(base_path + ['authentication', 'radius'])
+ if conf.exists(['acct-timeout']):
+ ipoe['radius_acct_tmo'] = conf.return_value(['acct-timeout'])
+
+ if conf.exists(['max-try']):
+ ipoe['radius_max_try'] = conf.return_value(['max-try'])
+
+ if conf.exists(['timeout']):
+ ipoe['radius_timeout'] = conf.return_value(['timeout'])
+
+ if conf.exists(['nas-identifier']):
+ ipoe['radius_nas_id'] = conf.return_value(['nas-identifier'])
+
+ if conf.exists(['nas-ip-address']):
+ ipoe['radius_nas_ip'] = conf.return_value(['nas-ip-address'])
+
+ if conf.exists(['source-address']):
+ ipoe['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'])
+
+ ipoe['radius_dynamic_author'] = dae
+
+
+ conf.set_level(base_path)
+ if conf.exists(['client-ipv6-pool', 'prefix']):
+ 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'])
+
+ ipoe['client_ipv6_pool'].append(tmp)
+
+
+ if conf.exists(['client-ipv6-pool', 'delegate']):
+ for prefix in conf.list_nodes(['client-ipv6-pool', 'delegate']):
+ tmp = {
+ 'prefix': prefix,
+ 'mask': ''
+ }
+
+ if conf.exists(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']):
+ tmp['mask'] = conf.return_value(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix'])
+
+ ipoe['client_ipv6_delegate_prefix'].append(tmp)
+
+ return ipoe
+
+
+def verify(ipoe):
+ if not ipoe:
+ return None
+
+ if not ipoe['interfaces']:
+ raise ConfigError('No IPoE interface configured')
+
+ for interface in ipoe['interfaces']:
+ if not interface['range']:
+ raise ConfigError(f'No IPoE client subnet defined on interface "{ interface }"')
+
+ if len(ipoe['dnsv4']) > 2:
+ raise ConfigError('Not more then two IPv4 DNS name-servers can be configured')
+
+ if len(ipoe['dnsv6']) > 3:
+ raise ConfigError('Not more then three IPv6 DNS name-servers can be configured')
+
+ if ipoe['auth_mode'] == 'radius':
+ if len(ipoe['radius_server']) == 0:
+ raise ConfigError('RADIUS authentication requires at least one server')
+
+ for radius in ipoe['radius_server']:
+ if not radius['key']:
+ server = radius['server']
+ raise ConfigError(f'Missing RADIUS secret key for server "{ server }"')
+
+ if ipoe['client_ipv6_delegate_prefix'] and not ipoe['client_ipv6_pool']:
+ raise ConfigError('IPoE IPv6 deletate-prefix requires IPv6 prefix to be configured!')
+
+ return None
+
+
+def generate(ipoe):
+ if not ipoe:
+ return None
+
+ render(ipoe_conf, 'accel-ppp/ipoe.config.tmpl', ipoe, trim_blocks=True)
+
+ if ipoe['auth_mode'] == 'local':
+ render(ipoe_chap_secrets, 'accel-ppp/chap-secrets.ipoe.tmpl', ipoe)
+ os.chmod(ipoe_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP)
+
+ else:
+ if os.path.exists(ipoe_chap_secrets):
+ os.unlink(ipoe_chap_secrets)
+
+ return None
+
+
+def apply(ipoe):
+ if ipoe == None:
+ call('systemctl stop accel-ppp@ipoe.service')
+ for file in [ipoe_conf, ipoe_chap_secrets]:
+ if os.path.exists(file):
+ os.unlink(file)
+
+ return None
+
+ call('systemctl restart accel-ppp@ipoe.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/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py
new file mode 100755
index 000000000..e05b0ab2a
--- /dev/null
+++ b/src/conf_mode/service_pppoe-server.py
@@ -0,0 +1,463 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import re
+
+from copy import deepcopy
+from stat import S_IRUSR, S_IWUSR, S_IRGRP
+from sys import exit
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.util import call, get_half_cpus
+from vyos.validate import is_ipv4
+from vyos import ConfigError
+
+pppoe_conf = r'/run/accel-pppd/pppoe.conf'
+pppoe_chap_secrets = r'/run/accel-pppd/pppoe.chap-secrets'
+
+default_config_data = {
+ 'auth_mode': 'local',
+ 'auth_proto': ['auth_mschap_v2', 'auth_mschap_v1', 'auth_chap_md5', 'auth_pap'],
+ 'chap_secrets_file': pppoe_chap_secrets, # used in Jinja2 template
+ 'client_ip_pool': '',
+ 'client_ip_subnets': [],
+ 'client_ipv6_pool': [],
+ 'client_ipv6_delegate_prefix': [],
+ 'concentrator': 'vyos-ac',
+ 'interfaces': [],
+ 'local_users' : [],
+
+ 'svc_name': [],
+ 'dnsv4': [],
+ 'dnsv6': [],
+ 'wins': [],
+ 'mtu': '1492',
+
+ 'limits_burst': '',
+ 'limits_connections': '',
+ 'limits_timeout': '',
+
+ 'pado_delay': '',
+ 'ppp_ccp': False,
+ 'ppp_gw': '',
+ 'ppp_ipv4': '',
+ 'ppp_ipv6': '',
+ 'ppp_ipv6_accept_peer_intf_id': False,
+ 'ppp_ipv6_intf_id': '',
+ 'ppp_ipv6_peer_intf_id': '',
+ 'ppp_echo_failure': '3',
+ 'ppp_echo_interval': '30',
+ 'ppp_echo_timeout': '0',
+ 'ppp_min_mtu': '',
+ 'ppp_mppe': 'prefer',
+ 'ppp_mru': '',
+
+ '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': '',
+ 'sesscrtl': 'replace',
+ 'snmp': False,
+ 'thread_cnt': get_half_cpus()
+}
+
+def get_config():
+ conf = Config()
+ base_path = ['service', 'pppoe-server']
+ if not conf.exists(base_path):
+ return None
+
+ conf.set_level(base_path)
+ pppoe = deepcopy(default_config_data)
+
+ # general options
+ if conf.exists(['access-concentrator']):
+ pppoe['concentrator'] = conf.return_value(['access-concentrator'])
+
+ if conf.exists(['service-name']):
+ pppoe['svc_name'] = conf.return_values(['service-name'])
+
+ if conf.exists(['interface']):
+ for interface in conf.list_nodes(['interface']):
+ conf.set_level(base_path + ['interface', interface])
+ tmp = {
+ 'name': interface,
+ 'vlans': []
+ }
+
+ if conf.exists(['vlan-id']):
+ tmp['vlans'] += conf.return_values(['vlan-id'])
+
+ if conf.exists(['vlan-range']):
+ tmp['vlans'] += conf.return_values(['vlan-range'])
+
+ pppoe['interfaces'].append(tmp)
+
+ conf.set_level(base_path)
+
+ if conf.exists(['local-ip']):
+ pppoe['ppp_gw'] = conf.return_value(['local-ip'])
+
+ if conf.exists(['name-server']):
+ for name_server in conf.return_values(['name-server']):
+ if is_ipv4(name_server):
+ pppoe['dnsv4'].append(name_server)
+ else:
+ pppoe['dnsv6'].append(name_server)
+
+ if conf.exists(['wins-server']):
+ pppoe['wins'] = conf.return_values(['wins-server'])
+
+
+ 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'])
+ pppoe['client_ip_pool'] = start + '-' + re.search('[0-9]+$', stop).group(0)
+
+ if conf.exists(['client-ip-pool', 'subnet']):
+ pppoe['client_ip_subnets'] = conf.return_values(['client-ip-pool', 'subnet'])
+
+
+ if conf.exists(['client-ipv6-pool', 'prefix']):
+ 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'])
+
+ pppoe['client_ipv6_pool'].append(tmp)
+
+
+ if conf.exists(['client-ipv6-pool', 'delegate']):
+ for prefix in conf.list_nodes(['client-ipv6-pool', 'delegate']):
+ tmp = {
+ 'prefix': prefix,
+ 'mask': ''
+ }
+
+ if conf.exists(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']):
+ tmp['mask'] = conf.return_value(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix'])
+
+ pppoe['client_ipv6_delegate_prefix'].append(tmp)
+
+
+ if conf.exists(['limits']):
+ if conf.exists(['limits', 'burst']):
+ pppoe['limits_burst'] = conf.return_value(['limits', 'burst'])
+
+ if conf.exists(['limits', 'connection-limit']):
+ pppoe['limits_connections'] = conf.return_value(['limits', 'connection-limit'])
+
+ if conf.exists(['limits', 'timeout']):
+ pppoe['limits_timeout'] = conf.return_value(['limits', 'timeout'])
+
+
+ if conf.exists(['snmp']):
+ pppoe['snmp'] = True
+
+ if conf.exists(['snmp', 'master-agent']):
+ pppoe['snmp'] = 'enable-ma'
+
+ # authentication mode local
+ if conf.exists(['authentication', 'mode']):
+ pppoe['auth_mode'] = conf.return_value(['authentication', 'mode'])
+
+ 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'])
+
+ pppoe['local_users'].append(user)
+
+ conf.set_level(base_path)
+
+ if conf.exists(['authentication', 'protocols']):
+ auth_mods = {
+ 'mschap-v2': 'auth_mschap_v2',
+ 'mschap': 'auth_mschap_v1',
+ 'chap': 'auth_chap_md5',
+ 'pap': 'auth_pap'
+ }
+
+ pppoe['auth_proto'] = []
+ for proto in conf.return_values(['authentication', 'protocols']):
+ pppoe['auth_proto'].append(auth_mods[proto])
+
+ #
+ # authentication mode radius servers and settings
+ if conf.exists(['authentication', 'mode', 'radius']):
+
+ for server in conf.list_nodes(['authentication', 'radius', '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']):
+ pppoe['radius_server'].append(radius)
+
+ #
+ # advanced radius-setting
+ conf.set_level(base_path + ['authentication', 'radius'])
+
+ if conf.exists(['acct-timeout']):
+ pppoe['radius_acct_tmo'] = conf.return_value(['acct-timeout'])
+
+ if conf.exists(['max-try']):
+ pppoe['radius_max_try'] = conf.return_value(['max-try'])
+
+ if conf.exists(['timeout']):
+ pppoe['radius_timeout'] = conf.return_value(['timeout'])
+
+ if conf.exists(['nas-identifier']):
+ pppoe['radius_nas_id'] = conf.return_value(['nas-identifier'])
+
+ if conf.exists(['nas-ip-address']):
+ pppoe['radius_nas_ip'] = conf.return_value(['nas-ip-address'])
+
+ if conf.exists(['source-address']):
+ pppoe['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'])
+
+ pppoe['radius_dynamic_author'] = dae
+
+ # RADIUS based rate-limiter
+ if conf.exists(['rate-limit', 'enable']):
+ pppoe['radius_shaper_attr'] = 'Filter-Id'
+ c_attr = ['rate-limit', 'enable', 'attribute']
+ if conf.exists(c_attr):
+ pppoe['radius_shaper_attr'] = conf.return_value(c_attr)
+
+ c_vendor = ['rate-limit', 'enable', 'vendor']
+ if conf.exists(c_vendor):
+ pppoe['radius_shaper_vendor'] = conf.return_value(c_vendor)
+
+ # re-set config level
+ conf.set_level(base_path)
+
+ if conf.exists(['mtu']):
+ pppoe['mtu'] = conf.return_value(['mtu'])
+
+ if conf.exists(['session-control']):
+ pppoe['sesscrtl'] = conf.return_value(['session-control'])
+
+ # ppp_options
+ if conf.exists(['ppp-options']):
+ conf.set_level(base_path + ['ppp-options'])
+
+ if conf.exists(['ccp']):
+ pppoe['ppp_ccp'] = True
+
+ if conf.exists(['ipv4']):
+ pppoe['ppp_ipv4'] = conf.return_value(['ipv4'])
+
+ if conf.exists(['ipv6']):
+ pppoe['ppp_ipv6'] = conf.return_value(['ipv6'])
+
+ if conf.exists(['ipv6-accept-peer-intf-id']):
+ pppoe['ppp_ipv6_peer_intf_id'] = True
+
+ if conf.exists(['ipv6-intf-id']):
+ pppoe['ppp_ipv6_intf_id'] = conf.return_value(['ipv6-intf-id'])
+
+ if conf.exists(['ipv6-peer-intf-id']):
+ pppoe['ppp_ipv6_peer_intf_id'] = conf.return_value(['ipv6-peer-intf-id'])
+
+ if conf.exists(['lcp-echo-failure']):
+ pppoe['ppp_echo_failure'] = conf.return_value(['lcp-echo-failure'])
+
+ if conf.exists(['lcp-echo-failure']):
+ pppoe['ppp_echo_interval'] = conf.return_value(['lcp-echo-failure'])
+
+ if conf.exists(['lcp-echo-timeout']):
+ pppoe['ppp_echo_timeout'] = conf.return_value(['lcp-echo-timeout'])
+
+ if conf.exists(['min-mtu']):
+ pppoe['ppp_min_mtu'] = conf.return_value(['min-mtu'])
+
+ if conf.exists(['mppe']):
+ pppoe['ppp_mppe'] = conf.return_value(['mppe'])
+
+ if conf.exists(['mru']):
+ pppoe['ppp_mru'] = conf.return_value(['mru'])
+
+ if conf.exists(['pado-delay']):
+ pppoe['pado_delay'] = '0'
+ a = {}
+ for id in conf.list_nodes(['pado-delay']):
+ if not conf.return_value(['pado-delay', id, 'sessions']):
+ a[id] = 0
+ else:
+ a[id] = conf.return_value(['pado-delay', id, 'sessions'])
+
+ for k in sorted(a.keys()):
+ if k != sorted(a.keys())[-1]:
+ pppoe['pado_delay'] += ",{0}:{1}".format(k, a[k])
+ else:
+ pppoe['pado_delay'] += ",{0}:{1}".format('-1', a[k])
+
+ return pppoe
+
+
+def verify(pppoe):
+ if not pppoe:
+ return None
+
+ # vertify auth settings
+ if pppoe['auth_mode'] == 'local':
+ if not pppoe['local_users']:
+ raise ConfigError('PPPoE local auth mode requires local users to be configured!')
+
+ for user in pppoe['local_users']:
+ username = user['name']
+ if not user['password']:
+ raise ConfigError(f'Password required for local user "{username}"')
+
+ # if up/download is set, check that both have a value
+ if user['upload'] and not user['download']:
+ raise ConfigError(f'Download speed value required for local user "{username}"')
+
+ if user['download'] and not user['upload']:
+ raise ConfigError(f'Upload speed value required for local user "{username}"')
+
+ elif pppoe['auth_mode'] == 'radius':
+ if len(pppoe['radius_server']) == 0:
+ raise ConfigError('RADIUS authentication requires at least one server')
+
+ for radius in pppoe['radius_server']:
+ if not radius['key']:
+ server = radius['server']
+ raise ConfigError(f'Missing RADIUS secret key for server "{ server }"')
+
+ if len(pppoe['wins']) > 2:
+ raise ConfigError('Not more then two IPv4 WINS name-servers can be configured')
+
+ if len(pppoe['dnsv4']) > 2:
+ raise ConfigError('Not more then two IPv4 DNS name-servers can be configured')
+
+ if len(pppoe['dnsv6']) > 3:
+ raise ConfigError('Not more then three IPv6 DNS name-servers can be configured')
+
+ # local ippool and gateway settings config checks
+ if pppoe['client_ip_subnets'] or pppoe['client_ip_pool']:
+ if not pppoe['ppp_gw']:
+ raise ConfigError('PPPoE server requires local IP to be configured')
+
+ if pppoe['ppp_gw'] and not pppoe['client_ip_subnets'] and not pppoe['client_ip_pool']:
+ print("Warning: No PPPoE client pool defined")
+
+ return None
+
+
+def generate(pppoe):
+ if not pppoe:
+ return None
+
+ render(pppoe_conf, 'accel-ppp/pppoe.config.tmpl', pppoe, trim_blocks=True)
+
+ if pppoe['local_users']:
+ render(pppoe_chap_secrets, 'accel-ppp/chap-secrets.tmpl', pppoe, trim_blocks=True)
+ os.chmod(pppoe_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP)
+ else:
+ if os.path.exists(pppoe_chap_secrets):
+ os.unlink(pppoe_chap_secrets)
+
+ return None
+
+
+def apply(pppoe):
+ if not pppoe:
+ call('systemctl stop accel-ppp@pppoe.service')
+ for file in [pppoe_conf, pppoe_chap_secrets]:
+ if os.path.exists(file):
+ os.unlink(file)
+
+ return None
+
+ call('systemctl restart accel-ppp@pppoe.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/service-router-advert.py b/src/conf_mode/service_router-advert.py
index 620f3eacf..620f3eacf 100755
--- a/src/conf_mode/service-router-advert.py
+++ b/src/conf_mode/service_router-advert.py
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index 91e2b369f..09c5422eb 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.py
@@ -17,6 +17,7 @@
import os
from crypt import crypt, METHOD_SHA512
+from netifaces import interfaces
from psutil import users
from pwd import getpwall, getpwnam
from stat import S_IRUSR, S_IWUSR, S_IRWXU, S_IRGRP, S_IXGRP
@@ -39,6 +40,7 @@ default_config_data = {
'del_users': [],
'radius_server': [],
'radius_source_address': '',
+ 'radius_vrf': ''
}
def get_local_users():
@@ -127,6 +129,10 @@ def get_config():
if conf.exists(['source-address']):
login['radius_source_address'] = conf.return_value(['source-address'])
+ # retrieve VRF instance
+ if conf.exists(['vrf']):
+ login['radius_vrf'] = conf.return_value(['vrf'])
+
# Read in all RADIUS servers and store to list
for server in conf.list_nodes(['server']):
server_cfg = {
@@ -193,6 +199,9 @@ def verify(login):
if fail:
raise ConfigError('At least one RADIUS server must be active.')
+ vrf_name = login['radius_vrf']
+ if vrf_name and vrf_name not in interfaces():
+ raise ConfigError(f'VRF "{vrf_name}" does not exist')
return None
@@ -217,7 +226,7 @@ def generate(login):
# env=env)
if len(login['radius_server']) > 0:
- render(radius_config_file, 'system-login/pam_radius_auth.conf.tmpl', login)
+ render(radius_config_file, 'system-login/pam_radius_auth.conf.tmpl', login, trim_blocks=True)
uid = getpwnam('root').pw_uid
gid = getpwnam('root').pw_gid
diff --git a/src/conf_mode/vpn-pptp.py b/src/conf_mode/vpn-pptp.py
deleted file mode 100755
index 15b80f984..000000000
--- a/src/conf_mode/vpn-pptp.py
+++ /dev/null
@@ -1,257 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-import os
-import re
-
-from socket import socket, AF_INET, SOCK_STREAM
-from sys import exit
-from time import sleep
-
-from vyos.config import Config
-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'
-chap_secrets = pptp_cnf_dir + '/chap-secrets'
-pptp_conf = pptp_cnf_dir + '/pptp.config'
-
-# config path creation
-if not os.path.exists(pptp_cnf_dir):
- os.makedirs(pptp_cnf_dir)
-
-def _chk_con():
- cnt = 0
- s = socket(AF_INET, SOCK_STREAM)
- while True:
- try:
- s.connect(("127.0.0.1", 2003))
- break
- except ConnectionRefusedError:
- sleep(0.5)
- cnt += 1
- if cnt == 100:
- raise("failed to start pptp server")
- break
-
-
-def _accel_cmd(command):
- return run('/usr/bin/accel-cmd -p 2003 {command}')
-
-###
-# inline helper functions end
-###
-
-
-def get_config():
- c = Config()
- if not c.exists(['vpn', 'pptp', 'remote-access']):
- return None
-
- c.set_level(['vpn', 'pptp', 'remote-access'])
- config_data = {
- 'authentication': {
- 'mode': 'local',
- 'local-users': {
- },
- 'radiussrv': {},
- 'auth_proto': 'auth_mschap_v2',
- 'mppe': 'require'
- },
- 'outside_addr': '',
- 'dns': [],
- 'wins': [],
- 'client_ip_pool': '',
- 'mtu': '1436',
- }
-
- ### 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(['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': ''
- }
- }
- )
-
- 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'])
-
- # 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 not c.return_value(['authentication', 'radius', 'server', rsrv, 'fail-time']):
- ftime = '0'
- else:
- ftime = c.return_value(
- ['authentication', 'radius', 'server', rsrv, 'fail-time'])
- if not c.return_value(['authentication', 'radius-server', rsrv, 'req-limit']):
- reql = '0'
- else:
- reql = 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
- }
- }
- )
-
- if c.exists(['client-ip-pool']):
- if c.exists(['client-ip-pool', 'start']):
- config_data['client_ip_pool'] = c.return_value(
- ['client-ip-pool', 'start'])
- if c.exists(['client-ip-pool', 'stop']):
- config_data['client_ip_pool'] += '-' + \
- re.search(
- '[0-9]+$', c.return_value(['client-ip-pool', 'stop'])).group(0)
- if c.exists(['mtu']):
- config_data['mtu'] = c.return_value(['mtu'])
-
- # gateway address
- if c.exists(['gateway-address']):
- config_data['gw_ip'] = c.return_value(['gateway-address'])
- else:
- config_data['gw_ip'] = re.sub(
- '[0-9]+$', '1', config_data['client_ip_pool'])
-
- if c.exists(['authentication', 'require']):
- if c.return_value(['authentication', 'require']) == 'pap':
- config_data['authentication']['auth_proto'] = 'auth_pap'
- if c.return_value(['authentication', 'require']) == 'chap':
- config_data['authentication']['auth_proto'] = 'auth_chap_md5'
- if c.return_value(['authentication', 'require']) == 'mschap':
- config_data['authentication']['auth_proto'] = 'auth_mschap_v1'
- if c.return_value(['authentication', 'require']) == 'mschap-v2':
- config_data['authentication']['auth_proto'] = 'auth_mschap_v2'
-
- if c.exists(['authentication', 'mppe']):
- config_data['authentication']['mppe'] = c.return_value(
- ['authentication', 'mppe'])
-
- return config_data
-
-
-def verify(c):
- if c == None:
- return None
-
- if c['authentication']['mode'] == 'local':
- if not c['authentication']['local-users']:
- raise ConfigError(
- 'pptp-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')
-
-
-def generate(c):
- if c == None:
- return None
-
- # accel-cmd reload doesn't work so any change results in a restart of the daemon
- try:
- if os.cpu_count() == 1:
- c['thread_cnt'] = 1
- else:
- c['thread_cnt'] = int(os.cpu_count()/2)
- except KeyError:
- if os.cpu_count() == 1:
- c['thread_cnt'] = 1
- else:
- c['thread_cnt'] = int(os.cpu_count()/2)
-
- render(pptp_conf, 'pptp/pptp.config.tmpl', c, trim_blocks=True)
-
- if c['authentication']['local-users']:
- old_umask = os.umask(0o077)
- render(chap_secrets, 'pptp/chap-secrets.tmpl', c, trim_blocks=True)
- os.umask(old_umask)
- # return c ??
- return c
-
-
-def apply(c):
- if c == None:
- if os.path.exists(pidfile):
- _accel_cmd('shutdown hard')
- if os.path.exists(pidfile):
- os.remove(pidfile)
- return None
-
- if not os.path.exists(pidfile):
- ret = run(f'/usr/sbin/accel-pppd -c {pptp_conf} -p {pidfile} -d')
- _chk_con()
- if ret != 0 and os.path.exists(pidfile):
- os.remove(pidfile)
- 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)
- exit(1)
diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py
index a8b183bef..f312f2a17 100755
--- a/src/conf_mode/vpn_l2tp.py
+++ b/src/conf_mode/vpn_l2tp.py
@@ -25,7 +25,7 @@ from time import sleep
from ipaddress import ip_network
from vyos.config import Config
-from vyos.util import call
+from vyos.util import call, get_half_cpus
from vyos.validate import is_ipv4
from vyos import ConfigError
from vyos.template import render
@@ -65,7 +65,7 @@ default_config_data = {
'radius_dynamic_author': '',
'wins': [],
'ip6_column': [],
- 'thread_cnt': 1
+ 'thread_cnt': get_half_cpus()
}
def get_config():
@@ -77,10 +77,6 @@ def get_config():
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']):
@@ -252,7 +248,7 @@ def get_config():
'mask': ''
}
- if conf.exists(['client-ipv6-pool', 'delegate', prefix, 'mask']):
+ if conf.exists(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']):
tmp['mask'] = conf.return_value(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix'])
l2tp['client_ipv6_delegate_prefix'].append(tmp)
@@ -313,7 +309,7 @@ def verify(l2tp):
for radius in l2tp['radius_server']:
if not radius['key']:
- raise ConfigError(f"Missing RADIUS secret for server {{ 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']):
@@ -344,14 +340,10 @@ 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)
+ render(l2tp_conf, 'accel-ppp/l2tp.config.tmpl', l2tp, trim_blocks=True)
if l2tp['auth_mode'] == 'local':
- render(l2tp_chap_secrets, 'l2tp/chap-secrets.tmpl', l2tp)
+ render(l2tp_chap_secrets, 'accel-ppp/chap-secrets.tmpl', l2tp)
os.chmod(l2tp_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP)
else:
@@ -364,12 +356,9 @@ def generate(l2tp):
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)
+ for file in [l2tp_chap_secrets, l2tp_conf]:
+ if os.path.exists(file):
+ os.unlink(file)
return None
diff --git a/src/conf_mode/vpn_pptp.py b/src/conf_mode/vpn_pptp.py
new file mode 100755
index 000000000..085c9c2c6
--- /dev/null
+++ b/src/conf_mode/vpn_pptp.py
@@ -0,0 +1,279 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import re
+
+from copy import deepcopy
+from stat import S_IRUSR, S_IWUSR, S_IRGRP
+from sys import exit
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.util import call, get_half_cpus
+from vyos import ConfigError
+
+pptp_conf = '/run/accel-pppd/pptp.conf'
+pptp_chap_secrets = '/run/accel-pppd/pptp.chap-secrets'
+
+default_pptp = {
+ 'auth_mode' : 'local',
+ 'local_users' : [],
+ 'radius_server' : [],
+ 'radius_acct_tmo' : '30',
+ 'radius_max_try' : '3',
+ 'radius_timeout' : '30',
+ 'radius_nas_id' : '',
+ 'radius_nas_ip' : '',
+ 'radius_source_address' : '',
+ 'radius_shaper_attr' : '',
+ 'radius_shaper_vendor': '',
+ 'radius_dynamic_author' : '',
+ 'chap_secrets_file': pptp_chap_secrets, # used in Jinja2 template
+ 'outside_addr': '',
+ 'dnsv4': [],
+ 'wins': [],
+ 'client_ip_pool': '',
+ 'mtu': '1436',
+ 'auth_proto' : ['auth_mschap_v2'],
+ 'ppp_mppe' : 'prefer',
+ 'thread_cnt': get_half_cpus()
+}
+
+def get_config():
+ conf = Config()
+ base_path = ['vpn', 'pptp', 'remote-access']
+ if not conf.exists(base_path):
+ return None
+
+ pptp = deepcopy(default_pptp)
+ conf.set_level(base_path)
+
+ if conf.exists(['name-server']):
+ pptp['dnsv4'] = conf.return_values(['name-server'])
+
+ if conf.exists(['wins-server']):
+ pptp['wins'] = conf.return_values(['wins-server'])
+
+ if conf.exists(['outside-address']):
+ pptp['outside_addr'] = conf.return_value(['outside-address'])
+
+ if conf.exists(['authentication', 'mode']):
+ pptp['auth_mode'] = conf.return_value(['authentication', 'mode'])
+
+ #
+ # local auth
+ if conf.exists(['authentication', 'local-users']):
+ for username in conf.list_nodes(['authentication', 'local-users', 'username']):
+ user = {
+ 'name': username,
+ 'password' : '',
+ 'state' : 'enabled',
+ 'ip' : '*',
+ }
+
+ 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 not conf.exists(['disable']):
+ pptp['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']):
+ pptp['radius_server'].append(radius)
+
+ #
+ # advanced radius-setting
+ conf.set_level(base_path + ['authentication', 'radius'])
+
+ if conf.exists(['acct-timeout']):
+ pptp['radius_acct_tmo'] = conf.return_value(['acct-timeout'])
+
+ if conf.exists(['max-try']):
+ pptp['radius_max_try'] = conf.return_value(['max-try'])
+
+ if conf.exists(['timeout']):
+ pptp['radius_timeout'] = conf.return_value(['timeout'])
+
+ if conf.exists(['nas-identifier']):
+ pptp['radius_nas_id'] = conf.return_value(['nas-identifier'])
+
+ if conf.exists(['nas-ip-address']):
+ pptp['radius_nas_ip'] = conf.return_value(['nas-ip-address'])
+
+ if conf.exists(['source-address']):
+ pptp['radius_source_address'] = conf.return_value(['source-address'])
+
+ # Dynamic Authorization Extensions (DOA)/Change Of Authentication (COA)
+ if conf.exists(['dae-server']):
+ dae = {
+ 'port' : '',
+ 'server' : '',
+ 'key' : ''
+ }
+
+ if conf.exists(['dynamic-author', 'ip-address']):
+ dae['server'] = conf.return_value(['dynamic-author', 'ip-address'])
+
+ 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'])
+
+ pptp['radius_dynamic_author'] = dae
+
+ if conf.exists(['rate-limit', 'enable']):
+ pptp['radius_shaper_attr'] = 'Filter-Id'
+ c_attr = ['rate-limit', 'enable', 'attribute']
+ if conf.exists(c_attr):
+ pptp['radius_shaper_attr'] = conf.return_value(c_attr)
+
+ c_vendor = ['rate-limit', 'enable', 'vendor']
+ if conf.exists(c_vendor):
+ pptp['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'])
+ pptp['client_ip_pool'] = start + '-' + re.search('[0-9]+$', stop).group(0)
+
+ if conf.exists(['mtu']):
+ pptp['mtu'] = conf.return_value(['mtu'])
+
+ # gateway address
+ if conf.exists(['gateway-address']):
+ pptp['gw_ip'] = conf.return_value(['gateway-address'])
+ else:
+ # calculate gw-ip-address
+ if conf.exists(['client-ip-pool', 'start']):
+ # use start ip as gw-ip-address
+ pptp['gateway_address'] = conf.return_value(['client-ip-pool', 'start'])
+
+ if conf.exists(['authentication', 'require']):
+ # clear default list content, now populate with actual CLI values
+ pptp['auth_proto'] = []
+ 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', 'require']):
+ pptp['auth_proto'].append(auth_mods[proto])
+
+ if conf.exists(['authentication', 'mppe']):
+ pptp['ppp_mppe'] = conf.return_value(['authentication', 'mppe'])
+
+ return pptp
+
+
+def verify(pptp):
+ if not pptp:
+ return None
+
+ if pptp['auth_mode'] == 'local':
+ if not pptp['local_users']:
+ raise ConfigError('PPTP local auth mode requires local users to be configured!')
+
+ for user in pptp['local_users']:
+ username = user['name']
+ if not user['password']:
+ raise ConfigError(f'Password required for local user "{username}"')
+
+ elif pptp['auth_mode'] == 'radius':
+ if len(pptp['radius_server']) == 0:
+ raise ConfigError('RADIUS authentication requires at least one server')
+
+ for radius in pptp['radius_server']:
+ if not radius['key']:
+ server = radius['server']
+ raise ConfigError(f'Missing RADIUS secret key for server "{ server }"')
+
+ if len(pptp['dnsv4']) > 2:
+ raise ConfigError('Not more then two IPv4 DNS name-servers can be configured')
+
+ if len(pptp['wins']) > 2:
+ raise ConfigError('Not more then two IPv4 WINS name-servers can be configured')
+
+
+def generate(pptp):
+ if not pptp:
+ return None
+
+ render(pptp_conf, 'accel-ppp/pptp.config.tmpl', pptp, trim_blocks=True)
+
+ if pptp['local_users']:
+ render(pptp_chap_secrets, 'accel-ppp/chap-secrets.tmpl', pptp, trim_blocks=True)
+ os.chmod(pptp_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP)
+ else:
+ if os.path.exists(pptp_chap_secrets):
+ os.unlink(pptp_chap_secrets)
+
+
+def apply(pptp):
+ if not pptp:
+ call('systemctl stop accel-ppp@pptp.service')
+ for file in [pptp_conf, pptp_chap_secrets]:
+ if os.path.exists(file):
+ os.unlink(file)
+
+ return None
+
+ call('systemctl restart accel-ppp@pptp.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 438731972..d250cd3b0 100755
--- a/src/conf_mode/vpn_sstp.py
+++ b/src/conf_mode/vpn_sstp.py
@@ -23,7 +23,7 @@ from stat import S_IRUSR, S_IWUSR, S_IRGRP
from vyos.config import Config
from vyos import ConfigError
-from vyos.util import call, run
+from vyos.util import call, run, get_half_cpus
from vyos.template import render
@@ -56,7 +56,7 @@ default_config_data = {
'ppp_echo_failure' : '',
'ppp_echo_interval' : '',
'ppp_echo_timeout' : '',
- 'thread_cnt' : 1
+ 'thread_cnt' : get_half_cpus()
}
def get_config():
@@ -68,10 +68,6 @@ def get_config():
conf.set_level(base_path)
- 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'])
@@ -259,21 +255,22 @@ def verify(sstp):
raise ConfigError('SSTP local auth mode requires local users to be configured!')
for user in sstp['local_users']:
+ username = user['name']
if not user['password']:
- raise ConfigError(f"Password required for user {user['name']}")
+ raise ConfigError(f'Password required for local user "{username}"')
# if up/download is set, check that both have a value
if user['upload'] and not user['download']:
- raise ConfigError(f"Download speed value required for user {user['name']}")
+ raise ConfigError(f'Download speed value required for local user "{username}"')
if user['download'] and not user['upload']:
- raise ConfigError(f"Upload speed value required for user {user['name']}")
+ raise ConfigError(f'Upload speed value required for local user "{username}"')
if not sstp['client_ip_pool']:
- raise ConfigError("Client IP subnet required")
+ raise ConfigError('Client IP subnet required')
if not sstp['client_gateway']:
- raise ConfigError("Client gateway IP address required")
+ raise ConfigError('Client gateway IP address required')
if len(sstp['dnsv4']) > 2:
raise ConfigError('Not more then two IPv4 DNS name-servers can be configured')
@@ -282,35 +279,35 @@ def verify(sstp):
raise ConfigError('One or more SSL certificates missing')
if not os.path.exists(sstp['ssl_ca']):
- raise ConfigError(f"CA cert file {sstp['ssl_ca']} does not exist")
+ file = sstp['ssl_ca']
+ raise ConfigError(f'SSL CA certificate file "{file}" does not exist')
if not os.path.exists(sstp['ssl_cert']):
- raise ConfigError(f"SSL cert file {sstp['ssl_cert']} does not exist")
+ file = sstp['ssl_cert']
+ raise ConfigError(f'SSL public key file "{file}" does not exist')
if not os.path.exists(sstp['ssl_key']):
- raise ConfigError(f"SSL key file {sstp['ssl_key']} does not exist")
+ file = sstp['ssl_key']
+ raise ConfigError(f'SSL private key file "{file}" does not exist')
if sstp['auth_mode'] == 'radius':
if len(sstp['radius_server']) == 0:
- raise ConfigError("RADIUS authentication requires at least one server")
+ raise ConfigError('RADIUS authentication requires at least one server')
for radius in sstp['radius_server']:
if not radius['key']:
- raise ConfigError(f"Missing RADIUS secret for server {{ radius['key'] }}")
+ server = radius['server']
+ raise ConfigError(f'Missing RADIUS secret key for server "{ server }"')
def generate(sstp):
if not sstp:
return None
- 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
- render(sstp_conf, 'sstp/sstp.config.tmpl', sstp, trim_blocks=True)
+ render(sstp_conf, 'accel-ppp/sstp.config.tmpl', sstp, trim_blocks=True)
if sstp['local_users']:
- render(sstp_chap_secrets, 'sstp/chap-secrets.tmpl', sstp, trim_blocks=True)
+ render(sstp_chap_secrets, 'accel-ppp/chap-secrets.tmpl', sstp, trim_blocks=True)
os.chmod(sstp_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP)
else:
if os.path.exists(sstp_chap_secrets):
@@ -321,12 +318,9 @@ def generate(sstp):
def apply(sstp):
if not sstp:
call('systemctl stop accel-ppp@sstp.service')
-
- if os.path.exists(sstp_conf):
- os.unlink(sstp_conf)
-
- if os.path.exists(sstp_chap_secrets):
- os.unlink(sstp_chap_secrets)
+ for file in [sstp_chap_secrets, sstp_conf]:
+ if os.path.exists(file):
+ os.unlink(file)
return None
diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper
index 59f92703c..f1167fcd2 100644
--- a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper
+++ b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper
@@ -15,8 +15,11 @@ function frr_alive () {
# convert ip route command to vtysh
function iptovtysh () {
# prepare variables for vtysh command
- VTYSH_DISTANCE="210"
- VTYSH_TAG="210"
+ local VTYSH_DISTANCE="210"
+ local VTYSH_TAG="210"
+ local VTYSH_NETADDR=""
+ local VTYSH_GATEWAY=""
+ local VTYSH_DEV=""
# convert default route to 0.0.0.0/0
if [ "$4" == "default" ] ; then
VTYSH_NETADDR="0.0.0.0/0"
@@ -74,3 +77,4 @@ function ip () {
fi
fi
}
+
diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
index ce846f6c3..88a4d9db9 100644
--- a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
+++ b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
@@ -1,12 +1,74 @@
+# NOTE: here we use 'ip' wrapper, therefore a route will be actually deleted via /usr/sbin/ip or vtysh, according to the system state
+
if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then
# delete dynamic nameservers from a configuration if lease was deleted
logmsg info "Deleting nameservers with tag \"dhcp-${interface}\" via vyos-hostsd-client"
vyos-hostsd-client --delete-name-servers --tag dhcp-${interface}
- # try to delete default ip route (NOTE: here we use 'ip' wrapper, therefore a route will be actually deleted via /usr/sbin/ip or vtysh, according to the system state)
+ # try to delete default ip route
for router in $old_routers; do
logmsg info "Deleting default route: via $router dev ${interface}"
ip -4 route del default via $router dev ${interface}
done
+ # delete rfc3442 routes
+ if [ -n "$old_rfc3442_classless_static_routes" ]; then
+ set -- $old_rfc3442_classless_static_routes
+ while [ $# -gt 0 ]; do
+ net_length=$1
+ via_arg=''
+ case $net_length in
+ 32|31|30|29|28|27|26|25)
+ if [ $# -lt 9 ]; then
+ return 1
+ fi
+ net_address="${2}.${3}.${4}.${5}"
+ gateway="${6}.${7}.${8}.${9}"
+ shift 9
+ ;;
+ 24|23|22|21|20|19|18|17)
+ if [ $# -lt 8 ]; then
+ return 1
+ fi
+ net_address="${2}.${3}.${4}.0"
+ gateway="${5}.${6}.${7}.${8}"
+ shift 8
+ ;;
+ 16|15|14|13|12|11|10|9)
+ if [ $# -lt 7 ]; then
+ return 1
+ fi
+ net_address="${2}.${3}.0.0"
+ gateway="${4}.${5}.${6}.${7}"
+ shift 7
+ ;;
+ 8|7|6|5|4|3|2|1)
+ if [ $# -lt 6 ]; then
+ return 1
+ fi
+ net_address="${2}.0.0.0"
+ gateway="${3}.${4}.${5}.${6}"
+ shift 6
+ ;;
+ 0) # default route
+ if [ $# -lt 5 ]; then
+ return 1
+ fi
+ net_address="0.0.0.0"
+ gateway="${2}.${3}.${4}.${5}"
+ shift 5
+ ;;
+ *) # error
+ return 1
+ ;;
+ esac
+ # take care of link-local routes
+ if [ "${gateway}" != '0.0.0.0' ]; then
+ via_arg="via ${gateway}"
+ fi
+ # delete route (ip detects host routes automatically)
+ ip -4 route del "${net_address}/${net_length}" \
+ ${via_arg} dev "${interface}" >/dev/null 2>&1
+ done
+ fi
fi
if [[ $reason =~ (EXPIRE6|RELEASE6|STOP6) ]]; then
diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/02-vyos-dhcp-renew-rfc3442 b/src/etc/dhcp/dhclient-exit-hooks.d/02-vyos-dhcp-renew-rfc3442
new file mode 100644
index 000000000..9202fe72d
--- /dev/null
+++ b/src/etc/dhcp/dhclient-exit-hooks.d/02-vyos-dhcp-renew-rfc3442
@@ -0,0 +1,148 @@
+# support for RFC3442 routes in DHCP RENEW
+
+function convert_to_cidr () {
+ cidr=""
+ set -- $1
+ while [ $# -gt 0 ]; do
+ net_length=$1
+
+ case $net_length in
+ 32|31|30|29|28|27|26|25)
+ if [ $# -lt 9 ]; then
+ return 1
+ fi
+ net_address="${2}.${3}.${4}.${5}"
+ gateway="${6}.${7}.${8}.${9}"
+ shift 9
+ ;;
+ 24|23|22|21|20|19|18|17)
+ if [ $# -lt 8 ]; then
+ return 1
+ fi
+ net_address="${2}.${3}.${4}.0"
+ gateway="${5}.${6}.${7}.${8}"
+ shift 8
+ ;;
+ 16|15|14|13|12|11|10|9)
+ if [ $# -lt 7 ]; then
+ return 1
+ fi
+ net_address="${2}.${3}.0.0"
+ gateway="${4}.${5}.${6}.${7}"
+ shift 7
+ ;;
+ 8|7|6|5|4|3|2|1)
+ if [ $# -lt 6 ]; then
+ return 1
+ fi
+ net_address="${2}.0.0.0"
+ gateway="${3}.${4}.${5}.${6}"
+ shift 6
+ ;;
+ 0) # default route
+ if [ $# -lt 5 ]; then
+ return 1
+ fi
+ net_address="0.0.0.0"
+ gateway="${2}.${3}.${4}.${5}"
+ shift 5
+ ;;
+ *) # error
+ return 1
+ ;;
+ esac
+
+ cidr+="${net_address}/${net_length}:${gateway} "
+ done
+}
+
+# main script starts here
+
+RUN="yes"
+
+if [ "$RUN" = "yes" ]; then
+ convert_to_cidr "$old_rfc3442_classless_static_routes"
+ old_cidr=$cidr
+ convert_to_cidr "$new_rfc3442_classless_static_routes"
+ new_cidr=$cidr
+
+ if [ "$reason" = "RENEW" ]; then
+ if [ "$new_rfc3442_classless_static_routes" != "$old_rfc3442_classless_static_routes" ]; then
+ logmsg info "RFC3442 route change detected, old_routes: $old_rfc3442_classless_static_routes"
+ logmsg info "RFC3442 route change detected, new_routes: $new_rfc3442_classless_static_routes"
+ if [ -z "$new_rfc3442_classless_static_routes" ]; then
+ # delete all routes from the old_rfc3442_classless_static_routes
+ for route in $old_cidr; do
+ network=$(printf "${route}" | awk -F ":" '{print $1}')
+ gateway=$(printf "${route}" | awk -F ":" '{print $2}')
+ # take care of link-local routes
+ if [ "${gateway}" != '0.0.0.0' ]; then
+ via_arg="via ${gateway}"
+ else
+ via_arg=""
+ fi
+ ip -4 route del "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1
+ done
+ elif [ -z "$old_rfc3442_classless_static_routes" ]; then
+ # add all routes from the new_rfc3442_classless_static_routes
+ for route in $new_cidr; do
+ network=$(printf "${route}" | awk -F ":" '{print $1}')
+ gateway=$(printf "${route}" | awk -F ":" '{print $2}')
+ # take care of link-local routes
+ if [ "${gateway}" != '0.0.0.0' ]; then
+ via_arg="via ${gateway}"
+ else
+ via_arg=""
+ fi
+ ip -4 route add "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1
+ done
+ else
+ # update routes
+ # delete old
+ for old_route in $old_cidr; do
+ match="false"
+ for new_route in $new_cidr; do
+ if [[ "$old_route" == "$new_route" ]]; then
+ match="true"
+ break
+ fi
+ done
+ if [[ "$match" == "false" ]]; then
+ # delete old_route
+ network=$(printf "${old_route}" | awk -F ":" '{print $1}')
+ gateway=$(printf "${old_route}" | awk -F ":" '{print $2}')
+ # take care of link-local routes
+ if [ "${gateway}" != '0.0.0.0' ]; then
+ via_arg="via ${gateway}"
+ else
+ via_arg=""
+ fi
+ ip -4 route del "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1
+ fi
+ done
+ # add new
+ for new_route in $new_cidr; do
+ match="false"
+ for old_route in $old_cidr; do
+ if [[ "$new_route" == "$old_route" ]]; then
+ match="true"
+ break
+ fi
+ done
+ if [[ "$match" == "false" ]]; then
+ # add new_route
+ network=$(printf "${new_route}" | awk -F ":" '{print $1}')
+ gateway=$(printf "${new_route}" | awk -F ":" '{print $2}')
+ # take care of link-local routes
+ if [ "${gateway}" != '0.0.0.0' ]; then
+ via_arg="via ${gateway}"
+ else
+ via_arg=""
+ fi
+ ip -4 route add "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1
+ fi
+ done
+ fi
+ fi
+ fi
+fi
diff --git a/src/etc/sysctl.d/31-vyos-addr_gen_mode.conf b/src/etc/sysctl.d/31-vyos-addr_gen_mode.conf
new file mode 100644
index 000000000..07a0d1584
--- /dev/null
+++ b/src/etc/sysctl.d/31-vyos-addr_gen_mode.conf
@@ -0,0 +1,14 @@
+### Added by vyos-1x ###
+#
+# addr_gen_mode - INTEGER
+# Defines how link-local and autoconf addresses are generated.
+#
+# 0: generate address based on EUI64 (default)
+# 1: do no generate a link-local address, use EUI64 for addresses generated
+# from autoconf
+# 2: generate stable privacy addresses, using the secret from
+# stable_secret (RFC7217)
+# 3: generate stable privacy addresses, using a random secret if unset
+#
+net.ipv6.conf.all.addr_gen_mode = 1
+net.ipv6.conf.default.addr_gen_mode = 1
diff --git a/src/migration-scripts/dhcpv6-server/0-to-1 b/src/migration-scripts/dhcpv6-server/0-to-1
new file mode 100755
index 000000000..6f1150da1
--- /dev/null
+++ b/src/migration-scripts/dhcpv6-server/0-to-1
@@ -0,0 +1,61 @@
+#!/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/>.
+
+# combine both sip-server-address and sip-server-name nodes to common sip-server
+
+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 = ['service', 'dhcpv6-server', 'shared-network-name']
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+else:
+ # we need to run this for every configured network
+ for network in config.list_nodes(base):
+ for subnet in config.list_nodes(base + [network, 'subnet']):
+ sip_server = []
+
+ # Do we have 'sip-server-address' configured?
+ if config.exists(base + [network, 'subnet', subnet, 'sip-server-address']):
+ sip_server += config.return_values(base + [network, 'subnet', subnet, 'sip-server-address'])
+ config.delete(base + [network, 'subnet', subnet, 'sip-server-address'])
+
+ # Do we have 'sip-server-name' configured?
+ if config.exists(base + [network, 'subnet', subnet, 'sip-server-name']):
+ sip_server += config.return_values(base + [network, 'subnet', subnet, 'sip-server-name'])
+ config.delete(base + [network, 'subnet', subnet, 'sip-server-name'])
+
+ # Write new CLI value for sip-server
+ for server in sip_server:
+ config.set(base + [network, 'subnet', subnet, 'sip-server'], value=server, replace=False)
+
+ 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/dns-forwarding/1-to-2 b/src/migration-scripts/dns-forwarding/1-to-2
index 9a50b6aa3..8c4f4b5c7 100755
--- a/src/migration-scripts/dns-forwarding/1-to-2
+++ b/src/migration-scripts/dns-forwarding/1-to-2
@@ -20,17 +20,16 @@
# listen-address nodes instead. This is required as PowerDNS can only listen
# on interface addresses and not on interface names.
-import sys
-
from ipaddress import ip_interface
+from sys import argv, exit
from vyos.ifconfig import Interface
from vyos.configtree import ConfigTree
-if (len(sys.argv) < 1):
+if (len(argv) < 1):
print("Must specify file name!")
- sys.exit(1)
+ exit(1)
-file_name = sys.argv[1]
+file_name = argv[1]
with open(file_name, 'r') as f:
config_file = f.read()
@@ -40,45 +39,45 @@ config = ConfigTree(config_file)
base = ['service', 'dns', 'forwarding']
if not config.exists(base):
# Nothing to do
- sys.exit(0)
-
-else:
- # XXX: we can remove the else and un-indent this whole block
+ exit(0)
- if config.exists(base + ['listen-on']):
- listen_intf = config.return_values(base + ['listen-on'])
- # Delete node with abandoned command
- config.delete(base + ['listen-on'])
+if config.exists(base + ['listen-on']):
+ listen_intf = config.return_values(base + ['listen-on'])
+ # Delete node with abandoned command
+ config.delete(base + ['listen-on'])
- # retrieve interface addresses for every configured listen-on interface
- listen_addr = []
- for intf in listen_intf:
- # we need to treat vif and vif-s interfaces differently,
- # both "real interfaces" use dots for vlan identifiers - those
- # need to be exchanged with vif and vif-s identifiers
- if intf.count('.') == 1:
- # this is a regular VLAN interface
- intf = intf.split('.')[0] + ' vif ' + intf.split('.')[1]
- elif intf.count('.') == 2:
- # this is a QinQ VLAN interface
- intf = intf.split('.')[0] + ' vif-s ' + intf.split('.')[1] + ' vif-c ' + intf.split('.')[2]
+ # retrieve interface addresses for every configured listen-on interface
+ listen_addr = []
+ for intf in listen_intf:
+ # we need to evaluate the interface section before manipulating the 'intf' variable
+ section = Interface.section(intf)
+ if not section:
+ raise ValueError(f'Invalid interface name {intf}')
- section = Interface.section(intf)
- if not section:
- raise ValueError(f'Invalid interface name {intf}')
- path = ['interfaces', section, intf, 'address']
+ # we need to treat vif and vif-s interfaces differently,
+ # both "real interfaces" use dots for vlan identifiers - those
+ # need to be exchanged with vif and vif-s identifiers
+ if intf.count('.') == 1:
+ # this is a regular VLAN interface
+ intf = intf.split('.')[0] + ' vif ' + intf.split('.')[1]
+ elif intf.count('.') == 2:
+ # this is a QinQ VLAN interface
+ intf = intf.split('.')[0] + ' vif-s ' + intf.split('.')[1] + ' vif-c ' + intf.split('.')[2]
- # retrieve corresponding interface addresses in CIDR format
- # those need to be converted in pure IP addresses without network information
- for addr in config.return_values(path):
- listen_addr.append( ip_interface(addr).ip )
+ # retrieve corresponding interface addresses in CIDR format
+ # those need to be converted in pure IP addresses without network information
+ path = ['interfaces', section, intf, 'address']
+ for addr in config.return_values(path):
+ listen_addr.append( ip_interface(addr).ip )
- for addr in listen_addr:
- config.set(base + ['listen-address'], value=addr, replace=False)
+ for addr in listen_addr:
+ config.set(base + ['listen-address'], value=addr, replace=False)
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))
- sys.exit(1)
+ exit(1)
+
+exit(0)
diff --git a/src/migration-scripts/ipoe-server/0-to-1 b/src/migration-scripts/ipoe-server/0-to-1
new file mode 100755
index 000000000..f328ebced
--- /dev/null
+++ b/src/migration-scripts/ipoe-server/0-to-1
@@ -0,0 +1,133 @@
+#!/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
+# - Unifi RADIUS configuration by placing it all under "authentication radius" node
+
+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 = ['service', 'ipoe-server']
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+else:
+
+ # Migrate IPv4 DNS servers
+ dns_base = base + ['dns-server']
+ 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-server']
+ if config.exists(dns_base):
+ for server in ['server-1', 'server-2', 'server-3']:
+ 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 radius-settings node to RADIUS and use this as base for the
+ # later migration of the RADIUS servers - this will save a lot of code
+ radius_settings = base + ['authentication', 'radius-settings']
+ if config.exists(radius_settings):
+ config.rename(radius_settings, 'radius')
+
+ # Migrate RADIUS dynamic author / change of authorisation server
+ dae_old = base + ['authentication', 'radius', 'dae-server']
+ if config.exists(dae_old):
+ config.rename(dae_old, 'dynamic-author')
+ dae_new = base + ['authentication', 'radius', 'dynamic-author']
+
+ if config.exists(dae_new + ['ip-address']):
+ config.rename(dae_new + ['ip-address'], 'server')
+
+ if config.exists(dae_new + ['secret']):
+ config.rename(dae_new + ['secret'], 'key')
+
+ # Migrate RADIUS server
+ radius_server = base + ['authentication', 'radius-server']
+ if config.exists(radius_server):
+ new_base = base + ['authentication', 'radius', 'server']
+ config.set(new_base)
+ config.set_tag(new_base)
+ for server in config.list_nodes(radius_server):
+ old_base = radius_server + [server]
+ config.copy(old_base, new_base + [server])
+
+ # migrate key
+ if config.exists(new_base + [server, 'secret']):
+ config.rename(new_base + [server, 'secret'], 'key')
+
+ # remove old req-limit node
+ if config.exists(new_base + [server, 'req-limit']):
+ config.delete(new_base + [server, 'req-limit'])
+
+ config.delete(radius_server)
+
+ # 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, 'delegation-prefix'], 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/migration-scripts/l2tp/2-to-3 b/src/migration-scripts/l2tp/2-to-3
index bd0839e03..3472ee3ed 100755
--- a/src/migration-scripts/l2tp/2-to-3
+++ b/src/migration-scripts/l2tp/2-to-3
@@ -95,13 +95,13 @@ else:
# 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 '])
+ 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)
+ config.set(ipv6_base + ['delegate', prefix, 'delegate-prefix'], value=mask)
try:
with open(file_name, 'w') as f:
diff --git a/src/migration-scripts/pppoe-server/2-to-3 b/src/migration-scripts/pppoe-server/2-to-3
new file mode 100755
index 000000000..fa6ef02da
--- /dev/null
+++ b/src/migration-scripts/pppoe-server/2-to-3
@@ -0,0 +1,142 @@
+#!/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
+
+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 = ['service', 'pppoe-server']
+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 ['server-1', 'server-2', 'server-3']:
+ 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 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)
+
+ # Migrate radius-settings node to RADIUS and use this as base for the
+ # later migration of the RADIUS servers - this will save a lot of code
+ radius_settings = base + ['authentication', 'radius-settings']
+ if config.exists(radius_settings):
+ config.rename(radius_settings, 'radius')
+
+ # Migrate RADIUS dynamic author / change of authorisation server
+ dae_old = base + ['authentication', 'radius', 'dae-server']
+ if config.exists(dae_old):
+ config.rename(dae_old, 'dynamic-author')
+ dae_new = base + ['authentication', 'radius', 'dynamic-author']
+
+ if config.exists(dae_new + ['ip-address']):
+ config.rename(dae_new + ['ip-address'], 'server')
+
+ if config.exists(dae_new + ['secret']):
+ config.rename(dae_new + ['secret'], 'key')
+
+ # Migrate RADIUS server
+ radius_server = base + ['authentication', 'radius-server']
+ if config.exists(radius_server):
+ new_base = base + ['authentication', 'radius', 'server']
+ config.set(new_base)
+ config.set_tag(new_base)
+ for server in config.list_nodes(radius_server):
+ old_base = radius_server + [server]
+ config.copy(old_base, new_base + [server])
+
+ # migrate key
+ if config.exists(new_base + [server, 'secret']):
+ config.rename(new_base + [server, 'secret'], 'key')
+
+ # remove old req-limit node
+ if config.exists(new_base + [server, 'req-limit']):
+ config.delete(new_base + [server, 'req-limit'])
+
+ config.delete(radius_server)
+
+ # 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, 'delegation-prefix'], 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/migration-scripts/pptp/1-to-2 b/src/migration-scripts/pptp/1-to-2
new file mode 100755
index 000000000..a13cc3a4f
--- /dev/null
+++ b/src/migration-scripts/pptp/1-to-2
@@ -0,0 +1,71 @@
+#!/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/>.
+
+# - migrate dns-servers node to common name-servers
+# - remove radios req-limit node
+
+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', 'pptp', '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 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'])
+
+ 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/salt/0-to-1 b/src/migration-scripts/salt/0-to-1
new file mode 100755
index 000000000..79053c056
--- /dev/null
+++ b/src/migration-scripts/salt/0-to-1
@@ -0,0 +1,58 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Delete log_file, log_level and user nodes
+# rename hash_type to hash
+# rename mine_interval to interval
+
+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 = ['service', 'salt-minion']
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+else:
+
+ # delete nodes which are now populated with sane defaults
+ for node in ['log_file', 'log_level', 'user']:
+ if config.exists(base + [node]):
+ config.delete(base + [node])
+
+ if config.exists(base + ['hash_type']):
+ config.rename(base + ['hash_type'], 'hash')
+
+ if config.exists(base + ['mine_interval']):
+ config.rename(base + ['mine_interval'], 'interval')
+
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)
diff --git a/src/op_mode/dns_forwarding_statistics.py b/src/op_mode/dns_forwarding_statistics.py
index 8ae92beb7..1fb61d263 100755
--- a/src/op_mode/dns_forwarding_statistics.py
+++ b/src/op_mode/dns_forwarding_statistics.py
@@ -4,7 +4,7 @@ import jinja2
from sys import exit
from vyos.config import Config
-from vyos.config import cmd
+from vyos.util import cmd
PDNS_CMD='/usr/bin/rec_control --socket-dir=/run/powerdns'
diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py
index 4ab91384b..69af427ec 100755
--- a/src/op_mode/powerctrl.py
+++ b/src/op_mode/powerctrl.py
@@ -15,168 +15,179 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
-import sys
-import argparse
import re
+from argparse import ArgumentParser
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
-from vyos.util import call
-from vyos.util import run
-from vyos.util import STDOUT
+from sys import exit
+from time import time
+
+from vyos.util import ask_yes_no, cmd, call, run, STDOUT
systemd_sched_file = "/run/systemd/shutdown/scheduled"
-def parse_time(s):
- try:
- if re.match(r'^\d{1,2}$', s):
- return datetime.strptime(s, "%M").time()
- else:
- return datetime.strptime(s, "%H:%M").time()
- except ValueError:
- return None
+def utc2local(datetime):
+ now = time()
+ offs = datetime.fromtimestamp(now) - datetime.utcfromtimestamp(now)
+ return datetime + offs
-def parse_date(s):
- for fmt in ["%d%m%Y", "%d/%m/%Y", "%d.%m.%Y", "%d:%m:%Y", "%Y-%m-%d"]:
+def parse_time(s):
try:
- return datetime.strptime(s, fmt).date()
+ if re.match(r'^\d{1,2}$', s):
+ return datetime.strptime(s, "%M").time()
+ else:
+ return datetime.strptime(s, "%H:%M").time()
except ValueError:
- continue
- # If nothing matched...
- return None
+ return None
+
+
+def parse_date(s):
+ for fmt in ["%d%m%Y", "%d/%m/%Y", "%d.%m.%Y", "%d:%m:%Y", "%Y-%m-%d"]:
+ try:
+ return datetime.strptime(s, fmt).date()
+ except ValueError:
+ continue
+ # If nothing matched...
+ return None
+
def get_shutdown_status():
- if os.path.exists(systemd_sched_file):
- # Get scheduled from systemd file
- with open(systemd_sched_file, 'r') as f:
- data = f.read().rstrip('\n')
- r_data = {}
- for line in data.splitlines():
- tmp_split = line.split("=")
- if tmp_split[0] == "USEC":
- # Convert USEC to human readable format
- r_data['DATETIME'] = datetime.utcfromtimestamp(int(tmp_split[1])/1000000).strftime('%Y-%m-%d %H:%M:%S')
- else:
- r_data[tmp_split[0]] = tmp_split[1]
- return r_data
- return None
+ if os.path.exists(systemd_sched_file):
+ # Get scheduled from systemd file
+ with open(systemd_sched_file, 'r') as f:
+ data = f.read().rstrip('\n')
+ r_data = {}
+ for line in data.splitlines():
+ tmp_split = line.split("=")
+ if tmp_split[0] == "USEC":
+ # Convert USEC to human readable format
+ r_data['DATETIME'] = datetime.utcfromtimestamp(
+ int(tmp_split[1])/1000000).strftime('%Y-%m-%d %H:%M:%S')
+ else:
+ r_data[tmp_split[0]] = tmp_split[1]
+ return r_data
+ return None
+
def check_shutdown():
- output = get_shutdown_status()
- if output and 'MODE' in output:
- if output['MODE'] == 'reboot':
- print("Reboot is scheduled", output['DATETIME'])
- elif output['MODE'] == 'poweroff':
- print("Poweroff is scheduled", output['DATETIME'])
- else:
- print("Reboot or poweroff is not scheduled")
+ output = get_shutdown_status()
+ if output and 'MODE' in output:
+ dt = datetime.strptime(output['DATETIME'], '%Y-%m-%d %H:%M:%S')
+ if output['MODE'] == 'reboot':
+ print("Reboot is scheduled", utc2local(dt))
+ elif output['MODE'] == 'poweroff':
+ print("Poweroff is scheduled", utc2local(dt))
+ else:
+ print("Reboot or poweroff is not scheduled")
+
def cancel_shutdown():
- output = get_shutdown_status()
- if output and 'MODE' in output:
- timenow = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
- try:
- cmd('/sbin/shutdown -c --no-wall')
- except OSError as e:
- sys.exit("Could not cancel a reboot or poweroff: %s" % e)
- message = "Scheduled %s has been cancelled %s" % (output['MODE'], timenow)
- run(f'wall {message}')
- else:
- print("Reboot or poweroff is not scheduled")
-
-def execute_shutdown(time, reboot = True, ask=True):
- if not ask:
- action = "reboot" if reboot else "poweroff"
- if not ask_yes_no("Are you sure you want to %s this system?" % action):
- sys.exit(0)
-
- action = "-r" if reboot else "-P"
-
- if len(time) == 0:
- ### T870 legacy reboot job support
- chk_vyatta_based_reboots()
- ###
-
- 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]}', stderr=STDOUT)
+ output = get_shutdown_status()
+ if output and 'MODE' in output:
+ timenow = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ try:
+ run('/sbin/shutdown -c --no-wall')
+ except OSError as e:
+ exit("Could not cancel a reboot or poweroff: %s" % e)
+
+ message = 'Scheduled {} has been cancelled {}'.format(output['MODE'], timenow)
+ run(f'wall {message} > /dev/null 2>&1')
else:
- sys.exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0]))
- elif len(time) == 2:
- # Assume it's date and time
- ts = parse_time(time[0])
- ds = parse_date(time[1])
- if ts and ds:
- t = datetime.combine(ds, ts)
- td = t - datetime.now()
- t2 = 1 + int(td.total_seconds())//60 # Get total minutes
- cmd('/sbin/shutdown {action} {t2}', stderr=STDOUT)
+ print("Reboot or poweroff is not scheduled")
+
+
+def execute_shutdown(time, reboot=True, ask=True):
+ if not ask:
+ action = "reboot" if reboot else "poweroff"
+ if not ask_yes_no("Are you sure you want to %s this system?" % action):
+ exit(0)
+
+ action = "-r" if reboot else "-P"
+
+ if len(time) == 0:
+ # T870 legacy reboot job support
+ chk_vyatta_based_reboots()
+ ###
+
+ 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]}', stderr=STDOUT)
+ else:
+ exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0]))
+ elif len(time) == 2:
+ # Assume it's date and time
+ ts = parse_time(time[0])
+ ds = parse_date(time[1])
+ if ts and ds:
+ t = datetime.combine(ds, ts)
+ td = t - datetime.now()
+ t2 = 1 + int(td.total_seconds())//60 # Get total minutes
+ cmd('/sbin/shutdown {action} {t2}', stderr=STDOUT)
+ else:
+ if not ts:
+ exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0]))
+ else:
+ exit("Invalid time \"{0}\". A valid format is YYYY-MM-DD [HH:MM]".format(time[1]))
else:
- if not ts:
- sys.exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0]))
- else:
- sys.exit("Invalid time \"{0}\". A valid format is YYYY-MM-DD [HH:MM]".format(time[1]))
- else:
- sys.exit("Could not decode date and time. Valids formats are HH:MM or YYYY-MM-DD HH:MM")
- check_shutdown()
+ exit("Could not decode date and time. Valids formats are HH:MM or YYYY-MM-DD HH:MM")
+ check_shutdown()
+
def chk_vyatta_based_reboots():
- ### T870 commit-confirm is still using the vyatta code base, once gone, the code below can be removed
- ### legacy scheduled reboot s are using at and store the is as /var/run/<name>.job
- ### name is the node of scheduled the job, commit-confirm checks for that
+ # T870 commit-confirm is still using the vyatta code base, once gone, the code below can be removed
+ # legacy scheduled reboot s are using at and store the is as /var/run/<name>.job
+ # name is the node of scheduled the job, commit-confirm checks for that
+
+ f = r'/var/run/confirm.job'
+ if os.path.exists(f):
+ jid = open(f).read().strip()
+ if jid != 0:
+ call(f'sudo atrm {jid}')
+ os.remove(f)
- f = r'/var/run/confirm.job'
- if os.path.exists(f):
- jid = open(f).read().strip()
- if jid != 0:
- call(f'sudo atrm {jid}')
- os.remove(f)
def main():
- parser = argparse.ArgumentParser()
- parser.add_argument("--yes", "-y",
- help="Do not ask for confirmation",
- action="store_true",
- dest="yes")
- action = parser.add_mutually_exclusive_group(required=True)
- action.add_argument("--reboot", "-r",
- help="Reboot the system",
- nargs="*",
- metavar="Minutes|HH:MM")
-
- action.add_argument("--poweroff", "-p",
- help="Poweroff the system",
- nargs="*",
- metavar="Minutes|HH:MM")
-
- action.add_argument("--cancel", "-c",
- help="Cancel pending shutdown",
- action="store_true")
-
- action.add_argument("--check",
- help="Check pending chutdown",
- action="store_true")
- args = parser.parse_args()
-
- try:
- if args.reboot is not None:
- execute_shutdown(args.reboot, reboot=True, ask=args.yes)
- if args.poweroff is not None:
- execute_shutdown(args.poweroff, reboot=False,ask=args.yes)
- if args.cancel:
- cancel_shutdown()
- if args.check:
- check_shutdown()
- except KeyboardInterrupt:
- sys.exit("Interrupted")
+ parser = ArgumentParser()
+ parser.add_argument("--yes", "-y",
+ help="Do not ask for confirmation",
+ action="store_true",
+ dest="yes")
+ action = parser.add_mutually_exclusive_group(required=True)
+ action.add_argument("--reboot", "-r",
+ help="Reboot the system",
+ nargs="*",
+ metavar="Minutes|HH:MM")
+
+ action.add_argument("--poweroff", "-p",
+ help="Poweroff the system",
+ nargs="*",
+ metavar="Minutes|HH:MM")
+
+ action.add_argument("--cancel", "-c",
+ help="Cancel pending shutdown",
+ action="store_true")
+
+ action.add_argument("--check",
+ help="Check pending chutdown",
+ action="store_true")
+ args = parser.parse_args()
+ try:
+ if args.reboot is not None:
+ execute_shutdown(args.reboot, reboot=True, ask=args.yes)
+ if args.poweroff is not None:
+ execute_shutdown(args.poweroff, reboot=False, ask=args.yes)
+ if args.cancel:
+ cancel_shutdown()
+ if args.check:
+ check_shutdown()
+ except KeyboardInterrupt:
+ exit("Interrupted")
if __name__ == "__main__":
- main()
-
+ main()
diff --git a/src/op_mode/show_dhcp.py b/src/op_mode/show_dhcp.py
index c49e604b7..f9577e57e 100755
--- a/src/op_mode/show_dhcp.py
+++ b/src/op_mode/show_dhcp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2019 VyOS maintainers and contributors
+# Copyright (C) 2018-2020 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -14,14 +14,14 @@
# 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 json
-import argparse
-import ipaddress
-import tabulate
-import sys
-import collections
-import os
+# TODO: merge with show_dhcpv6.py
+
+from json import dumps
+from argparse import ArgumentParser
+from ipaddress import ip_address
+from tabulate import tabulate
+from sys import exit
+from collections import OrderedDict
from datetime import datetime
from isc_dhcp_leases import Lease, IscDhcpLeases
@@ -33,7 +33,7 @@ from vyos.util import call
lease_file = "/config/dhcpd.leases"
pool_key = "shared-networkname"
-lease_display_fields = collections.OrderedDict()
+lease_display_fields = OrderedDict()
lease_display_fields['ip'] = 'IP address'
lease_display_fields['hardware_address'] = 'Hardware address'
lease_display_fields['state'] = 'State'
@@ -102,7 +102,7 @@ def get_lease_data(lease):
return data
-def get_leases(leases, state, pool=None, sort='ip'):
+def get_leases(config, leases, state, pool=None, sort='ip'):
# get leases from file
leases = IscDhcpLeases(lease_file).get()
@@ -116,7 +116,7 @@ def get_leases(leases, state, pool=None, sort='ip'):
leases = list(filter(lambda x: in_pool(x, pool), leases))
else:
print("Pool {0} does not exist.".format(pool))
- sys.exit(0)
+ exit(0)
# should maybe filter all state=active by lease.valid here?
@@ -134,7 +134,7 @@ def get_leases(leases, state, pool=None, sort='ip'):
# apply output/display sort
if sort == 'ip':
- leases = sorted(leases, key = lambda lease: int(ipaddress.ip_address(lease['ip'])))
+ leases = sorted(leases, key = lambda lease: int(ip_address(lease['ip'])))
else:
leases = sorted(leases, key = lambda lease: lease[sort])
@@ -148,7 +148,7 @@ def show_leases(leases):
lease_list_params.append(l[k])
lease_list.append(lease_list_params)
- output = tabulate.tabulate(lease_list, lease_display_fields.values())
+ output = tabulate(lease_list, lease_display_fields.values())
print(output)
@@ -161,18 +161,18 @@ def get_pool_size(config, pool):
start = config.return_effective_value("service dhcp-server shared-network-name {0} subnet {1} range {2} start".format(pool, s, r))
stop = config.return_effective_value("service dhcp-server shared-network-name {0} subnet {1} range {2} stop".format(pool, s, r))
- size += int(ipaddress.ip_address(stop)) - int(ipaddress.ip_address(start))
+ size += int(ip_address(stop)) - int(ip_address(start))
return size
def show_pool_stats(stats):
headers = ["Pool", "Size", "Leases", "Available", "Usage"]
- output = tabulate.tabulate(stats, headers)
+ output = tabulate(stats, headers)
print(output)
if __name__ == '__main__':
- parser = argparse.ArgumentParser()
+ parser = ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument("-l", "--leases", action="store_true", help="Show DHCP leases")
@@ -180,27 +180,50 @@ if __name__ == '__main__':
group.add_argument("--allowed", type=str, choices=["pool", "sort", "state"], help="Show allowed values for argument")
parser.add_argument("-p", "--pool", type=str, help="Show lease for specific pool")
- parser.add_argument("-S", "--sort", type=str, choices=lease_display_fields.keys(), default='ip', help="Sort by")
- parser.add_argument("-t", "--state", type=str, nargs="+", choices=lease_valid_states, default="active", help="Lease state to show (can specify multiple with spaces)")
+ parser.add_argument("-S", "--sort", type=str, default='ip', help="Sort by")
+ parser.add_argument("-t", "--state", type=str, nargs="+", default=["active"], help="Lease state to show (can specify multiple with spaces)")
parser.add_argument("-j", "--json", action="store_true", default=False, help="Produce JSON output")
args = parser.parse_args()
+ conf = Config()
+
+ if args.allowed == 'pool':
+ if conf.exists_effective('service dhcp-server'):
+ print(' '.join(conf.list_effective_nodes("service dhcp-server shared-network-name")))
+ exit(0)
+ elif args.allowed == 'sort':
+ print(' '.join(lease_display_fields.keys()))
+ exit(0)
+ elif args.allowed == 'state':
+ print(' '.join(lease_valid_states))
+ exit(0)
+ elif args.allowed:
+ parser.print_help()
+ exit(1)
+
+ if args.sort not in lease_display_fields.keys():
+ print(f'Invalid sort key, choose from: {list(lease_display_fields.keys())}')
+ exit(0)
+
+ if not set(args.state) < set(lease_valid_states):
+ print(f'Invalid lease state, choose from: {lease_valid_states}')
+ exit(0)
+
# Do nothing if service is not configured
- config = Config()
- if not config.exists_effective('service dhcp-server'):
+ if not conf.exists_effective('service dhcp-server'):
print("DHCP service is not configured.")
- sys.exit(0)
+ exit(0)
# if dhcp server is down, inactive leases may still be shown as active, so warn the user.
if call('systemctl -q is-active isc-dhcp-server.service') != 0:
print("WARNING: DHCP server is configured but not started. Data may be stale.")
if args.leases:
- leases = get_leases(lease_file, args.state, args.pool, args.sort)
+ leases = get_leases(conf, lease_file, args.state, args.pool, args.sort)
if args.json:
- print(json.dumps(leases, indent=4))
+ print(dumps(leases, indent=4))
else:
show_leases(leases)
@@ -211,18 +234,15 @@ if __name__ == '__main__':
if args.pool:
pools = [args.pool]
else:
- pools = config.list_effective_nodes("service dhcp-server shared-network-name")
+ pools = conf.list_effective_nodes("service dhcp-server shared-network-name")
# Get pool usage stats
stats = []
for p in pools:
- size = get_pool_size(config, p)
- leases = len(get_leases(lease_file, state='active', pool=p))
+ size = get_pool_size(conf, p)
+ leases = len(get_leases(conf, lease_file, state='active', pool=p))
- if size != 0:
- use_percentage = round(leases / size * 100)
- else:
- use_percentage = 0
+ use_percentage = round(leases / size * 100) if size != 0 else 0
if args.json:
pool_stats = {"pool": p, "size": size, "leases": leases,
@@ -234,15 +254,10 @@ if __name__ == '__main__':
# Print stats
if args.json:
- print(json.dumps(stats, indent=4))
+ print(dumps(stats, indent=4))
else:
show_pool_stats(stats)
- elif args.allowed == 'pool':
- print(' '.join(config.list_effective_nodes("service dhcp-server shared-network-name")))
- elif args.allowed == 'sort':
- print(' '.join(lease_display_fields.keys()))
- elif args.allowed == 'state':
- print(' '.join(lease_valid_states))
else:
parser.print_help()
+ exit(1)
diff --git a/src/op_mode/show_dhcpv6.py b/src/op_mode/show_dhcpv6.py
index d686defc0..ac211fb0a 100755
--- a/src/op_mode/show_dhcpv6.py
+++ b/src/op_mode/show_dhcpv6.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2019 VyOS maintainers and contributors
+# Copyright (C) 2018-2020 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -14,14 +14,14 @@
# 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 json
-import argparse
-import ipaddress
-import tabulate
-import sys
-import collections
-import os
+# TODO: merge with show_dhcp.py
+
+from json import dumps
+from argparse import ArgumentParser
+from ipaddress import ip_address
+from tabulate import tabulate
+from sys import exit
+from collections import OrderedDict
from datetime import datetime
from isc_dhcp_leases import Lease, IscDhcpLeases
@@ -32,7 +32,7 @@ from vyos.util import call
lease_file = "/config/dhcpdv6.leases"
pool_key = "shared-networkname"
-lease_display_fields = collections.OrderedDict()
+lease_display_fields = OrderedDict()
lease_display_fields['ip'] = 'IPv6 address'
lease_display_fields['state'] = 'State'
lease_display_fields['last_comm'] = 'Last communication'
@@ -108,7 +108,7 @@ def get_lease_data(lease):
return data
-def get_leases(leases, state, pool=None, sort='ip'):
+def get_leases(config, leases, state, pool=None, sort='ip'):
leases = IscDhcpLeases(lease_file).get()
# filter leases by state
@@ -121,7 +121,7 @@ def get_leases(leases, state, pool=None, sort='ip'):
leases = list(filter(lambda x: in_pool(x, pool), leases))
else:
print("Pool {0} does not exist.".format(pool))
- sys.exit(0)
+ exit(0)
# should maybe filter all state=active by lease.valid here?
@@ -139,7 +139,7 @@ def get_leases(leases, state, pool=None, sort='ip'):
# apply output/display sort
if sort == 'ip':
- leases = sorted(leases, key = lambda k: int(ipaddress.ip_address(k['ip'])))
+ leases = sorted(leases, key = lambda k: int(ip_address(k['ip'])))
else:
leases = sorted(leases, key = lambda k: k[sort])
@@ -153,12 +153,12 @@ def show_leases(leases):
lease_list_params.append(l[k])
lease_list.append(lease_list_params)
- output = tabulate.tabulate(lease_list, lease_display_fields.values())
+ output = tabulate(lease_list, lease_display_fields.values())
print(output)
if __name__ == '__main__':
- parser = argparse.ArgumentParser()
+ parser = ArgumentParser()
group = parser.add_mutually_exclusive_group()
group.add_argument("-l", "--leases", action="store_true", help="Show DHCPv6 leases")
@@ -166,36 +166,54 @@ if __name__ == '__main__':
group.add_argument("--allowed", type=str, choices=["pool", "sort", "state"], help="Show allowed values for argument")
parser.add_argument("-p", "--pool", type=str, help="Show lease for specific pool")
- parser.add_argument("-S", "--sort", type=str, choices=lease_display_fields.keys(), default='ip', help="Sort by")
- parser.add_argument("-t", "--state", type=str, nargs="+", choices=lease_valid_states, default="active", help="Lease state to show (can specify multiple with spaces)")
+ parser.add_argument("-S", "--sort", type=str, default='ip', help="Sort by")
+ parser.add_argument("-t", "--state", type=str, nargs="+", default=["active"], help="Lease state to show (can specify multiple with spaces)")
parser.add_argument("-j", "--json", action="store_true", default=False, help="Produce JSON output")
args = parser.parse_args()
+ conf = Config()
+
+ if args.allowed == 'pool':
+ if conf.exists_effective('service dhcpv6-server'):
+ print(' '.join(conf.list_effective_nodes("service dhcpv6-server shared-network-name")))
+ exit(0)
+ elif args.allowed == 'sort':
+ print(' '.join(lease_display_fields.keys()))
+ exit(0)
+ elif args.allowed == 'state':
+ print(' '.join(lease_valid_states))
+ exit(0)
+ elif args.allowed:
+ parser.print_help()
+ exit(1)
+
+ if args.sort not in lease_display_fields.keys():
+ print(f'Invalid sort key, choose from: {list(lease_display_fields.keys())}')
+ exit(0)
+
+ if not set(args.state) < set(lease_valid_states):
+ print(f'Invalid lease state, choose from: {lease_valid_states}')
+ exit(0)
+
# Do nothing if service is not configured
- c = Config()
- if not c.exists_effective('service dhcpv6-server'):
+ if not conf.exists_effective('service dhcpv6-server'):
print("DHCPv6 service is not configured")
- sys.exit(0)
+ exit(0)
# if dhcp server is down, inactive leases may still be shown as active, so warn the user.
if call('systemctl -q is-active isc-dhcp-server6.service') != 0:
print("WARNING: DHCPv6 server is configured but not started. Data may be stale.")
if args.leases:
- leases = get_leases(lease_file, args.state, args.pool, args.sort)
+ leases = get_leases(conf, lease_file, args.state, args.pool, args.sort)
if args.json:
- print(json.dumps(leases, indent=4))
+ print(dumps(leases, indent=4))
else:
show_leases(leases)
elif args.statistics:
print("DHCPv6 statistics option is not available")
- elif args.allowed == 'pool':
- print(' '.join(c.list_effective_nodes("service dhcpv6-server shared-network-name")))
- elif args.allowed == 'sort':
- print(' '.join(lease_display_fields.keys()))
- elif args.allowed == 'state':
- print(' '.join(lease_valid_states))
else:
parser.print_help()
+ exit(1)
diff --git a/src/op_mode/show_interfaces.py b/src/op_mode/show_interfaces.py
index 8b6690b7d..7041c7e16 100755
--- a/src/op_mode/show_interfaces.py
+++ b/src/op_mode/show_interfaces.py
@@ -18,6 +18,7 @@
import os
import re
import sys
+import glob
import datetime
import argparse
import netifaces
@@ -146,9 +147,20 @@ def run_allowed(**kwarg):
sys.stdout.write(' '.join(Section.interfaces()))
+def pppoe(ifname):
+ out = cmd(f'ps -C pppd -f')
+ if ifname in out:
+ return 'C'
+ elif ifname in [_.split('/')[-1] for _ in glob.glob('/etc/ppp/peers/pppoe*')]:
+ return 'D'
+ return ''
+
+
@register('show')
def run_show_intf(ifnames, iftypes, vif, vrrp):
+ handled = []
for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp):
+ handled.append(interface.ifname)
cache = interface.operational.load_counters()
out = cmd(f'ip addr show {interface.ifname}')
@@ -173,6 +185,17 @@ def run_show_intf(ifnames, iftypes, vif, vrrp):
print()
print(interface.operational.formated_stats())
+ for ifname in ifnames:
+ if ifname not in handled and ifname.startswith('pppoe'):
+ state = pppoe(ifname)
+ if not state:
+ continue
+ string = {
+ 'C': 'Coming up',
+ 'D': 'Link down',
+ }[state]
+ print('{}: {}'.format(ifname, string))
+
@register('show-brief')
def run_show_intf_brief(ifnames, iftypes, vif, vrrp):
@@ -183,7 +206,10 @@ def run_show_intf_brief(ifnames, iftypes, vif, vrrp):
print(format1 % ("Interface", "IP Address", "S/L", "Description"))
print(format1 % ("---------", "----------", "---", "-----------"))
+ handled = []
for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp):
+ handled.append(interface.ifname)
+
oper_state = interface.operational.get_state()
admin_state = interface.get_admin_state()
@@ -206,6 +232,17 @@ def run_show_intf_brief(ifnames, iftypes, vif, vrrp):
print(format2 % (i, a))
print(format1 % ('', '', '/'.join(s+l), d))
+ for ifname in ifnames:
+ if ifname not in handled and ifname.startswith('pppoe'):
+ state = pppoe(ifname)
+ if not state:
+ continue
+ string = {
+ 'C': 'u/D',
+ 'D': 'A/D',
+ }[state]
+ print(format1 % (ifname, '', string, ''))
+
@register('show-count')
def run_show_counters(ifnames, iftypes, vif, vrrp):
diff --git a/src/op_mode/version.py b/src/op_mode/version.py
index 2791f5e5c..c335eefce 100755
--- a/src/op_mode/version.py
+++ b/src/op_mode/version.py
@@ -25,14 +25,13 @@ import sys
import argparse
import json
-import pystache
-
import vyos.version
import vyos.limericks
from vyos.util import cmd
from vyos.util import call
from vyos.util import run
+from vyos.util import read_file
from vyos.util import DEVNULL
@@ -41,90 +40,42 @@ parser.add_argument("-a", "--all", action="store_true", help="Include individual
parser.add_argument("-f", "--funny", action="store_true", help="Add something funny to the output")
parser.add_argument("-j", "--json", action="store_true", help="Produce JSON output")
-def read_file(name):
- try:
- with open (name, "r") as f:
- data = f.read()
- return data.strip()
- except:
- # This works since we only read /sys/class/* stuff
- # with this function
- return "Unknown"
version_output_tmpl = """
-Version: VyOS {{version}}
-Release Train: {{release_train}}
+Version: VyOS {version}
+Release Train: {release_train}
-Built by: {{built_by}}
-Built on: {{built_on}}
-Build UUID: {{build_uuid}}
-Build Commit ID: {{build_git}}
+Built by: {built_by}
+Built on: {built_on}
+Build UUID: {build_uuid}
+Build Commit ID: {build_git}
-Architecture: {{system_arch}}
-Boot via: {{boot_via}}
-System type: {{system_type}}
+Architecture: {system_arch}
+Boot via: {boot_via}
+System type: {system_type}
-Hardware vendor: {{hardware_vendor}}
-Hardware model: {{hardware_model}}
-Hardware S/N: {{hardware_serial}}
-Hardware UUID: {{hardware_uuid}}
+Hardware vendor: {hardware_vendor}
+Hardware model: {hardware_model}
+Hardware S/N: {hardware_serial}
+Hardware UUID: {hardware_uuid}
Copyright: VyOS maintainers and contributors
-
"""
if __name__ == '__main__':
args = parser.parse_args()
- version_data = vyos.version.get_version_data()
-
- # Get system architecture (well, kernel architecture rather)
- version_data['system_arch'] = cmd('uname -m')
-
-
- # Get hypervisor name, if any
- system_type = "bare metal"
- try:
- hypervisor = cmd('hvinfo',stderr=DEVNULL)
- system_type = "{0} guest".format(hypervisor)
- except OSError:
- # hvinfo returns 1 if it cannot detect any hypervisor
- pass
- version_data['system_type'] = system_type
-
-
- # Get boot type, it can be livecd, installed image, or, possible, a system installed
- # via legacy "install system" mechanism
- # In installed images, the squashfs image file is named after its image version,
- # while on livecd it's just "filesystem.squashfs", that's how we tell a livecd boot
- # from an installed image
- boot_via = "installed image"
- if run(""" grep -e '^overlay.*/filesystem.squashfs' /proc/mounts >/dev/null""") == 0:
- boot_via = "livecd"
- elif run(""" grep '^overlay /' /proc/mounts >/dev/null """) != 0:
- boot_via = "legacy non-image installation"
- version_data['boot_via'] = boot_via
-
-
- # Get hardware details from DMI
- version_data['hardware_vendor'] = read_file('/sys/class/dmi/id/sys_vendor')
- version_data['hardware_model'] = read_file('/sys/class/dmi/id/product_name')
-
- # These two assume script is run as root, normal users can't access those files
- version_data['hardware_serial'] = read_file('/sys/class/dmi/id/subsystem/id/product_serial')
- version_data['hardware_uuid'] = read_file('/sys/class/dmi/id/subsystem/id/product_uuid')
-
+ version_data = vyos.version.get_full_version_data()
if args.json:
print(json.dumps(version_data))
sys.exit(0)
- else:
- output = pystache.render(version_output_tmpl, version_data).strip()
- print(output)
- if args.all:
- print("Package versions:")
- call("dpkg -l")
+ print(version_output_tmpl.format(**version_data).strip())
+
+ if args.all:
+ print("Package versions:")
+ call("dpkg -l")
- if args.funny:
- print(vyos.limericks.get_random())
+ if args.funny:
+ print(vyos.limericks.get_random())
diff --git a/src/op_mode/vrrp.py b/src/op_mode/vrrp.py
index 923cfa4d4..e024d7f63 100755
--- a/src/op_mode/vrrp.py
+++ b/src/op_mode/vrrp.py
@@ -21,7 +21,6 @@ import argparse
import json
import tabulate
-import vyos.keepalived
import vyos.util
from vyos.ifconfig.vrrp import VRRP
diff --git a/src/op_mode/wireguard.py b/src/op_mode/wireguard.py
index 297ba599d..15bf63e81 100755
--- a/src/op_mode/wireguard.py
+++ b/src/op_mode/wireguard.py
@@ -147,8 +147,12 @@ if __name__ == '__main__':
if args.listkdir:
list_key_dirs()
if args.showinterface:
- intf = WireGuardIf(args.showinterface, create=False, debug=False)
- print(intf.operational.show_interface())
+ try:
+ intf = WireGuardIf(args.showinterface, create=False, debug=False)
+ print(intf.operational.show_interface())
+ # the interface does not exists
+ except Exception:
+ pass
if args.delkdir:
if args.location:
del_key_dir(args.location)
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
index b5ad8b159..c36cbd640 100755
--- a/src/services/vyos-http-api-server
+++ b/src/services/vyos-http-api-server
@@ -22,6 +22,7 @@ import grp
import json
import traceback
import threading
+import signal
import vyos.config
@@ -317,11 +318,20 @@ def generate_op():
command = json.loads(command)
try:
- cmd = command['cmd']
- res = session.generate(cmd)
+ op = command['op']
+ path = command['path']
except KeyError:
- return error(400, "Missing required field \"cmd\"")
- except VyOSError as e:
+ return error(400, "Missing required field. \"op\" and \"path\" fields are required")
+
+ if not isinstance(path, list):
+ return error(400, "Malformed command: \"path\" field must be a list of strings")
+
+ try:
+ if op == 'generate':
+ res = session.generate(path)
+ else:
+ return error(400, "\"{0}\" is not a valid operation".format(op))
+ except ConfigSessionError as e:
return error(400, str(e))
except Exception as e:
print(traceback.format_exc(), file=sys.stderr)
@@ -338,11 +348,20 @@ def show_op():
command = json.loads(command)
try:
- cmd = command['cmd']
- res = session.show(cmd)
+ op = command['op']
+ path = command['path']
except KeyError:
- return error(400, "Missing required field \"cmd\"")
- except VyOSError as e:
+ return error(400, "Missing required field. \"op\" and \"path\" fields are required")
+
+ if not isinstance(path, list):
+ return error(400, "Malformed command: \"path\" field must be a list of strings")
+
+ try:
+ if op == 'show':
+ res = session.show(path)
+ else:
+ return error(400, "\"{0}\" is not a valid operation".format(op))
+ except ConfigSessionError as e:
return error(400, str(e))
except Exception as e:
print(traceback.format_exc(), file=sys.stderr)
@@ -350,6 +369,8 @@ def show_op():
return success(res)
+def shutdown():
+ raise KeyboardInterrupt
if __name__ == '__main__':
# systemd's user and group options don't work, do it by hand here,
@@ -372,4 +393,9 @@ if __name__ == '__main__':
app.config['vyos_keys'] = server_config['api_keys']
app.config['vyos_debug'] = server_config['debug']
+ def sig_handler(signum, frame):
+ shutdown()
+
+ signal.signal(signal.SIGTERM, sig_handler)
+
bottle.run(app, host=server_config["listen_address"], port=server_config["port"], debug=True)
diff --git a/src/systemd/dhclient6@.service b/src/systemd/dhclient6@.service
new file mode 100644
index 000000000..fd69e4d48
--- /dev/null
+++ b/src/systemd/dhclient6@.service
@@ -0,0 +1,18 @@
+[Unit]
+Description=DHCPv6 client on %i
+Documentation=man:dhclient(8)
+ConditionPathExists=/var/lib/dhcp/dhclient_v6_%i.conf
+ConditionPathExists=/var/lib/dhcp/dhclient_v6_%i.options
+After=vyos-router.service
+
+[Service]
+WorkingDirectory=/var/lib/dhcp
+Type=exec
+EnvironmentFile=-/var/lib/dhcp/dhclient_v6_%i.options
+PIDFile=/var/lib/dhcp/dhclient_v6_%i.pid
+ExecStart=/sbin/dhclient -6 $DHCLIENT_OPTS
+ExecStop=/sbin/dhclient -6 $DHCLIENT_OPTS -r
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/systemd/dhclient@.service b/src/systemd/dhclient@.service
new file mode 100644
index 000000000..2ced1038a
--- /dev/null
+++ b/src/systemd/dhclient@.service
@@ -0,0 +1,18 @@
+[Unit]
+Description=DHCP client on %i
+Documentation=man:dhclient(8)
+ConditionPathExists=/var/lib/dhcp/dhclient_%i.conf
+ConditionPathExists=/var/lib/dhcp/dhclient_%i.options
+After=vyos-router.service
+
+[Service]
+WorkingDirectory=/var/lib/dhcp
+Type=exec
+EnvironmentFile=-/var/lib/dhcp/dhclient_%i.options
+PIDFile=/var/lib/dhcp/dhclient_%i.pid
+ExecStart=/sbin/dhclient -4 $DHCLIENT_OPTS
+ExecStop=/sbin/dhclient -4 $DHCLIENT_OPTS -r
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/systemd/isc-dhcp-server6.service b/src/systemd/isc-dhcp-server6.service
index 743f16840..27bebc57f 100644
--- a/src/systemd/isc-dhcp-server6.service
+++ b/src/systemd/isc-dhcp-server6.service
@@ -2,7 +2,7 @@
Description=ISC DHCP IPv6 server
Documentation=man:dhcpd(8)
RequiresMountsFor=/run
-ConditionPathExists=/run/dhcp-server/dhcpd.conf
+ConditionPathExists=/run/dhcp-server/dhcpdv6.conf
After=vyos-router.service
[Service]
diff --git a/src/validators/numeric b/src/validators/numeric
index 0a2d83d14..2cd5178b9 100755
--- a/src/validators/numeric
+++ b/src/validators/numeric
@@ -19,7 +19,6 @@
import sys
import argparse
-import re
parser = argparse.ArgumentParser()
parser.add_argument("-f", "--float", action="store_true", help="Accept floating point values")
@@ -50,8 +49,9 @@ if args.range:
valid = False
for r in args.range:
try:
- lower, upper = re.match(r'(\d+)\s*\-\s*(\d+)', r).groups()
- lower, upper = int(lower), int(upper)
+ list = r.split('-')
+ lower = int(list[0])
+ upper = int(list[1])
except:
print("{0} is not a valid number range",format(args.range), file=sys.stderr)
sys.exit(1)