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.py145
-rwxr-xr-xsrc/conf_mode/dns_forwarding.py6
-rwxr-xr-xsrc/conf_mode/dynamic_dns.py4
-rwxr-xr-xsrc/conf_mode/flow_accounting_conf.py2
-rwxr-xr-xsrc/conf_mode/interfaces-bonding.py331
-rwxr-xr-xsrc/conf_mode/interfaces-bridge.py156
-rwxr-xr-xsrc/conf_mode/interfaces-dummy.py36
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py262
-rwxr-xr-xsrc/conf_mode/interfaces-geneve.py22
-rwxr-xr-xsrc/conf_mode/interfaces-l2tpv3.py56
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py82
-rwxr-xr-xsrc/conf_mode/interfaces-pppoe.py56
-rwxr-xr-xsrc/conf_mode/interfaces-pseudo-ethernet.py248
-rwxr-xr-xsrc/conf_mode/interfaces-tunnel.py55
-rwxr-xr-xsrc/conf_mode/interfaces-vxlan.py53
-rwxr-xr-xsrc/conf_mode/interfaces-wireguard.py69
-rwxr-xr-xsrc/conf_mode/interfaces-wireless.py212
-rwxr-xr-xsrc/conf_mode/interfaces-wirelessmodem.py60
-rwxr-xr-xsrc/conf_mode/ipsec-settings.py2
-rwxr-xr-xsrc/conf_mode/nat.py268
-rwxr-xr-xsrc/conf_mode/protocols_bfd.py2
-rwxr-xr-xsrc/conf_mode/protocols_igmp.py2
-rwxr-xr-xsrc/conf_mode/protocols_mpls.py2
-rwxr-xr-xsrc/conf_mode/protocols_pim.py2
-rwxr-xr-xsrc/conf_mode/protocols_static_multicast.py2
-rwxr-xr-xsrc/conf_mode/salt-minion.py103
-rwxr-xr-xsrc/conf_mode/service_ipoe-server.py20
-rwxr-xr-xsrc/conf_mode/service_pppoe-server.py22
-rwxr-xr-xsrc/conf_mode/system-login.py11
-rwxr-xr-xsrc/conf_mode/vpn_l2tp.py4
-rwxr-xr-xsrc/conf_mode/vpn_pptp.py4
-rwxr-xr-xsrc/conf_mode/vpn_sstp.py60
-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
-rw-r--r--src/etc/systemd/system/pdns-recursor.service.d/override.conf2
-rwxr-xr-xsrc/helpers/validate-value.py45
-rwxr-xr-xsrc/migration-scripts/dhcpv6-server/0-to-161
-rwxr-xr-xsrc/migration-scripts/nat/4-to-558
-rwxr-xr-xsrc/migration-scripts/salt/0-to-158
-rwxr-xr-xsrc/op_mode/flow_accounting_op.py2
-rwxr-xr-xsrc/op_mode/powerctrl.py291
-rwxr-xr-xsrc/op_mode/show_interfaces.py43
-rwxr-xr-xsrc/op_mode/show_nat_statistics.py63
-rwxr-xr-xsrc/op_mode/to_be_migrated/vyatta-nat-translations.pl267
-rwxr-xr-xsrc/services/vyos-hostsd13
-rwxr-xr-xsrc/services/vyos-http-api-server92
-rwxr-xr-xsrc/system/on-dhcp-event.sh4
-rw-r--r--src/systemd/dhclient@.service18
-rw-r--r--src/systemd/dhcp6c@.service16
-rw-r--r--src/systemd/isc-dhcp-relay.service8
-rw-r--r--src/systemd/isc-dhcp-relay6.service8
-rw-r--r--src/systemd/isc-dhcp-server.service19
-rw-r--r--src/systemd/isc-dhcp-server6.service20
-rwxr-xr-xsrc/validators/file-exists71
-rwxr-xr-xsrc/validators/fqdn21
-rwxr-xr-xsrc/validators/ip-protocol41
-rwxr-xr-xsrc/validators/ipv4-address-exclude7
-rwxr-xr-xsrc/validators/ipv4-prefix-exclude7
-rwxr-xr-xsrc/validators/ipv4-range33
-rwxr-xr-xsrc/validators/ipv4-range-exclude7
-rwxr-xr-xsrc/validators/mac-address17
-rwxr-xr-xsrc/validators/numeric78
-rwxr-xr-xsrc/validators/script31
-rwxr-xr-xsrc/validators/timezone22
-rwxr-xr-xsrc/validators/vrf-name31
70 files changed, 2457 insertions, 1605 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 ce98e39c3..9e24ee591 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': [],
@@ -84,6 +85,7 @@ def get_config():
'nis_server': [],
'nisp_domain': '',
'nisp_server': [],
+ 'prefix_delegation': [],
'sip_address': [],
'sip_hostname': [],
'sntp_server': [],
@@ -94,25 +96,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 +122,83 @@ 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'])
+
+ # Local SIP server that is to be used for all outbound SIP requests - IPv6 address
+ 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'])
# Prefix Delegation (RFC 3633)
- if conf.exists('prefix-delegation'):
- print('TODO: This option is actually not implemented right now!')
+ if conf.exists(['prefix-delegation', 'start']):
+ for address in conf.list_nodes(['prefix-delegation', 'start']):
+ conf.set_level(base + ['shared-network-name', network, 'subnet', net, 'prefix-delegation', 'start', address])
+ prefix = {
+ 'start' : address,
+ 'stop' : '',
+ 'length' : ''
+ }
- # 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')
+ if conf.exists(['prefix-length']):
+ prefix['length'] = conf.return_value(['prefix-length'])
- # 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(['stop']):
+ prefix['stop'] = conf.return_value(['stop'])
- # 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')
+ subnet['prefix_delegation'].append(prefix)
#
# 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))
+ conf.set_level(base + ['shared-network-name', network, 'subnet', net])
+ 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 +207,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,7 +224,6 @@ 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)
@@ -282,6 +296,14 @@ def verify(dhcpv6):
else:
range6_stop.append(stop)
+ # Prefix delegation sanity checks
+ for prefix in subnet['prefix_delegation']:
+ if not prefix['stop']:
+ raise ConfigError('Stop address of delegated IPv6 prefix range must be configured')
+
+ if not prefix['length']:
+ raise ConfigError('Length of delegated IPv6 prefix must be configured')
+
# We also have prefixes that require checking
for prefix in subnet['range6_prefix']:
# If configured prefix does not match our subnet, we have to check that it's inside
@@ -335,11 +357,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
diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py
index 567dfa4b3..f87c198f7 100755
--- a/src/conf_mode/dns_forwarding.py
+++ b/src/conf_mode/dns_forwarding.py
@@ -152,11 +152,7 @@ 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)
+ render(config_file, 'dns-forwarding/recursor.conf.tmpl', dns, trim_blocks=True, user='pdns', group='pdns')
return None
def apply(dns):
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/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py
index 11df81b1d..d691e6abd 100755
--- a/src/conf_mode/flow_accounting_conf.py
+++ b/src/conf_mode/flow_accounting_conf.py
@@ -281,7 +281,7 @@ def verify(config):
# check if configured netflow source-ip exist in the system
if config['netflow']['source-ip']:
source_ip_presented = None
- for iface in Interface.listing():
+ for iface in Section.interfaces():
for address in Interface(iface).get_addr():
# check an IP
regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$')
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
index 380457772..bdca9d170 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
@@ -21,52 +21,30 @@ from sys import exit
from netifaces import interfaces
from vyos.ifconfig import BondIf
-from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
-from vyos.configdict import list_diff, vlan_to_dict
+from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config
+from vyos.configdict import list_diff, intf_to_dict, add_to_dict, interface_default_data
from vyos.config import Config
-from vyos.util import call
-from vyos.validate import is_bridge_member
+from vyos.util import call, cmd
+from vyos.validate import is_member, has_address_configured
from vyos import ConfigError
default_config_data = {
- 'address': [],
- 'address_remove': [],
+ **interface_default_data,
'arp_mon_intvl': 0,
'arp_mon_tgt': [],
- 'description': '',
'deleted': False,
- 'dhcp_client_id': '',
- 'dhcp_hostname': '',
- 'dhcp_vendor_class_id': '',
- 'dhcpv6_prm_only': False,
- 'dhcpv6_temporary': False,
- '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,
- 'ip_enable_arp_announce': 0,
- 'ip_enable_arp_ignore': 0,
- 'ip_proxy_arp': 0,
'ip_proxy_arp_pvlan': 0,
- 'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
- 'ipv6_forwarding': 1,
- 'ipv6_dup_addr_detect': 1,
- 'is_bridge_member': False,
- 'mac': '',
'mode': '802.3ad',
'member': [],
'shutdown_required': False,
- 'mtu': 1500,
'primary': '',
- 'vif_s': [],
+ 'vif_s': {},
'vif_s_remove': [],
- 'vif': [],
+ 'vif': {},
'vif_remove': [],
- 'vrf': ''
}
@@ -89,6 +67,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
@@ -97,34 +82,18 @@ 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, bond['intf'])
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)
# ARP link monitoring frequency in milliseconds
if conf.exists('arp-monitor interval'):
@@ -134,38 +103,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')
@@ -174,50 +111,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')
@@ -227,10 +124,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')
@@ -247,35 +140,8 @@ 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
@@ -283,22 +149,38 @@ def get_config():
def verify(bond):
if bond['deleted']:
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}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{bond["intf"]}" as it is a '
+ f'member of bridge "{bond["is_bridge_member"]}"!'))
+
return None
- if len (bond['arp_mon_tgt']) > 16:
- raise ConfigError('The maximum number of targets that can be specified is 16')
+ if len(bond['arp_mon_tgt']) > 16:
+ raise ConfigError('The maximum number of arp-monitor targets is 16')
if bond['primary']:
if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:
- raise ConfigError('Mode dependency failed, primary not supported ' \
- 'in mode "{}"!'.format(bond['mode']))
+ raise ConfigError((
+ 'Mode dependency failed, primary not supported in mode '
+ f'"{bond["mode"]}"!'))
+
+ if ( bond['is_bridge_member']
+ and ( bond['address']
+ or bond['ipv6_eui64_prefix']
+ or bond['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{bond["intf"]}" '
+ f'as it is a member of bridge "{bond["is_bridge_member"]}"!'))
+
+ if bond['vrf']:
+ if bond['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{bond["vrf"]}" does not exist')
- vrf_name = bond['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ if bond['is_bridge_member']:
+ raise ConfigError((
+ f'Interface "{bond["intf"]}" cannot be member of VRF '
+ f'"{bond["vrf"]}" and bridge {bond["is_bridge_member"]} '
+ f'at the same time!'))
# use common function to verify VLAN configuration
verify_vlan_config(bond)
@@ -307,51 +189,55 @@ def verify(bond):
for intf in bond['member']:
# check if member interface is "real"
if intf not in interfaces():
- raise ConfigError('interface {} does not exist!'.format(intf))
+ raise ConfigError(f'Interface {intf} does not exist!')
# a bonding member interface is only allowed to be assigned to one bond!
all_bonds = conf.list_nodes('interfaces bonding')
# We do not need to check our own bond
all_bonds.remove(bond['intf'])
for tmp in all_bonds:
- if conf.exists('interfaces bonding ' + tmp + ' member interface ' + intf):
- raise ConfigError('can not enslave interface {} which already ' \
- 'belongs to {}'.format(intf, tmp))
+ if conf.exists('interfaces bonding {tmp} member interface {intf}'):
+ raise ConfigError((
+ f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
+ f'it is already a member of bond "{tmp}"!'))
# can not add interfaces with an assigned address to a bond
- if conf.exists('interfaces ethernet ' + intf + ' address'):
- raise ConfigError('can not enslave interface {} which has an address ' \
- 'assigned'.format(intf))
-
- # bond members are not allowed to be bridge members, too
- for tmp in conf.list_nodes('interfaces bridge'):
- if conf.exists('interfaces bridge ' + tmp + ' member interface ' + intf):
- raise ConfigError('can not enslave interface {} which belongs to ' \
- 'bridge {}'.format(intf, tmp))
-
- # bond members are not allowed to be vrrp members, too
+ if has_address_configured(conf, intf):
+ raise ConfigError((
+ f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
+ f'it has an address assigned!'))
+
+ # bond members are not allowed to be bridge members
+ tmp = is_member(conf, intf, 'bridge')
+ if tmp:
+ raise ConfigError((
+ f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
+ f'it is already a member of bridge "{tmp}"!'))
+
+ # bond members are not allowed to be vrrp members
for tmp in conf.list_nodes('high-availability vrrp group'):
- if conf.exists('high-availability vrrp group ' + tmp + ' interface ' + intf):
- raise ConfigError('can not enslave interface {} which belongs to ' \
- 'VRRP group {}'.format(intf, tmp))
+ if conf.exists('high-availability vrrp group {tmp} interface {intf}'):
+ raise ConfigError((
+ f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
+ f'it is already a member of VRRP group "{tmp}"!'))
# bond members are not allowed to be underlaying psuedo-ethernet devices
for tmp in conf.list_nodes('interfaces pseudo-ethernet'):
- if conf.exists('interfaces pseudo-ethernet ' + tmp + ' link ' + intf):
- raise ConfigError('can not enslave interface {} which belongs to ' \
- 'pseudo-ethernet {}'.format(intf, tmp))
+ if conf.exists('interfaces pseudo-ethernet {tmp} link {intf}'):
+ raise ConfigError((
+ f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
+ f'it is already the link of pseudo-ethernet "{tmp}"!'))
# bond members are not allowed to be underlaying vxlan devices
for tmp in conf.list_nodes('interfaces vxlan'):
- if conf.exists('interfaces vxlan ' + tmp + ' link ' + intf):
- raise ConfigError('can not enslave interface {} which belongs to ' \
- 'vxlan {}'.format(intf, tmp))
-
+ if conf.exists('interfaces vxlan {tmp} link {intf}'):
+ raise ConfigError((
+ f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
+ f'it is already the link of VXLAN "{tmp}"!'))
if bond['primary']:
if bond['primary'] not in bond['member']:
- raise ConfigError('primary interface must be a member interface of {}' \
- .format(bond['intf']))
+ raise ConfigError(f'Bond "{bond["intf"]}" primary interface must be a member')
if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:
raise ConfigError('primary interface only works for mode active-backup, ' \
@@ -364,7 +250,6 @@ def verify(bond):
return None
-
def generate(bond):
return None
@@ -414,6 +299,9 @@ def apply(bond):
if bond['dhcpv6_temporary']:
b.dhcp.v6.options['dhcpv6_temporary'] = True
+ if bond['dhcpv6_pd']:
+ b.dhcp.v6.options['dhcpv6_pd'] = bond['dhcpv6_pd']
+
# ignore link state changes
b.set_link_detect(bond['disable_link_detect'])
# Bonding transmit hash policy
@@ -432,19 +320,27 @@ def apply(bond):
b.set_proxy_arp(bond['ip_proxy_arp'])
# Enable private VLAN proxy ARP on this interface
b.set_proxy_arp_pvlan(bond['ip_proxy_arp_pvlan'])
+ # IPv6 accept RA
+ b.set_ipv6_accept_ra(bond['ipv6_accept_ra'])
# 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 +363,9 @@ def apply(bond):
# Add (enslave) interfaces to bond
for intf in bond['member']:
+ # if we've come here we already verified the interface doesn't
+ # have addresses configured so just flush any remaining ones
+ cmd(f'ip addr flush dev "{intf}"')
b.add_port(intf)
# As the bond interface is always disabled first when changing
@@ -485,37 +384,17 @@ def apply(bond):
for addr in bond['address']:
b.add_addr(addr)
- # assign/remove VRF
- b.set_vrf(bond['vrf'])
-
- # remove no longer required service VLAN interfaces (vif-s)
- for vif_s in bond['vif_s_remove']:
- b.del_vlan(vif_s)
-
- # create service VLAN interfaces (vif-s)
- for vif_s in bond['vif_s']:
- s_vlan = b.add_vlan(vif_s['id'], ethertype=vif_s['ethertype'])
- apply_vlan_config(s_vlan, vif_s)
-
- # remove no longer required client VLAN interfaces (vif-c)
- # on lower service VLAN interface
- for vif_c in vif_s['vif_c_remove']:
- s_vlan.del_vlan(vif_c)
-
- # create client VLAN interfaces (vif-c)
- # on lower service VLAN interface
- for vif_c in vif_s['vif_c']:
- c_vlan = s_vlan.add_vlan(vif_c['id'])
- apply_vlan_config(c_vlan, vif_c)
-
- # remove no longer required VLAN interfaces (vif)
- for vif in bond['vif_remove']:
- b.del_vlan(vif)
-
- # create VLAN interfaces (vif)
- for vif in bond['vif']:
- vlan = b.add_vlan(vif['id'])
- apply_vlan_config(vlan, vif)
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not bond['is_bridge_member']:
+ b.set_vrf(bond['vrf'])
+
+ # re-add ourselves to any bridge we might have fallen out of
+ if bond['is_bridge_member']:
+ b.add_to_bridge(bond['is_bridge_member'])
+
+ # apply all vlans to interface
+ apply_all_vlans(b, bond)
return None
diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py
index 93c6db97e..3ff339f0f 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,45 +20,28 @@ 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.configdict import list_diff, interface_default_data
+from vyos.validate import is_member, has_address_configured
from vyos.config import Config
+from vyos.util import cmd, get_bridge_member_config
from vyos import ConfigError
default_config_data = {
- 'address': [],
- 'address_remove': [],
+ **interface_default_data,
'aging': 300,
'arp_cache_tmo': 30,
- 'description': '',
'deleted': False,
- 'dhcp_client_id': '',
- 'dhcp_hostname': '',
- 'dhcp_vendor_class_id': '',
- 'dhcpv6_prm_only': False,
- 'dhcpv6_temporary': False,
- 'disable': False,
- 'disable_link_detect': 1,
'forwarding_delay': 14,
'hello_time': 2,
- 'ip_disable_arp_filter': 1,
- 'ip_enable_arp_accept': 0,
- 'ip_enable_arp_announce': 0,
- 'ip_enable_arp_ignore': 0,
- 'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
- 'ipv6_forwarding': 1,
- 'ipv6_dup_addr_detect': 1,
'igmp_querier': 0,
'intf': '',
- 'mac' : '',
'max_age': 20,
'member': [],
'member_remove': [],
'priority': 32768,
- 'stp': 0,
- 'vrf': ''
+ 'stp': 0
}
def get_config():
@@ -160,9 +143,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,28 +171,29 @@ 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']
+
+ # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
+ # accept_ra must be 2
+ if bridge['ipv6_autoconf'] or 'dhcpv6' in bridge['address']:
+ bridge['ipv6_accept_ra'] = 2
+
# Interval at which neighbor bridges are removed
if conf.exists('max-age'):
bridge['max_age'] = int(conf.return_value('max-age'))
# Determine bridge member interface (currently configured)
for intf in conf.list_nodes('member interface'):
- # cost and priority initialized with linux defaults
- # by reading /sys/devices/virtual/net/br0/brif/eth2/{path_cost,priority}
- # after adding interface to bridge after reboot
- iface = {
- 'name': intf,
- 'cost': 100,
- 'priority': 32
- }
-
- if conf.exists('member interface {} cost'.format(intf)):
- iface['cost'] = int(conf.return_value('member interface {} cost'.format(intf)))
-
- if conf.exists('member interface {} priority'.format(intf)):
- iface['priority'] = int(conf.return_value('member interface {} priority'.format(intf)))
-
- bridge['member'].append(iface)
+ # defaults are stored in util.py (they can't be here as all interface
+ # scripts use the function)
+ memberconf = get_bridge_member_config(conf, bridge['intf'], intf)
+ if memberconf:
+ memberconf['name'] = intf
+ bridge['member'].append(memberconf)
# Determine bridge member interface (currently effective) - to determine which
# interfaces is no longer assigend to the bridge and thus can be removed
@@ -228,30 +224,40 @@ def verify(bridge):
raise ConfigError(f'VRF "{vrf_name}" does not exist')
conf = Config()
- for br in conf.list_nodes('interfaces bridge'):
- # it makes no sense to verify ourself in this case
- if br == bridge['intf']:
- continue
-
- for intf in bridge['member']:
- tmp = conf.list_nodes('interfaces bridge {} member interface'.format(br))
- if intf['name'] in tmp:
- raise ConfigError('Interface "{}" belongs to bridge "{}" and can not be enslaved.'.format(intf['name'], bridge['intf']))
-
- # the interface must exist prior adding it to a bridge
for intf in bridge['member']:
+ # the interface must exist prior adding it to a bridge
if intf['name'] not in interfaces():
- raise ConfigError('Can not add non existing interface "{}" to bridge "{}"'.format(intf['name'], bridge['intf']))
+ raise ConfigError((
+ f'Cannot add nonexistent interface "{intf["name"]}" '
+ f'to bridge "{bridge["intf"]}"'))
if intf['name'] == 'lo':
raise ConfigError('Loopback interface "lo" can not be added to a bridge')
- # bridge members are not allowed to be bond members, too
- for intf in bridge['member']:
- for bond in conf.list_nodes('interfaces bonding'):
- if conf.exists('interfaces bonding ' + bond + ' member interface'):
- if intf['name'] in conf.return_values('interfaces bonding ' + bond + ' member interface'):
- raise ConfigError('Interface {} belongs to bond {}, can not add it to {}'.format(intf['name'], bond, bridge['intf']))
+ # bridge members aren't allowed to be members of another bridge
+ for br in conf.list_nodes('interfaces bridge'):
+ # it makes no sense to verify ourself in this case
+ if br == bridge['intf']:
+ continue
+
+ tmp = conf.list_nodes(f'interfaces bridge {br} member interface')
+ if intf['name'] in tmp:
+ raise ConfigError((
+ f'Cannot add interface "{intf["name"]}" to bridge '
+ f'"{bridge["intf"]}", it is already a member of bridge "{br}"!'))
+
+ # bridge members are not allowed to be bond members
+ tmp = is_member(conf, intf['name'], 'bonding')
+ if tmp:
+ raise ConfigError((
+ f'Cannot add interface "{intf["name"]}" to bridge '
+ f'"{bridge["intf"]}", it is already a member of bond "{tmp}"!'))
+
+ # bridge members must not have an assigned address
+ if has_address_configured(conf, intf['name']):
+ raise ConfigError((
+ f'Cannot add interface "{intf["name"]}" to bridge '
+ f'"{bridge["intf"]}", it has an address assigned!'))
return None
@@ -281,10 +287,10 @@ def apply(bridge):
br.set_arp_announce(bridge['ip_enable_arp_announce'])
# configure ARP ignore
br.set_arp_ignore(bridge['ip_enable_arp_ignore'])
+ # IPv6 accept RA
+ br.set_ipv6_accept_ra(bridge['ipv6_accept_ra'])
# 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
@@ -315,12 +321,16 @@ def apply(bridge):
if bridge['dhcpv6_temporary']:
br.dhcp.v6.options['dhcpv6_temporary'] = True
+ if bridge['dhcpv6_pd']:
+ br.dhcp.v6.options['dhcpv6_pd'] = br['dhcpv6_pd']
+
# 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 +338,20 @@ def apply(bridge):
# add interfaces to bridge
for member in bridge['member']:
+ # if we've come here we already verified the interface doesn't
+ # have addresses configured so just flush any remaining ones
+ 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')
@@ -347,9 +369,9 @@ def apply(bridge):
for member in bridge['member']:
i = STPBridgeIf(member['name'])
# configure ARP cache timeout
- i.set_arp_cache_tmo(bridge['arp_cache_tmo'])
+ i.set_arp_cache_tmo(member['arp_cache_tmo'])
# ignore link state changes
- i.set_link_detect(bridge['disable_link_detect'])
+ i.set_link_detect(member['disable_link_detect'])
# set bridge port path cost
i.set_path_cost(member['cost'])
# set bridge port path priority
diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py
index 23eaa4ecb..4a77b0c1a 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.validate import is_bridge_member
+from vyos.validate import is_member
from vyos import ConfigError
default_config_data = {
@@ -47,11 +47,12 @@ def get_config():
dummy['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ # check if we are a member of any bridge
+ dummy['is_bridge_member'] = is_member(conf, dummy['intf'], 'bridge')
+
# 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
@@ -84,15 +85,26 @@ def get_config():
def verify(dummy):
if dummy['deleted']:
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}"!')
+ raise ConfigError((
+ f'Interface "{dummy["intf"]}" cannot be deleted as it is a '
+ f'member of bridge "{dummy["is_bridge_member"]}"!'))
return None
- vrf_name = dummy['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ if dummy['vrf']:
+ if dummy['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{dummy["vrf"]}" does not exist')
+
+ if dummy['is_bridge_member']:
+ raise ConfigError((
+ f'Interface "{dummy["intf"]}" cannot be member of VRF '
+ f'"{dummy["vrf"]}" and bridge "{dummy["is_bridge_member"]}" '
+ f'at the same time!'))
+
+ if dummy['is_bridge_member'] and dummy['address']:
+ raise ConfigError((
+ f'Cannot assign address to interface "{dummy["intf"]}" '
+ f'as it is a member of bridge "{dummy["is_bridge_member"]}"!'))
return None
@@ -117,8 +129,10 @@ def apply(dummy):
for addr in dummy['address']:
d.add_addr(addr)
- # assign/remove VRF
- d.set_vrf(dummy['vrf'])
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not dummy['is_bridge_member']:
+ d.set_vrf(dummy['vrf'])
# disable interface on demand
if dummy['disable']:
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index 5a977d797..f45a77a3e 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
@@ -21,66 +21,49 @@ from copy import deepcopy
from netifaces import interfaces
from vyos.ifconfig import EthernetIf
-from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
-from vyos.configdict import list_diff, vlan_to_dict
+from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config
+from vyos.configdict import list_diff, intf_to_dict, add_to_dict, interface_default_data
+from vyos.validate import is_member
from vyos.config import Config
from vyos import ConfigError
default_config_data = {
- 'address': [],
- 'address_remove': [],
- 'description': '',
+ **interface_default_data,
'deleted': False,
- 'dhcp_client_id': '',
- 'dhcp_hostname': '',
- 'dhcp_vendor_class_id': '',
- 'dhcpv6_prm_only': False,
- 'dhcpv6_temporary': False,
- 'disable': False,
- 'disable_link_detect': 1,
'duplex': 'auto',
'flow_control': 'on',
'hw_id': '',
'ip_arp_cache_tmo': 30,
- 'ip_disable_arp_filter': 1,
- 'ip_enable_arp_accept': 0,
- 'ip_enable_arp_announce': 0,
- 'ip_enable_arp_ignore': 0,
- 'ip_proxy_arp': 0,
'ip_proxy_arp_pvlan': 0,
- 'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
- 'ipv6_forwarding': 1,
- 'ipv6_dup_addr_detect': 1,
+ 'is_bond_member': False,
'intf': '',
- 'mac': '',
- 'mtu': 1500,
'offload_gro': 'off',
'offload_gso': 'off',
'offload_sg': 'off',
'offload_tso': 'off',
'offload_ufo': 'off',
'speed': 'auto',
- 'vif_s': [],
+ 'vif_s': {},
'vif_s_remove': [],
- 'vif': [],
+ 'vif': {},
'vif_remove': [],
'vrf': ''
}
-def get_config():
- eth = deepcopy(default_config_data)
- conf = Config()
+def get_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 +73,7 @@ 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)
# disable ethernet flow control (pause frames)
if conf.exists('disable-flow-control'):
@@ -135,10 +83,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,53 +91,12 @@ 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'))
+ # check if we are a member of any bond
+ eth['is_bond_member'] = is_member(conf, eth['intf'], 'bonding')
# GRO (generic receive offload)
if conf.exists('offload-options generic-receive'):
@@ -219,37 +122,13 @@ 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')
+ # remove default IPv6 link-local address if member of a bond
+ if eth['is_bond_member'] and 'fe80::/64' in eth['ipv6_eui64_prefix']:
+ eth['ipv6_eui64_prefix'].remove('fe80::/64')
+ eth['ipv6_eui64_prefix_remove'].append('fe80::/64')
- # 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
@@ -272,18 +151,24 @@ def verify(eth):
if eth['dhcpv6_prm_only'] and eth['dhcpv6_temporary']:
raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
- vrf_name = eth['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ memberof = eth['is_bridge_member'] if eth['is_bridge_member'] else eth['is_bond_member']
- conf = Config()
- # some options can not be changed when interface is enslaved to a bond
- for bond in conf.list_nodes('interfaces bonding'):
- if conf.exists('interfaces bonding ' + bond + ' member interface'):
- bond_member = conf.return_values('interfaces bonding ' + bond + ' member interface')
- if eth['intf'] in bond_member:
- if eth['address']:
- raise ConfigError(f"Can not assign address to interface {eth['intf']} which is a member of {bond}")
+ if ( memberof
+ and ( eth['address']
+ or eth['ipv6_eui64_prefix']
+ or eth['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{eth["intf"]}" '
+ f'as it is a member of "{memberof}"!'))
+
+ if eth['vrf']:
+ if eth['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{eth["vrf"]}" does not exist')
+
+ if memberof:
+ raise ConfigError((
+ f'Interface "{eth["intf"]}" cannot be member of VRF "{eth["vrf"]}" '
+ f'and "{memberof}" at the same time!'))
# use common function to verify VLAN configuration
verify_vlan_config(eth)
@@ -316,6 +201,9 @@ def apply(eth):
if eth['dhcpv6_temporary']:
e.dhcp.v6.options['dhcpv6_temporary'] = True
+ if eth['dhcpv6_pd']:
+ e.dhcp.v6.options['dhcpv6_pd'] = e['dhcpv6_pd']
+
# ignore link state changes
e.set_link_detect(eth['disable_link_detect'])
# disable ethernet flow control (pause frames)
@@ -334,15 +222,19 @@ def apply(eth):
e.set_proxy_arp(eth['ip_proxy_arp'])
# Enable private VLAN proxy ARP on this interface
e.set_proxy_arp_pvlan(eth['ip_proxy_arp_pvlan'])
+ # IPv6 accept RA
+ e.set_ipv6_accept_ra(eth['ipv6_accept_ra'])
# 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'])
@@ -385,47 +281,17 @@ def apply(eth):
for addr in eth['address']:
e.add_addr(addr)
- # assign/remove VRF
- e.set_vrf(eth['vrf'])
-
- # remove no longer required service VLAN interfaces (vif-s)
- for vif_s in eth['vif_s_remove']:
- e.del_vlan(vif_s)
-
- # create service VLAN interfaces (vif-s)
- for vif_s in eth['vif_s']:
- s_vlan = e.add_vlan(vif_s['id'], ethertype=vif_s['ethertype'])
- apply_vlan_config(s_vlan, vif_s)
-
- # remove no longer required client VLAN interfaces (vif-c)
- # on lower service VLAN interface
- for vif_c in vif_s['vif_c_remove']:
- s_vlan.del_vlan(vif_c)
-
- # create client VLAN interfaces (vif-c)
- # on lower service VLAN interface
- for vif_c in vif_s['vif_c']:
- c_vlan = s_vlan.add_vlan(vif_c['id'])
- apply_vlan_config(c_vlan, vif_c)
-
- # remove no longer required VLAN interfaces (vif)
- for vif in eth['vif_remove']:
- e.del_vlan(vif)
-
- # create VLAN interfaces (vif)
- for vif in eth['vif']:
- # QoS priority mapping can only be set during interface creation
- # so we delete the interface first if required.
- if vif['egress_qos_changed'] or vif['ingress_qos_changed']:
- try:
- # on system bootup the above condition is true but the interface
- # does not exists, which throws an exception, but that's legal
- e.del_vlan(vif['id'])
- except:
- pass
-
- vlan = e.add_vlan(vif['id'], ingress_qos=vif['ingress_qos'], egress_qos=vif['egress_qos'])
- apply_vlan_config(vlan, vif)
+ # assign/remove VRF (ONLY when not a member of a bridge or bond,
+ # otherwise 'nomaster' removes it from it)
+ if not ( eth['is_bridge_member'] or eth['is_bond_member'] ):
+ e.set_vrf(eth['vrf'])
+
+ # re-add ourselves to any bridge we might have fallen out of
+ if eth['is_bridge_member']:
+ e.add_to_bridge(eth['is_bridge_member'])
+
+ # apply all vlans to interface
+ apply_all_vlans(e, eth)
return None
diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py
index 708a64474..e4109a221 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.validate import is_bridge_member
+from vyos.validate import is_member
from vyos import ConfigError
default_config_data = {
@@ -49,11 +49,12 @@ def get_config():
geneve['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ # check if interface is member if a bridge
+ geneve['is_bridge_member'] = is_member(conf, geneve['intf'], 'bridge')
+
# 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
@@ -97,12 +98,17 @@ def get_config():
def verify(geneve):
if geneve['deleted']:
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}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{geneve["intf"]}" as it is a '
+ f'member of bridge "{geneve["is_bridge_member"]}"!'))
return None
+ if geneve['is_bridge_member'] and geneve['address']:
+ raise ConfigError((
+ f'Cannot assign address to interface "{geneve["intf"]}" '
+ f'as it is a member of bridge "{geneve["is_bridge_member"]}"!'))
+
if not geneve['remote']:
raise ConfigError('GENEVE remote must be configured')
@@ -158,6 +164,10 @@ def apply(geneve):
if not geneve['disable']:
g.set_admin_state('up')
+ # re-add ourselves to any bridge we might have fallen out of
+ if geneve['is_bridge_member']:
+ g.add_to_bridge(geneve['is_bridge_member'])
+
return None
diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py
index 8312d6f37..cdfc6ea84 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
@@ -24,7 +24,7 @@ from vyos.config import Config
from vyos.ifconfig import L2TPv3If, Interface
from vyos import ConfigError
from vyos.util import call
-from vyos.validate import is_bridge_member
+from vyos.validate import is_member, is_addr_assigned
default_config_data = {
'address': [],
@@ -35,8 +35,9 @@ default_config_data = {
'local_address': '',
'local_port': 5000,
'intf': '',
+ 'ipv6_accept_ra': 1,
'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
+ 'ipv6_eui64_prefix': [],
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
'is_bridge_member': False,
@@ -66,12 +67,13 @@ def get_config():
l2tpv3['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ # check if interface is member of a bridge
+ l2tpv3['is_bridge_member'] = is_member(conf, l2tpv3['intf'], 'bridge')
+
# Check if interface has been removed
if not conf.exists('interfaces l2tpv3 ' + l2tpv3['intf']):
l2tpv3['deleted'] = True
interface = l2tpv3['intf']
- # check if interface is member if a bridge
- l2tpv3['is_bridge_member'] = is_bridge_member(conf, interface)
# to delete the l2tpv3 interface we need the current tunnel_id and session_id
if conf.exists_effective(f'interfaces l2tpv3 {interface} tunnel-id'):
@@ -113,9 +115,15 @@ 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') or
+ l2tpv3['is_bridge_member'] ):
+ # 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'):
@@ -125,6 +133,11 @@ def get_config():
if conf.exists('ipv6 dup-addr-detect-transmits'):
l2tpv3['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+ # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
+ # accept_ra must be 2
+ if l2tpv3['ipv6_autoconf'] or 'dhcpv6' in l2tpv3['address']:
+ l2tpv3['ipv6_accept_ra'] = 2
+
# Maximum Transmission Unit (MTU)
if conf.exists('mtu'):
l2tpv3['mtu'] = int(conf.return_value('mtu'))
@@ -161,15 +174,18 @@ def verify(l2tpv3):
if l2tpv3['deleted']:
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}"!')
+ raise ConfigError((
+ f'Interface "{l2tpv3["intf"]}" cannot be deleted as it is a '
+ f'member of bridge "{l2tpv3["is_bridge_member"]}"!'))
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}')
@@ -185,6 +201,14 @@ def verify(l2tpv3):
if not l2tpv3['peer_session_id']:
raise ConfigError(f'Must configure the l2tpv3 peer-session-id for {interface}')
+ if ( l2tpv3['is_bridge_member']
+ and ( l2tpv3['address']
+ or l2tpv3['ipv6_eui64_prefix']
+ or l2tpv3['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{l2tpv3["intf"]}" '
+ f'as it is a member of bridge "{l2tpv3["is_bridge_member"]}"!'))
+
return None
@@ -223,10 +247,10 @@ def apply(l2tpv3):
l.set_alias(l2tpv3['description'])
# Maximum Transfer Unit (MTU)
l.set_mtu(l2tpv3['mtu'])
+ # IPv6 accept RA
+ l.set_ipv6_accept_ra(l2tpv3['ipv6_accept_ra'])
# 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
@@ -238,12 +262,20 @@ 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
if not l2tpv3['disable']:
l.set_admin_state('up')
+ # re-add ourselves to any bridge we might have fallen out of
+ if l2tpv3['is_bridge_member']:
+ l.add_to_bridge(l2tpv3['is_bridge_member'])
+
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 708ac8f91..ea8e1a7c4 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -25,10 +25,11 @@ 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.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.validate import is_addr_assigned, is_member, is_ipv4
from vyos import ConfigError
user = 'openvpn'
@@ -40,7 +41,6 @@ default_config_data = {
'auth_pass': '',
'auth_user_pass_file': '',
'auth': False,
- 'bridge_member': [],
'compress_lzo': False,
'deleted': False,
'description': '',
@@ -49,8 +49,10 @@ default_config_data = {
'encryption': '',
'hash': '',
'intf': '',
+ 'ipv6_accept_ra': 1,
'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
+ 'ipv6_eui64_prefix': [],
+ 'ipv6_eui64_prefix_remove': [],
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
'ipv6_local_address': [],
@@ -197,21 +199,16 @@ def get_config():
openvpn['intf'] = os.environ['VYOS_TAGNODE_VALUE']
openvpn['auth_user_pass_file'] = f"/run/openvpn/{openvpn['intf']}.pw"
+ # check if interface is member of a bridge
+ openvpn['is_bridge_member'] = is_member(conf, openvpn['intf'], 'bridge')
+
# 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
- for bridge in conf.list_nodes('interfaces bridge'):
- for intf in conf.list_nodes('interfaces bridge {} member interface'.format(bridge)):
- if intf == openvpn['intf']:
- openvpn['bridge_member'].append(intf)
-
# bridged server should not have a pool by default (but can be specified manually)
- if openvpn['bridge_member']:
+ if openvpn['is_bridge_member']:
openvpn['server_pool'] = False
openvpn['server_ipv6_pool'] = False
@@ -314,9 +311,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'):
@@ -326,6 +335,11 @@ def get_config():
if conf.exists('ipv6 dup-addr-detect-transmits'):
openvpn['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+ # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
+ # accept_ra must be 2
+ if openvpn['ipv6_autoconf'] or 'dhcpv6' in openvpn['address']:
+ openvpn['ipv6_accept_ra'] = 2
+
# OpenVPN operation mode
if conf.exists('mode'):
openvpn['mode'] = conf.return_value('mode')
@@ -583,7 +597,7 @@ def get_config():
default_server = getDefaultServer(server_network_v4, openvpn['server_topology'], openvpn['type'])
if default_server:
# server-bridge doesn't require a pool so don't set defaults for it
- if openvpn['server_pool'] and not openvpn['bridge_member']:
+ if openvpn['server_pool'] and not openvpn['is_bridge_member']:
if not openvpn['server_pool_start']:
openvpn['server_pool_start'] = default_server['pool_start']
@@ -621,22 +635,15 @@ def get_config():
def verify(openvpn):
if openvpn['deleted']:
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}"!')
-
+ raise ConfigError((
+ f'Cannot delete interface "{openvpn["intf"]}" as it is a '
+ f'member of bridge "{openvpn["is_bridge_menber"]}"!'))
return None
if not openvpn['mode']:
raise ConfigError('Must specify OpenVPN operation mode')
- # Checks which need to be performed on interface rmeoval
- if openvpn['deleted']:
- # OpenVPN interface can not be deleted if it's still member of a bridge
- if openvpn['bridge_member']:
- raise ConfigError('Can not delete {} as it is a member interface of bridge {}!'.format(openvpn['intf'], bridge))
-
# Check if we have disabled ncp and at the same time specified ncp-ciphers
if openvpn['disable_ncp'] and openvpn['ncp_ciphers']:
raise ConfigError('Cannot specify both "encryption disable-ncp" and "encryption ncp-ciphers"')
@@ -666,9 +673,9 @@ def verify(openvpn):
if openvpn['ncp_ciphers']:
raise ConfigError('encryption ncp-ciphers cannot be specified in site-to-site mode, only server or client')
- if openvpn['mode'] == 'site-to-site' and not openvpn['bridge_member']:
+ if openvpn['mode'] == 'site-to-site' and not openvpn['is_bridge_member']:
if not (openvpn['local_address'] or openvpn['ipv6_local_address']):
- raise ConfigError('Must specify "local-address" or "bridge member interface"')
+ raise ConfigError('Must specify "local-address" or add interface to bridge')
if len(openvpn['local_address']) > 1 or len(openvpn['ipv6_local_address']) > 1:
raise ConfigError('Cannot specify more than 1 IPv4 and 1 IPv6 "local-address"')
@@ -747,8 +754,8 @@ def verify(openvpn):
raise ConfigError(f'Client "{client["name"]}" IP {client["ip"][0]} not in server subnet {subnet}')
else:
- if not openvpn['bridge_member']:
- raise ConfigError('Must specify "server subnet" or "bridge member interface" in server mode')
+ if not openvpn['is_bridge_member']:
+ raise ConfigError('Must specify "server subnet" or add interface to bridge in server mode')
if openvpn['server_pool']:
if not (openvpn['server_pool_start'] and openvpn['server_pool_stop']):
@@ -1041,15 +1048,28 @@ def apply(openvpn):
o = VTunIf(interface)
# update interface description used e.g. within SNMP
o.set_alias(openvpn['description'])
+ # IPv6 accept RA
+ o.set_ipv6_accept_ra(openvpn['ipv6_accept_ra'])
# 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..e46d52d19 100755
--- a/src/conf_mode/interfaces-pppoe.py
+++ b/src/conf_mode/interfaces-pppoe.py
@@ -36,6 +36,7 @@ default_config_data = {
'deleted': False,
'description': '\0',
'disable': False,
+ 'dhcpv6_pd': [],
'intf': '',
'idle_timeout': '',
'ipv6_autoconf': False,
@@ -138,6 +139,27 @@ def get_config():
if conf.exists('vrf'):
pppoe['vrf'] = conf.return_value(['vrf'])
+ if conf.exists(['dhcpv6-options', 'delegate']):
+ for interface in conf.list_nodes(['dhcpv6-options', 'delegate']):
+ pd = {
+ 'ifname': interface,
+ 'sla_id': '',
+ 'sla_len': '',
+ 'if_id': ''
+ }
+ conf.set_level(base_path + [pppoe['intf'], 'dhcpv6-options', 'delegate', interface])
+
+ if conf.exists(['sla-id']):
+ pd['sla_id'] = conf.return_value(['sla-id'])
+
+ if conf.exists(['sla-len']):
+ pd['sla_len'] = conf.return_value(['sla-len'])
+
+ if conf.exists(['interface-id']):
+ pd['if_id'] = conf.return_value(['interface-id'])
+
+ pppoe['dhcpv6_pd'].append(pd)
+
return pppoe
def verify(pppoe):
@@ -169,15 +191,15 @@ def generate(pppoe):
script_pppoe_ip_up = f'/etc/ppp/ip-up.d/1000-vyos-pppoe-{intf}'
script_pppoe_ip_down = f'/etc/ppp/ip-down.d/1000-vyos-pppoe-{intf}'
script_pppoe_ipv6_up = f'/etc/ppp/ipv6-up.d/1000-vyos-pppoe-{intf}'
+ config_wide_dhcp6c = f'/run/dhcp6c/dhcp6c.{intf}.conf'
config_files = [config_pppoe, script_pppoe_pre_up, script_pppoe_ip_up,
- script_pppoe_ip_down, script_pppoe_ipv6_up]
+ script_pppoe_ip_down, script_pppoe_ipv6_up, config_wide_dhcp6c]
+
+ # Shutdown DHCPv6 prefix delegation client
+ if not pppoe['dhcpv6_pd']:
+ cmd(f'systemctl stop dhcp6c@{intf}.service')
- # 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 +211,29 @@ 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)
+ pppoe, trim_blocks=True, permission=0o755)
- # 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)
+ if len(pppoe['dhcpv6_pd']) > 0:
+ # ipv6.tmpl relies on ifname - this should be made consitent in the
+ # future better then double key-ing the same value
+ pppoe['ifname'] = intf
+ render(config_wide_dhcp6c, 'dhcp-client/ipv6.tmpl', pppoe, trim_blocks=True)
return None
diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py
index d5f308ed3..3e036a753 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
@@ -21,154 +21,55 @@ from sys import exit
from netifaces import interfaces
from vyos.config import Config
-from vyos.configdict import list_diff, vlan_to_dict
-from vyos.ifconfig import MACVLANIf
-from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
-from vyos.validate import is_bridge_member
+from vyos.configdict import list_diff, intf_to_dict, add_to_dict, interface_default_data
+from vyos.ifconfig import MACVLANIf, Section
+from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config
from vyos import ConfigError
default_config_data = {
- 'address': [],
- 'address_remove': [],
- 'description': '',
+ **interface_default_data,
'deleted': False,
- 'dhcp_client_id': '',
- 'dhcp_hostname': '',
- 'dhcp_vendor_class_id': '',
- 'dhcpv6_prm_only': False,
- '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,
- 'ip_enable_arp_announce': 0,
- 'ip_enable_arp_ignore': 0,
- 'ip_proxy_arp': 0,
'ip_proxy_arp_pvlan': 0,
- 'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
- 'ipv6_forwarding': 1,
- 'ipv6_dup_addr_detect': 1,
- 'is_bridge_member': False,
'source_interface': '',
'source_interface_changed': False,
- 'mac': '',
'mode': 'private',
- 'vif_s': [],
+ 'vif_s': {},
'vif_s_remove': [],
- 'vif': [],
+ 'vif': {},
'vif_remove': [],
'vrf': ''
}
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, peth['intf'])
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)
# 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'])
@@ -176,67 +77,49 @@ 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']:
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}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{peth["intf"]}" as it is a '
+ f'member of bridge "{peth["is_bridge_member"]}"!'))
return None
if not peth['source_interface']:
- raise ConfigError('Link device must be set for virtual ethernet {}'.format(peth['intf']))
+ raise ConfigError((
+ f'Link device must be set for pseudo-ethernet "{peth["intf"]}"'))
if not peth['source_interface'] in interfaces():
- raise ConfigError('Pseudo-ethernet source interface does not exist')
+ raise ConfigError((
+ f'Pseudo-ethernet "{peth["intf"]}" link device does not exist'))
+
+ if ( peth['is_bridge_member']
+ and ( peth['address']
+ or peth['ipv6_eui64_prefix']
+ or peth['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{peth["intf"]}" '
+ f'as it is a member of bridge "{peth["is_bridge_member"]}"!'))
- vrf_name = peth['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ if peth['vrf']:
+ if peth['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{peth["vrf"]}" does not exist')
+
+ if peth['is_bridge_member']:
+ raise ConfigError((
+ f'Interface "{peth["intf"]}" cannot be member of VRF '
+ f'"{peth["vrf"]}" and bridge {peth["is_bridge_member"]} '
+ f'at the same time!'))
# use common function to verify VLAN configuration
verify_vlan_config(peth)
@@ -288,6 +171,9 @@ def apply(peth):
if peth['dhcpv6_temporary']:
p.dhcp.v6.options['dhcpv6_temporary'] = True
+ if peth['dhcpv6_pd']:
+ p.dhcp.v6.options['dhcpv6_pd'] = peth['dhcpv6_pd']
+
# ignore link state changes
p.set_link_detect(peth['disable_link_detect'])
# configure ARP cache timeout in milliseconds
@@ -304,22 +190,32 @@ def apply(peth):
p.set_proxy_arp(peth['ip_proxy_arp'])
# Enable private VLAN proxy ARP on this interface
p.set_proxy_arp_pvlan(peth['ip_proxy_arp_pvlan'])
+ # IPv6 accept RA
+ p.set_ipv6_accept_ra(peth['ipv6_accept_ra'])
# IPv6 address autoconfiguration
p.set_ipv6_autoconf(peth['ipv6_autoconf'])
- # IPv6 EUI-based address
- p.set_ipv6_eui64_address(peth['ipv6_eui64_prefix'])
# IPv6 forwarding
p.set_ipv6_forwarding(peth['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
p.set_ipv6_dad_messages(peth['ipv6_dup_addr_detect'])
- # assign/remove VRF
- p.set_vrf(peth['vrf'])
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not peth['is_bridge_member']:
+ 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'])
@@ -337,34 +233,12 @@ def apply(peth):
for addr in peth['address']:
p.add_addr(addr)
- # remove no longer required service VLAN interfaces (vif-s)
- for vif_s in peth['vif_s_remove']:
- p.del_vlan(vif_s)
-
- # create service VLAN interfaces (vif-s)
- for vif_s in peth['vif_s']:
- s_vlan = p.add_vlan(vif_s['id'], ethertype=vif_s['ethertype'])
- apply_vlan_config(s_vlan, vif_s)
-
- # remove no longer required client VLAN interfaces (vif-c)
- # on lower service VLAN interface
- for vif_c in vif_s['vif_c_remove']:
- s_vlan.del_vlan(vif_c)
-
- # create client VLAN interfaces (vif-c)
- # on lower service VLAN interface
- for vif_c in vif_s['vif_c']:
- c_vlan = s_vlan.add_vlan(vif_c['id'])
- apply_vlan_config(c_vlan, vif_c)
-
- # remove no longer required VLAN interfaces (vif)
- for vif in peth['vif_remove']:
- p.del_vlan(vif)
-
- # create VLAN interfaces (vif)
- for vif in peth['vif']:
- vlan = p.add_vlan(vif['id'])
- apply_vlan_config(vlan, vif)
+ # re-add ourselves to any bridge we might have fallen out of
+ if peth['is_bridge_member']:
+ p.add_to_bridge(peth['is_bridge_member'])
+
+ # apply all vlans to interface
+ apply_all_vlans(p, peth)
return None
diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
index 9c0c42414..8e9bb069e 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, is_bridge_member
+from vyos.validate import is_ipv4, is_ipv6, is_member
from vyos import ConfigError
from vyos.dicts import FixedDict
@@ -222,7 +222,7 @@ class ConfigurationState(Config):
remove all the values which were not changed from the default
"""
for option in options:
- if self.exists(option) and self_return_value(option) != self.default[option]:
+ if self.exists(option) and self.self_return_value(option) != self.default[option]:
continue
del self.options[option]
@@ -251,6 +251,7 @@ default_config_data = {
'ip': False,
'ipv6': False,
'nhrp': [],
+ 'ipv6_accept_ra': 1,
'ipv6_autoconf': 0,
'ipv6_forwarding': 1,
'ipv6_dad_transmits': 1,
@@ -401,6 +402,11 @@ def get_config():
eff_addr = conf.return_effective_values('address')
options['addresses-del'] = list_diff(eff_addr, options['addresses-add'])
+ # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
+ # accept_ra must be 2
+ if options['ipv6_autoconf'] or 'dhcpv6' in options['addresses-add']:
+ options['ipv6_accept_ra'] = 2
+
# allmulticast fate is linked to multicast
options['allmulticast'] = options['multicast']
@@ -410,7 +416,7 @@ def get_config():
options['tunnel'] = {}
# check for bridges
- options['bridge'] = is_bridge_member(conf, ifname)
+ options['bridge'] = is_member(conf, ifname, 'bridge')
options['interfaces'] = interfaces()
for name in ct:
@@ -436,11 +442,14 @@ 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')
+ raise ConfigError((
+ f'Cannot 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}"!')
+ if options['bridge']:
+ raise ConfigError((
+ f'Cannot delete interface "{options["ifname"]}" as it is a '
+ f'member of bridge "{options["bridge"]}"!'))
# done, bail out early
return None
@@ -461,7 +470,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',])
@@ -525,15 +534,28 @@ def verify(conf):
print(f'Should not use IPv6 addresses on tunnel {iftype} {ifname}')
# vrf check
-
- vrf = options['vrf']
- if vrf and vrf not in options['interfaces']:
- raise ConfigError(f'VRF "{vrf}" does not exist')
+ if options['vrf']:
+ if options['vrf'] not in options['interfaces']:
+ raise ConfigError(f'VRF "{options["vrf"]}" does not exist')
+
+ if options['bridge']:
+ raise ConfigError((
+ f'Interface "{options["ifname"]}" cannot be member of VRF '
+ f'"{options["vrf"]}" and bridge {options["bridge"]} '
+ f'at the same time!'))
+
+ # bridge and address check
+ if ( options['bridge']
+ and ( options['addresses-add']
+ or options['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{options["name"]}" '
+ f'as it is a member of bridge "{options["bridge"]}"!'))
# source-interface check
if tun_dev and tun_dev not in options['interfaces']:
- raise ConfigError(f'device "{dev}" does not exist')
+ raise ConfigError(f'device "{tun_dev}" does not exist')
# tunnel encapsulation check
@@ -620,12 +642,17 @@ def apply(conf):
# set other interface properties
for option in ('alias', 'mtu', 'link_detect', 'multicast', 'allmulticast',
- 'vrf', 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits'):
+ 'ipv6_accept_ra', 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits'):
if not options[option]:
# should never happen but better safe
continue
tunnel.set_interface(option, options[option])
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not options['bridge']:
+ tunnel.set_vrf(options['vrf'])
+
# Configure interface address(es)
for addr in options['addresses-del']:
tunnel.del_addr(addr)
diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index d238ddb57..84fe3dfc8 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.validate import is_bridge_member
+from vyos.validate import is_member
from vyos import ConfigError
default_config_data = {
@@ -38,8 +38,9 @@ default_config_data = {
'ip_enable_arp_announce': 0,
'ip_enable_arp_ignore': 0,
'ip_proxy_arp': 0,
+ 'ipv6_accept_ra': 1,
'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
+ 'ipv6_eui64_prefix': [],
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
'is_bridge_member': False,
@@ -62,11 +63,12 @@ def get_config():
vxlan['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ # check if interface is member if a bridge
+ vxlan['is_bridge_member'] = is_member(conf, vxlan['intf'], 'bridge')
+
# 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
@@ -116,9 +118,15 @@ 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')
+ or vxlan['is_bridge_member'] ):
+ # 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'):
@@ -128,6 +136,11 @@ def get_config():
if conf.exists('ipv6 dup-addr-detect-transmits'):
vxlan['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+ # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
+ # accept_ra must be 2
+ if vxlan['ipv6_autoconf'] or 'dhcpv6' in vxlan['address']:
+ vxlan['ipv6_accept_ra'] = 2
+
# VXLAN source address
if conf.exists('source-address'):
vxlan['source_address'] = conf.return_value('source-address')
@@ -158,9 +171,9 @@ def get_config():
def verify(vxlan):
if vxlan['deleted']:
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}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{vxlan["intf"]}" as it is a '
+ f'member of bridge "{vxlan["is_bridge_member"]}"!'))
return None
@@ -188,6 +201,14 @@ def verify(vxlan):
raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \
'MTU is to small ({})'.format(underlay_mtu))
+ if ( vxlan['is_bridge_member']
+ and ( vxlan['address']
+ or vxlan['ipv6_eui64_prefix']
+ or vxlan['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{vxlan["intf"]}" '
+ f'as it is a member of bridge "{vxlan["is_bridge_member"]}"!'))
+
return None
@@ -236,10 +257,10 @@ def apply(vxlan):
v.set_arp_ignore(vxlan['ip_enable_arp_ignore'])
# Enable proxy-arp on this interface
v.set_proxy_arp(vxlan['ip_proxy_arp'])
+ # IPv6 accept RA
+ v.set_ipv6_accept_ra(vxlan['ipv6_accept_ra'])
# 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
@@ -251,12 +272,20 @@ def apply(vxlan):
for addr in vxlan['address']:
v.add_addr(addr)
+ # 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']:
v.set_admin_state('up')
+ # re-add ourselves to any bridge we might have fallen out of
+ if vxlan['is_bridge_member']:
+ v.add_to_bridge(vxlan['is_bridge_member'])
+
return None
diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py
index 423700370..97dcf626b 100755
--- a/src/conf_mode/interfaces-wireguard.py
+++ b/src/conf_mode/interfaces-wireguard.py
@@ -25,7 +25,7 @@ from vyos.config import Config
from vyos.configdict import list_diff
from vyos.ifconfig import WireGuardIf
from vyos.util import chown, chmod_750, call
-from vyos.validate import is_bridge_member
+from vyos.validate import is_member, is_ipv6
from vyos import ConfigError
kdir = r'/config/auth/wireguard'
@@ -35,11 +35,11 @@ default_config_data = {
'address': [],
'address_remove': [],
'description': '',
- 'lport': None,
+ 'listen_port': '',
'deleted': False,
'disable': False,
+ 'fwmark': 0,
'is_bridge_member': False,
- 'fwmark': 0x00,
'mtu': 1420,
'peer': [],
'peer_remove': [], # stores public keys of peers to remove
@@ -78,11 +78,12 @@ def get_config():
wg = deepcopy(default_config_data)
wg['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ # check if interface is member if a bridge
+ wg['is_bridge_member'] = is_member(conf, wg['intf'], 'bridge')
+
# 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']])
@@ -106,7 +107,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,38 +190,52 @@ def get_config():
def verify(wg):
- interface = wg['intf']
-
if wg['deleted']:
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}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{wg["intf"]}" as it is a member '
+ f'of bridge "{wg["is_bridge_member"]}"!'))
return None
- vrf_name = wg['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ if wg['is_bridge_member'] and wg['address']:
+ raise ConfigError((
+ f'Cannot assign address to interface "{wg["intf"]}" '
+ f'as it is a member of bridge "{wg["is_bridge_member"]}"!'))
+
+ if wg['vrf']:
+ if wg['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{wg["vrf"]}" does not exist')
+
+ if wg['is_bridge_member']:
+ raise ConfigError((
+ f'Interface "{wg["intf"]}" cannot be member of VRF '
+ f'"{wg["vrf"]}" and bridge {wg["is_bridge_member"]} '
+ f'at the same time!'))
if not os.path.exists(wg['pk']):
raise ConfigError('No keys found, generate them by executing:\n' \
'"run generate wireguard [keypair|named-keypairs]"')
if not wg['address']:
- raise ConfigError(f'IP address required for interface "{interface}"!')
+ raise ConfigError(f'IP address required for interface "{wg["intf"]}"!')
if not wg['peer']:
- raise ConfigError(f'Peer required for interface "{interface}"!')
+ raise ConfigError(f'Peer required for interface "{wg["intf"]}"!')
# run checks on individual configured WireGuard peer
for peer in wg['peer']:
- peer_name = peer['name']
if not peer['allowed-ips']:
- raise ConfigError(f'Peer allowed-ips required for peer "{peer_name}"!')
+ raise ConfigError(f'Peer allowed-ips required for peer "{peer["name"]}"!')
if not peer['pubkey']:
- raise ConfigError(f'Peer public-key required for peer "{peer_name}"!')
+ 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):
@@ -246,8 +261,10 @@ def apply(wg):
# update interface description used e.g. within SNMP
w.set_alias(wg['description'])
- # assign/remove VRF
- w.set_vrf(wg['vrf'])
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not wg['is_bridge_member']:
+ w.set_vrf(wg['vrf'])
# remove peers
for pub_key in wg['peer_remove']:
@@ -263,16 +280,18 @@ 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']
# endpoint
if peer['address'] and peer['port']:
- w.config['endpoint'] = '{}:{}'.format(
- peer['address'], peer['port'])
+ if is_ipv6(peer['address']):
+ w.config['endpoint'] = '[{}]:{}'.format(peer['address'], peer['port'])
+ else:
+ w.config['endpoint'] = '{}:{}'.format(peer['address'], peer['port'])
# persistent-keepalive
if peer['persistent_keepalive']:
diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py
index 42842f9bd..f13408fa2 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
@@ -24,17 +24,16 @@ from netifaces import interfaces
from netaddr import EUI, mac_unix_expanded
from vyos.config import Config
-from vyos.configdict import list_diff, vlan_to_dict
-from vyos.ifconfig import WiFiIf
-from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
+from vyos.configdict import list_diff, intf_to_dict, add_to_dict, interface_default_data
+from vyos.ifconfig import WiFiIf, Section
+from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config
from vyos.template import render
from vyos.util import chown, call
-from vyos.validate import is_bridge_member
+from vyos.validate import is_member
from vyos import ConfigError
default_config_data = {
- 'address': [],
- 'address_remove': [],
+ **interface_default_data,
'cap_ht' : False,
'cap_ht_40mhz_incapable' : False,
'cap_ht_powersave' : False,
@@ -69,30 +68,13 @@ default_config_data = {
'cap_vht_vht_cf' : False,
'channel': '',
'country_code': '',
- 'description': '',
'deleted': False,
- 'dhcp_client_id': '',
- 'dhcp_hostname': '',
- 'dhcp_vendor_class_id': '',
- 'dhcpv6_prm_only': False,
- 'dhcpv6_temporary': False,
- 'disable': False,
'disable_broadcast_ssid' : False,
'disable_link_detect' : 1,
'expunge_failing_stations' : False,
'hw_id' : '',
'intf': '',
'isolate_stations' : False,
- 'ip_disable_arp_filter': 1,
- 'ip_enable_arp_accept': 0,
- 'ip_enable_arp_announce': 0,
- 'ip_enable_arp_ignore': 0,
- 'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
- 'ipv6_forwarding': 1,
- 'ipv6_dup_addr_detect': 1,
- 'is_bridge_member': False,
- 'mac' : '',
'max_stations' : '',
'mgmt_frame_protection' : 'disabled',
'mode' : 'g',
@@ -107,9 +89,10 @@ default_config_data = {
'sec_wpa_radius' : [],
'ssid' : '',
'op_mode' : 'monitor',
- 'vif': [],
+ 'vif': {},
'vif_remove': [],
- 'vrf': ''
+ 'vif_s': {},
+ 'vif_s_remove': []
}
def get_conf_file(conf_type, intf):
@@ -124,21 +107,21 @@ def get_conf_file(conf_type, intf):
return cfg_file
def get_config():
- wifi = 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')
- wifi['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+ conf = Config()
# check if wireless interface has been removed
- cfg_base = 'interfaces wireless ' + wifi['intf']
+ cfg_base = ['interfaces', 'wireless ', ifname]
if not conf.exists(cfg_base):
+ wifi = deepcopy(default_config_data)
+ wifi['intf'] = ifname
wifi['deleted'] = True
- # check if interface is member if a bridge
- wifi['is_bridge_member'] = is_bridge_member(conf, wifi['intf'])
+ # we need to know if we're a bridge member so we can refuse deletion
+ wifi['is_bridge_member'] = is_member(conf, wifi['intf'], 'bridge')
# 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
@@ -147,14 +130,8 @@ def get_config():
# set new configuration level
conf.set_level(cfg_base)
- # retrieve configured interface addresses
- if conf.exists('address'):
- wifi['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')
- wifi['address_remove'] = list_diff(eff_addr, wifi['address'])
+ # get common interface settings
+ wifi, disabled = intf_to_dict(conf, default_config_data)
# 40MHz intolerance, use 20MHz only
if conf.exists('capabilities ht 40mhz-incapable'):
@@ -308,38 +285,10 @@ def get_config():
if conf.exists('channel'):
wifi['channel'] = conf.return_value('channel')
- # retrieve interface description
- if conf.exists('description'):
- wifi['description'] = conf.return_value('description')
-
- # get DHCP client identifier
- if conf.exists('dhcp-options client-id'):
- wifi['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'):
- wifi['dhcp_hostname'] = conf.return_value('dhcp-options host-name')
-
- # DHCP client vendor identifier
- if conf.exists('dhcp-options vendor-class-id'):
- wifi['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'):
- wifi['dhcpv6_prm_only'] = conf.return_value('dhcpv6-options parameters-only')
-
- # DHCPv6 temporary IPv6 address
- if conf.exists('dhcpv6-options temporary'):
- wifi['dhcpv6_temporary'] = conf.return_value('dhcpv6-options temporary')
-
# Disable broadcast of SSID from access-point
if conf.exists('disable-broadcast-ssid'):
wifi['disable_broadcast_ssid'] = True
- # ignore link state changes on this interface
- if conf.exists('disable-link-detect'):
- wifi['disable_link_detect'] = 2
-
# Disassociate stations based on excessive transmission failures
if conf.exists('expunge-failing-stations'):
wifi['expunge_failing_stations'] = True
@@ -352,46 +301,10 @@ def get_config():
if conf.exists('isolate-stations'):
wifi['isolate_stations'] = True
- # ARP filter configuration
- if conf.exists('ip disable-arp-filter'):
- wifi['ip_disable_arp_filter'] = 0
-
- # ARP enable accept
- if conf.exists('ip enable-arp-accept'):
- wifi['ip_enable_arp_accept'] = 1
-
- # ARP enable announce
- if conf.exists('ip enable-arp-announce'):
- wifi['ip_enable_arp_announce'] = 1
-
- # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
- if conf.exists('ipv6 address autoconf'):
- wifi['ipv6_autoconf'] = 1
-
- # Get prefix for IPv6 addressing based on MAC address (EUI-64)
- if conf.exists('ipv6 address eui64'):
- wifi['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
-
- # ARP enable ignore
- if conf.exists('ip enable-arp-ignore'):
- wifi['ip_enable_arp_ignore'] = 1
-
- # Disable IPv6 forwarding on this interface
- if conf.exists('ipv6 disable-forwarding'):
- wifi['ipv6_forwarding'] = 0
-
- # IPv6 Duplicate Address Detection (DAD) tries
- if conf.exists('ipv6 dup-addr-detect-transmits'):
- wifi['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
-
# Wireless physical device
if conf.exists('physical-device'):
wifi['phy'] = conf.return_value('physical-device')
- # Media Access Control (MAC) address
- if conf.exists('mac'):
- wifi['mac'] = conf.return_value('mac')
-
# Maximum number of wireless radio stations
if conf.exists('max-stations'):
wifi['max_stations'] = conf.return_value('max-stations')
@@ -404,10 +317,6 @@ def get_config():
if conf.exists('mode'):
wifi['mode'] = conf.return_value('mode')
- # retrieve VRF instance
- if conf.exists('vrf'):
- wifi['vrf'] = conf.return_value('vrf')
-
# Transmission power reduction in dBm
if conf.exists('reduce-transmit-power'):
wifi['reduce_transmit_power'] = conf.return_value('reduce-transmit-power')
@@ -503,24 +412,6 @@ def get_config():
wifi['op_mode'] = tmp
- # 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')
- wifi['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)
- wifi['vif'].append(vlan_to_dict(conf))
-
- # disable interface
- if conf.exists('disable'):
- wifi['disable'] = True
-
# retrieve configured regulatory domain
conf.set_level('system')
if conf.exists('wifi-regulatory-domain'):
@@ -532,9 +423,9 @@ def get_config():
def verify(wifi):
if wifi['deleted']:
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}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{wifi["intf"]}" as it is a '
+ f'member of bridge "{wifi["is_bridge_member"]}"!'))
return None
@@ -579,9 +470,23 @@ def verify(wifi):
if not radius['key']:
raise ConfigError('Misssing RADIUS shared secret key for server: {}'.format(radius['server']))
- vrf_name = wifi['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ if ( wifi['is_bridge_member']
+ and ( wifi['address']
+ or wifi['ipv6_eui64_prefix']
+ or wifi['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{wifi["intf"]}" '
+ f'as it is a member of bridge "{wifi["is_bridge_member"]}"!'))
+
+ if wifi['vrf']:
+ if wifi['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{wifi["vrf"]}" does not exist')
+
+ if wifi['is_bridge_member']:
+ raise ConfigError((
+ f'Interface "{wifi["intf"]}" cannot be member of VRF '
+ f'"{wifi["vrf"]}" and bridge {wifi["is_bridge_member"]} '
+ f'at the same time!'))
# use common function to verify VLAN configuration
verify_vlan_config(wifi)
@@ -672,8 +577,10 @@ def apply(wifi):
# Finally create the new interface
w = WiFiIf(interface, **conf)
- # assign/remove VRF
- w.set_vrf(wifi['vrf'])
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not wifi['is_bridge_member']:
+ w.set_vrf(wifi['vrf'])
# update interface description used e.g. within SNMP
w.set_alias(wifi['description'])
@@ -693,9 +600,16 @@ def apply(wifi):
if wifi['dhcpv6_temporary']:
w.dhcp.v6.options['dhcpv6_temporary'] = True
+ if wifi['dhcpv6_pd']:
+ w.dhcp.v6.options['dhcpv6_pd'] = wifi['dhcpv6_pd']
+
# 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']:
@@ -703,6 +617,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
@@ -711,10 +629,10 @@ def apply(wifi):
w.set_arp_announce(wifi['ip_enable_arp_announce'])
# configure ARP ignore
w.set_arp_ignore(wifi['ip_enable_arp_ignore'])
+ # IPv6 accept RA
+ w.set_ipv6_accept_ra(wifi['ipv6_accept_ra'])
# 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
@@ -728,24 +646,8 @@ def apply(wifi):
for addr in wifi['address']:
w.add_addr(addr)
- # remove no longer required VLAN interfaces (vif)
- for vif in wifi['vif_remove']:
- w.del_vlan(vif)
-
- # create VLAN interfaces (vif)
- for vif in wifi['vif']:
- # QoS priority mapping can only be set during interface creation
- # so we delete the interface first if required.
- if vif['egress_qos_changed'] or vif['ingress_qos_changed']:
- try:
- # on system bootup the above condition is true but the interface
- # does not exists, which throws an exception, but that's legal
- w.del_vlan(vif['id'])
- except:
- pass
-
- vlan = w.add_vlan(vif['id'])
- apply_vlan_config(vlan, vif)
+ # apply all vlans to interface
+ apply_all_vlans(w, wifi)
# Enable/Disable interface - interface is always placed in
# administrative down state in WiFiIf class
diff --git a/src/conf_mode/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py
index 163778e22..975e21d9f 100755
--- a/src/conf_mode/interfaces-wirelessmodem.py
+++ b/src/conf_mode/interfaces-wirelessmodem.py
@@ -21,9 +21,10 @@ from copy import deepcopy
from netifaces import interfaces
from vyos.config import Config
+from vyos.ifconfig import BridgeIf, Section
from vyos.template import render
from vyos.util import chown, chmod_755, cmd, call
-from vyos.validate import is_bridge_member
+from vyos.validate import is_member
from vyos import ConfigError
default_config_data = {
@@ -64,11 +65,12 @@ def get_config():
wwan['logfile'] = f"/var/log/vyatta/ppp_{wwan['intf']}.log"
wwan['chat_script'] = f"/etc/ppp/peers/chat.{wwan['intf']}"
+ # check if interface is member if a bridge
+ wwan['is_bridge_member'] = is_member(conf, wwan['intf'], 'bridge')
+
# 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,9 +121,9 @@ def get_config():
def verify(wwan):
if wwan['deleted']:
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}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{wwan["intf"]}" as it is a '
+ f'member of bridge "{wwan["is_bridge_member"]}"!'))
return None
@@ -133,9 +135,20 @@ def verify(wwan):
if not os.path.exists(f"/dev/{wwan['device']}"):
raise ConfigError(f"Device {wwan['device']} does not exist")
- vrf_name = wwan['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF {vrf_name} does not exist')
+ if wwan['is_bridge_member'] and wwan['address']:
+ raise ConfigError((
+ f'Cannot assign address to interface "{wwan["intf"]}" '
+ f'as it is a member of bridge "{wwan["is_bridge_member"]}"!'))
+
+ if wwan['vrf']:
+ if wwan['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{wwan["vrf"]}" does not exist')
+
+ if wwan['is_bridge_member']:
+ raise ConfigError((
+ f'Interface "{wwan["intf"]}" cannot be member of VRF '
+ f'"{wwan["vrf"]}" and bridge {wwan["is_bridge_member"]} '
+ f'at the same time!'))
return None
@@ -152,12 +165,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')
@@ -172,17 +179,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
@@ -198,6 +206,12 @@ def apply(wwan):
# make logfile owned by root / vyattacfg
chown(wwan['logfile'], 'root', 'vyattacfg')
+ # re-add ourselves to any bridge we might have fallen out of
+ # FIXME: wwan isn't under vyos.ifconfig so we can't call
+ # Interfaces.add_to_bridge() so STP settings won't get applied
+ if wwan['is_bridge_member'] in Section.interfaces('bridge'):
+ BridgeIf(wwan['is_bridge_member'], create=False).add_port(wwan['intf'])
+
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py
index 3398bcdf2..6282c2cc7 100755
--- a/src/conf_mode/ipsec-settings.py
+++ b/src/conf_mode/ipsec-settings.py
@@ -99,7 +99,7 @@ def get_config():
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":
+ elif c.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)
diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py
new file mode 100755
index 000000000..d491395ac
--- /dev/null
+++ b/src/conf_mode/nat.py
@@ -0,0 +1,268 @@
+#!/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 jmespath
+import json
+import os
+
+from copy import deepcopy
+from sys import exit
+from netifaces import interfaces
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.util import call, cmd
+from vyos.validate import is_addr_assigned
+from vyos import ConfigError
+
+default_config_data = {
+ 'deleted': False,
+ 'destination': [],
+ 'helper_functions': None,
+ 'pre_ct_helper': '',
+ 'pre_ct_conntrack': '',
+ 'out_ct_helper': '',
+ 'out_ct_conntrack': '',
+ 'source': []
+}
+
+iptables_nat_config = '/tmp/vyos-nat-rules.nft'
+
+def _check_kmod():
+ """ load required Kernel modules """
+ modules = ['nft_nat', 'nft_chain_nat_ipv4']
+ for module in modules:
+ if not os.path.exists(f'/sys/module/{module}'):
+ if call(f'modprobe {module}') != 0:
+ raise ConfigError(f'Loading Kernel module {module} failed')
+
+
+def get_handler(json, chain, target):
+ """ Get nftable rule handler number of given chain/target combination.
+ Handler is required when adding NAT/Conntrack helper targets """
+ for x in json:
+ if x['chain'] != chain:
+ continue
+ if x['target'] != target:
+ continue
+ return x['handle']
+
+ return None
+
+
+def verify_rule(rule, err_msg):
+ """ Common verify steps used for both source and destination NAT """
+ if rule['translation_port'] or rule['dest_port'] or rule['source_port']:
+ if rule['protocol'] not in ['tcp', 'udp', 'tcp_udp']:
+ proto = rule['protocol']
+ raise ConfigError(f'{err_msg} ports can only be specified when protocol is "tcp", "udp" or "tcp_udp" (currently "{proto}")')
+
+ if '/' in rule['translation_address']:
+ raise ConfigError(f'{err_msg}\n' \
+ 'Cannot use ports with an IPv4net type translation address as it\n' \
+ 'statically maps a whole network of addresses onto another\n' \
+ 'network of addresses')
+
+ if not rule['translation_address']:
+ raise ConfigError(f'{err_msg} translation address not specified')
+
+
+def parse_source_destination(conf, source_dest):
+ """ Common wrapper to read in both NAT source and destination CLI """
+ tmp = []
+ base_level = ['nat', source_dest]
+ conf.set_level(base_level)
+ for number in conf.list_nodes(['rule']):
+ rule = {
+ 'description': '',
+ 'dest_address': '',
+ 'dest_port': '',
+ 'disabled': False,
+ 'exclude': False,
+ 'interface_in': '',
+ 'interface_out': '',
+ 'log': False,
+ 'protocol': 'all',
+ 'number': number,
+ 'source_address': '',
+ 'source_prefix': '',
+ 'source_port': '',
+ 'translation_address': '',
+ 'translation_prefix': '',
+ 'translation_port': ''
+ }
+ conf.set_level(base_level + ['rule', number])
+
+ if conf.exists(['description']):
+ rule['description'] = conf.return_value(['description'])
+
+ if conf.exists(['destination', 'address']):
+ rule['dest_address'] = conf.return_value(['destination', 'address'])
+
+ if conf.exists(['destination', 'port']):
+ rule['dest_port'] = conf.return_value(['destination', 'port'])
+
+ if conf.exists(['disable']):
+ rule['disabled'] = True
+
+ if conf.exists(['exclude']):
+ rule['exclude'] = True
+
+ if conf.exists(['inbound-interface']):
+ rule['interface_in'] = conf.return_value(['inbound-interface'])
+
+ if conf.exists(['outbound-interface']):
+ rule['interface_out'] = conf.return_value(['outbound-interface'])
+
+ if conf.exists(['log']):
+ rule['log'] = True
+
+ if conf.exists(['protocol']):
+ rule['protocol'] = conf.return_value(['protocol'])
+
+ if conf.exists(['source', 'address']):
+ rule['source_address'] = conf.return_value(['source', 'address'])
+
+ if conf.exists(['source', 'prefix']):
+ rule['source_prefix'] = conf.return_value(['source', 'prefix'])
+
+ if conf.exists(['source', 'port']):
+ rule['source_port'] = conf.return_value(['source', 'port'])
+
+ if conf.exists(['translation', 'address']):
+ rule['translation_address'] = conf.return_value(['translation', 'address'])
+
+ if conf.exists(['translation', 'prefix']):
+ rule['translation_prefix'] = conf.return_value(['translation', 'prefix'])
+
+ if conf.exists(['translation', 'port']):
+ rule['translation_port'] = conf.return_value(['translation', 'port'])
+
+ tmp.append(rule)
+
+ return tmp
+
+def get_config():
+ nat = deepcopy(default_config_data)
+ conf = Config()
+
+ # read in current nftable (once) for further processing
+ tmp = cmd('nft -j list table raw')
+ nftable_json = json.loads(tmp)
+
+ # condense the full JSON table into a list with only relevand informations
+ pattern = 'nftables[?rule].rule[?expr[].jump].{chain: chain, handle: handle, target: expr[].jump.target | [0]}'
+ condensed_json = jmespath.search(pattern, nftable_json)
+
+ if not conf.exists(['nat']):
+ nat['helper_functions'] = 'remove'
+
+ # Retrieve current table handler positions
+ nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_HELPER')
+ nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK')
+ nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_HELPER')
+ nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'NAT_CONNTRACK')
+
+ nat['deleted'] = True
+
+ return nat
+
+ # check if NAT connection tracking helpers need to be set up - this has to
+ # be done only once
+ if not get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK'):
+ nat['helper_functions'] = 'add'
+
+ # Retrieve current table handler positions
+ nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_IGNORE')
+ nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_PREROUTING_HOOK')
+ nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_IGNORE')
+ nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_OUTPUT_HOOK')
+
+ # set config level for parsing in NAT configuration
+ conf.set_level(['nat'])
+
+ # use a common wrapper function to read in the source / destination
+ # tree from the config - thus we do not need to replicate almost the
+ # same code :-)
+ for tgt in ['source', 'destination', 'nptv6']:
+ nat[tgt] = parse_source_destination(conf, tgt)
+
+ return nat
+
+def verify(nat):
+ if nat['deleted']:
+ # no need to verify the CLI as NAT is going to be deactivated
+ return None
+
+ if nat['helper_functions']:
+ if not (nat['pre_ct_ignore'] or nat['pre_ct_conntrack'] or nat['out_ct_ignore'] or nat['out_ct_conntrack']):
+ raise Exception('could not determine nftable ruleset handlers')
+
+ for rule in nat['source']:
+ interface = rule['interface_out']
+ err_msg = f"Source NAT configuration error in rule {rule['number']}:"
+
+ if interface and interface not in interfaces():
+ print(f'NAT configuration warning: interface {interface} does not exist on this system')
+
+ if not rule['interface_out']:
+ raise ConfigError(f'{err_msg} outbound-interface not specified')
+
+ if rule['translation_address']:
+ addr = rule['translation_address']
+ if addr != 'masquerade' and not is_addr_assigned(addr):
+ print(f'Warning: IP address {addr} does not exist on the system!')
+
+ # common rule verification
+ verify_rule(rule, err_msg)
+
+ for rule in nat['destination']:
+ interface = rule['interface_in']
+ err_msg = f"Destination NAT configuration error in rule {rule['number']}:"
+
+ if interface and interface not in interfaces():
+ print(f'NAT configuration warning: interface {interface} does not exist on this system')
+
+ if not rule['interface_in']:
+ raise ConfigError(f'{err_msg} inbound-interface not specified')
+
+ # common rule verification
+ verify_rule(rule, err_msg)
+
+ return None
+
+def generate(nat):
+ render(iptables_nat_config, 'firewall/nftables-nat.tmpl', nat, trim_blocks=True, permission=0o755)
+
+ return None
+
+def apply(nat):
+ cmd(f'{iptables_nat_config}')
+ if os.path.isfile(iptables_nat_config):
+ os.unlink(iptables_nat_config)
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ _check_kmod()
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py
index ed8c3637b..d6577579e 100755
--- a/src/conf_mode/protocols_bfd.py
+++ b/src/conf_mode/protocols_bfd.py
@@ -190,7 +190,7 @@ def generate(bfd):
if bfd is None:
return None
- render(config_file, 'frr-bfd/bfd.frr.tmpl', bfd)
+ render(config_file, 'frr/bfd.frr.tmpl', bfd)
return None
def apply(bfd):
diff --git a/src/conf_mode/protocols_igmp.py b/src/conf_mode/protocols_igmp.py
index 9b338c5b9..821ccb0fc 100755
--- a/src/conf_mode/protocols_igmp.py
+++ b/src/conf_mode/protocols_igmp.py
@@ -87,7 +87,7 @@ def generate(igmp):
if igmp is None:
return None
- render(config_file, 'igmp/igmp.frr.tmpl', igmp)
+ render(config_file, 'frr/igmp.frr.tmpl', igmp)
return None
def apply(igmp):
diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py
index 0a241277d..9b946b43a 100755
--- a/src/conf_mode/protocols_mpls.py
+++ b/src/conf_mode/protocols_mpls.py
@@ -127,7 +127,7 @@ def generate(mpls):
if mpls is None:
return None
- render(config_file, 'mpls/ldpd.frr.tmpl', mpls)
+ render(config_file, 'frr/ldpd.frr.tmpl', mpls)
return None
def apply(mpls):
diff --git a/src/conf_mode/protocols_pim.py b/src/conf_mode/protocols_pim.py
index f12de4a72..15c4a2b0f 100755
--- a/src/conf_mode/protocols_pim.py
+++ b/src/conf_mode/protocols_pim.py
@@ -114,7 +114,7 @@ def generate(pim):
if pim is None:
return None
- render(config_file, 'pim/pimd.frr.tmpl', pim)
+ render(config_file, 'frr/pimd.frr.tmpl', pim)
return None
def apply(pim):
diff --git a/src/conf_mode/protocols_static_multicast.py b/src/conf_mode/protocols_static_multicast.py
index 411a130ec..ba6324393 100755
--- a/src/conf_mode/protocols_static_multicast.py
+++ b/src/conf_mode/protocols_static_multicast.py
@@ -91,7 +91,7 @@ def generate(mroute):
if mroute is None:
return None
- render(config_file, 'frr-mcast/static_mcast.frr.tmpl', mroute)
+ render(config_file, 'frr/static_mcast.frr.tmpl', mroute)
return None
def apply(mroute):
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-server.py b/src/conf_mode/service_ipoe-server.py
index 17fa2c3f0..84443ade3 100755
--- a/src/conf_mode/service_ipoe-server.py
+++ b/src/conf_mode/service_ipoe-server.py
@@ -112,28 +112,30 @@ def get_config():
'name': interface,
'mac': []
}
- for client in conf.list_nodes(base_path + ['authentication', 'interface', interface, 'mac-address']):
- mac = {
+ for mac in conf.list_nodes(['authentication', 'interface', interface, 'mac-address']):
+ client = {
'address': mac,
'rate_download': '',
'rate_upload': '',
'vlan_id': ''
}
- conf.set_level(base_path + ['authentication', 'interface', interface, 'mac-address', client])
+ conf.set_level(base_path + ['authentication', 'interface', interface, 'mac-address', mac])
if conf.exists(['rate-limit', 'download']):
- mac['rate_download'] = conf.return_value(['rate-limit', 'download'])
+ client['rate_download'] = conf.return_value(['rate-limit', 'download'])
if conf.exists(['rate-limit', 'upload']):
- mac['rate_upload'] = conf.return_value(['rate-limit', 'upload'])
+ client['rate_upload'] = conf.return_value(['rate-limit', 'upload'])
if conf.exists(['vlan-id']):
- mac['vlan'] = conf.return_value(['vlan-id'])
+ client['vlan'] = conf.return_value(['vlan-id'])
- tmp['mac'].append(mac)
+ tmp['mac'].append(client)
ipoe['auth_interfaces'].append(tmp)
+ conf.set_level(base_path)
+
#
# authentication mode radius servers and settings
if conf.exists(['authentication', 'mode', 'radius']):
@@ -265,10 +267,6 @@ def generate(ipoe):
if not ipoe:
return None
- dirname = os.path.dirname(ipoe_conf)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
-
render(ipoe_conf, 'accel-ppp/ipoe.config.tmpl', ipoe, trim_blocks=True)
if ipoe['auth_mode'] == 'local':
diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py
index 95cb066d8..e05b0ab2a 100755
--- a/src/conf_mode/service_pppoe-server.py
+++ b/src/conf_mode/service_pppoe-server.py
@@ -23,7 +23,7 @@ from sys import exit
from vyos.config import Config
from vyos.template import render
-from vyos.util import call, get_half_cpus()
+from vyos.util import call, get_half_cpus
from vyos.validate import is_ipv4
from vyos import ConfigError
@@ -32,6 +32,7 @@ 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': [],
@@ -216,6 +217,19 @@ def get_config():
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']):
@@ -301,7 +315,7 @@ def get_config():
pppoe['mtu'] = conf.return_value(['mtu'])
if conf.exists(['session-control']):
- pppoe['session_control'] = conf.return_value(['session-control'])
+ pppoe['sesscrtl'] = conf.return_value(['session-control'])
# ppp_options
if conf.exists(['ppp-options']):
@@ -415,10 +429,6 @@ def generate(pppoe):
if not pppoe:
return None
- dirname = os.path.dirname(pppoe_conf)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
-
render(pppoe_conf, 'accel-ppp/pppoe.config.tmpl', pppoe, trim_blocks=True)
if pppoe['local_users']:
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_l2tp.py b/src/conf_mode/vpn_l2tp.py
index a4ef99d45..f312f2a17 100755
--- a/src/conf_mode/vpn_l2tp.py
+++ b/src/conf_mode/vpn_l2tp.py
@@ -340,10 +340,6 @@ 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, 'accel-ppp/l2tp.config.tmpl', l2tp, trim_blocks=True)
if l2tp['auth_mode'] == 'local':
diff --git a/src/conf_mode/vpn_pptp.py b/src/conf_mode/vpn_pptp.py
index 046fc8f9c..085c9c2c6 100755
--- a/src/conf_mode/vpn_pptp.py
+++ b/src/conf_mode/vpn_pptp.py
@@ -247,10 +247,6 @@ def generate(pptp):
if not pptp:
return None
- dirname = os.path.dirname(pptp_conf)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
-
render(pptp_conf, 'accel-ppp/pptp.config.tmpl', pptp, trim_blocks=True)
if pptp['local_users']:
diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py
index e6ce94709..7c3e3f515 100755
--- a/src/conf_mode/vpn_sstp.py
+++ b/src/conf_mode/vpn_sstp.py
@@ -22,10 +22,10 @@ from copy import deepcopy
from stat import S_IRUSR, S_IWUSR, S_IRGRP
from vyos.config import Config
-from vyos import ConfigError
-from vyos.util import call, run, get_half_cpus
from vyos.template import render
-
+from vyos.util import call, run, get_half_cpus
+from vyos.validate import is_ipv4
+from vyos import ConfigError
sstp_conf = '/run/accel-pppd/sstp.conf'
sstp_chap_secrets = '/run/accel-pppd/sstp.chap-secrets'
@@ -35,7 +35,12 @@ default_config_data = {
'auth_mode' : 'local',
'auth_proto' : ['auth_mschap_v2'],
'chap_secrets_file': sstp_chap_secrets, # used in Jinja2 template
+ 'client_ip_pool' : [],
+ 'client_ipv6_pool': [],
+ 'client_ipv6_delegate_prefix': [],
'client_gateway': '',
+ 'dnsv4' : [],
+ 'dnsv6' : [],
'radius_server' : [],
'radius_acct_tmo' : '3',
'radius_max_try' : '3',
@@ -49,8 +54,6 @@ default_config_data = {
'ssl_ca' : '',
'ssl_cert' : '',
'ssl_key' : '',
- 'client_ip_pool' : [],
- 'dnsv4' : [],
'mtu' : '',
'ppp_mppe' : 'prefer',
'ppp_echo_failure' : '',
@@ -210,7 +213,7 @@ def get_config():
#
- # read in client ip pool settings
+ # read in client IPv4 pool
conf.set_level(base_path + ['network-settings', 'client-ip-settings'])
if conf.exists(['subnet']):
sstp['client_ip_pool'] = conf.return_values(['subnet'])
@@ -219,10 +222,41 @@ def get_config():
sstp['client_gateway'] = conf.return_value(['gateway-address'])
#
+ # read in client IPv6 pool
+ conf.set_level(base_path + ['network-settings', 'client-ipv6-pool'])
+ if conf.exists(['prefix']):
+ for prefix in conf.list_nodes(['prefix']):
+ tmp = {
+ 'prefix': prefix,
+ 'mask': '64'
+ }
+
+ if conf.exists(['prefix', prefix, 'mask']):
+ tmp['mask'] = conf.return_value(['prefix', prefix, 'mask'])
+
+ sstp['client_ipv6_pool'].append(tmp)
+
+ if conf.exists(['delegate']):
+ for prefix in conf.list_nodes(['delegate']):
+ tmp = {
+ 'prefix': prefix,
+ 'mask': ''
+ }
+
+ if conf.exists(['delegate', prefix, 'delegation-prefix']):
+ tmp['mask'] = conf.return_value(['delegate', prefix, 'delegation-prefix'])
+
+ sstp['client_ipv6_delegate_prefix'].append(tmp)
+
+ #
# read in network settings
conf.set_level(base_path + ['network-settings'])
if conf.exists(['name-server']):
- sstp['dnsv4'] = conf.return_values(['name-server'])
+ for name_server in conf.return_values(['name-server']):
+ if is_ipv4(name_server):
+ sstp['dnsv4'].append(name_server)
+ else:
+ sstp['dnsv6'].append(name_server)
if conf.exists(['mtu']):
sstp['mtu'] = conf.return_value(['mtu'])
@@ -275,6 +309,14 @@ def verify(sstp):
if len(sstp['dnsv4']) > 2:
raise ConfigError('Not more then two IPv4 DNS name-servers can be configured')
+ # check ipv6
+ if sstp['client_ipv6_delegate_prefix'] and not sstp['client_ipv6_pool']:
+ raise ConfigError('IPv6 prefix delegation requires client-ipv6-pool prefix')
+
+ for prefix in sstp['client_ipv6_delegate_prefix']:
+ if not prefix['mask']:
+ raise ConfigError('Delegation-prefix required for individual delegated networks')
+
if not sstp['ssl_ca'] or not sstp['ssl_cert'] or not sstp['ssl_key']:
raise ConfigError('One or more SSL certificates missing')
@@ -303,10 +345,6 @@ 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, 'accel-ppp/sstp.config.tmpl', sstp, trim_blocks=True)
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/etc/systemd/system/pdns-recursor.service.d/override.conf b/src/etc/systemd/system/pdns-recursor.service.d/override.conf
index ef4dec303..750bc9972 100644
--- a/src/etc/systemd/system/pdns-recursor.service.d/override.conf
+++ b/src/etc/systemd/system/pdns-recursor.service.d/override.conf
@@ -1,5 +1,7 @@
[Service]
WorkingDirectory=
WorkingDirectory=/run/powerdns
+RuntimeDirectory=
+RuntimeDirectory=/run/powerdns
ExecStart=
ExecStart=/usr/sbin/pdns_recursor --daemon=no --write-pid=no --disable-syslog --log-timestamp=no --config-dir=/run/powerdns --socket-dir=/run/powerdns
diff --git a/src/helpers/validate-value.py b/src/helpers/validate-value.py
deleted file mode 100755
index a58ba61d1..000000000
--- a/src/helpers/validate-value.py
+++ /dev/null
@@ -1,45 +0,0 @@
-#!/usr/bin/env python3
-
-import re
-import os
-import sys
-import argparse
-
-from vyos.util import call
-
-parser = argparse.ArgumentParser()
-parser.add_argument('--regex', action='append')
-parser.add_argument('--exec', action='append')
-parser.add_argument('--value', action='store')
-
-args = parser.parse_args()
-
-debug = False
-
-# Multiple arguments work like logical OR
-
-try:
- for r in args.regex:
- if re.fullmatch(r, args.value):
- sys.exit(0)
-except Exception as exn:
- if debug:
- print(exn)
- else:
- pass
-
-try:
- for cmd in args.exec:
- cmd = "{0} {1}".format(cmd, args.value)
- if debug:
- print(cmd)
- res = call(cmd)
- if res == 0:
- sys.exit(0)
-except Exception as exn:
- if debug:
- print(exn)
- else:
- pass
-
-sys.exit(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/nat/4-to-5 b/src/migration-scripts/nat/4-to-5
new file mode 100755
index 000000000..dda191719
--- /dev/null
+++ b/src/migration-scripts/nat/4-to-5
@@ -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/>.
+
+# Drop the enable/disable from the nat "log" node. If log node is specified
+# it is "enabled"
+
+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)
+
+if not config.exists(['nat']):
+ # Nothing to do
+ exit(0)
+else:
+ for direction in ['source', 'destination']:
+ if not config.exists(['nat', direction]):
+ continue
+
+ for rule in config.list_nodes(['nat', direction, 'rule']):
+ base = ['nat', direction, 'rule', rule]
+
+ # Check if the log node exists and if log is enabled,
+ # migrate it to the new valueless 'log' node
+ if config.exists(base + ['log']):
+ tmp = config.return_value(base + ['log'])
+ config.delete(base + ['log'])
+ if tmp == 'enable':
+ config.set(base + ['log'])
+
+ 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/flow_accounting_op.py b/src/op_mode/flow_accounting_op.py
index bf8c39fd6..219ad6316 100755
--- a/src/op_mode/flow_accounting_op.py
+++ b/src/op_mode/flow_accounting_op.py
@@ -195,7 +195,7 @@ if not _uacctd_running():
# restart pmacct daemon
if cmd_args.action == 'restart':
# run command to restart flow-accounting
- cmd('systemctl restart uacctd.service',
+ cmd('sudo systemctl restart uacctd.service',
message='Failed to restart flow-accounting')
# clear in-memory collected flows
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_interfaces.py b/src/op_mode/show_interfaces.py
index 8b6690b7d..2f0f8a1c9 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):
@@ -230,17 +267,15 @@ def run_show_counters(ifnames, iftypes, vif, vrrp):
@register('clear')
-def run_clear_intf(intf, iftypes, vif, vrrp):
+def run_clear_intf(ifnames, iftypes, vif, vrrp):
for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp):
print(f'Clearing {interface.ifname}')
- interface = Interface(ifname, create=False, debug=False)
interface.operational.clear_counters()
@register('reset')
-def run_reset_intf(intf, iftypes, vif, vrrp):
+def run_reset_intf(ifnames, iftypes, vif, vrrp):
for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp):
- interface = Interface(ifname, create=False, debug=False)
interface.operational.reset_counters()
diff --git a/src/op_mode/show_nat_statistics.py b/src/op_mode/show_nat_statistics.py
new file mode 100755
index 000000000..0b53112f2
--- /dev/null
+++ b/src/op_mode/show_nat_statistics.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import jmespath
+import json
+
+from argparse import ArgumentParser
+from jinja2 import Template
+from sys import exit
+from vyos.util import cmd
+
+OUT_TMPL_SRC="""
+rule pkts bytes interface
+---- ---- ----- ---------
+{% for r in output %}
+{%- if r.comment -%}
+{%- set packets = r.counter.packets -%}
+{%- set bytes = r.counter.bytes -%}
+{%- set interface = r.interface -%}
+{# remove rule comment prefix #}
+{%- set comment = r.comment | replace('SRC-NAT-', '') | replace('DST-NAT-', '') | replace(' tcp_udp', '') -%}
+{{ "%-4s" | format(comment) }} {{ "%9s" | format(packets) }} {{ "%12s" | format(bytes) }} {{ interface }}
+{%- endif %}
+{% endfor %}
+"""
+
+parser = ArgumentParser()
+group = parser.add_mutually_exclusive_group()
+group.add_argument("--source", help="Show statistics for configured source NAT rules", action="store_true")
+group.add_argument("--destination", help="Show statistics for configured destination NAT rules", action="store_true")
+args = parser.parse_args()
+
+if args.source or args.destination:
+ tmp = cmd('sudo nft -j list table nat')
+ tmp = json.loads(tmp)
+
+ source = r"nftables[?rule.chain=='POSTROUTING'].rule.{chain: chain, handle: handle, comment: comment, counter: expr[].counter | [0], interface: expr[].match.right | [0] }"
+ destination = r"nftables[?rule.chain=='PREROUTING'].rule.{chain: chain, handle: handle, comment: comment, counter: expr[].counter | [0], interface: expr[].match.right | [0] }"
+ data = {
+ 'output' : jmespath.search(source if args.source else destination, tmp),
+ 'direction' : 'source' if args.source else 'destination'
+ }
+
+ tmpl = Template(OUT_TMPL_SRC, lstrip_blocks=True)
+ print(tmpl.render(data))
+ exit(0)
+else:
+ parser.print_help()
+ exit(1)
+
diff --git a/src/op_mode/to_be_migrated/vyatta-nat-translations.pl b/src/op_mode/to_be_migrated/vyatta-nat-translations.pl
new file mode 100755
index 000000000..94ed74bad
--- /dev/null
+++ b/src/op_mode/to_be_migrated/vyatta-nat-translations.pl
@@ -0,0 +1,267 @@
+#!/usr/bin/perl
+#
+# Module: vyatta-nat-translate.pl
+#
+# **** License ****
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 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.
+#
+# This code was originally developed by Vyatta, Inc.
+# Portions created by Vyatta are Copyright (C) 2007 Vyatta, Inc.
+# All Rights Reserved.
+#
+# Author: Stig Thormodsrud
+# Date: July 2008
+# Description: Script to display nat translations
+#
+# **** End License ****
+#
+
+use Getopt::Long;
+use XML::Simple;
+use Data::Dumper;
+use POSIX;
+
+use warnings;
+use strict;
+
+my $dump = 0;
+my ($xml_file, $verbose, $proto, $stats, $ipaddr, $pipe);
+my $type;
+my $verbose_format = "%-20s %-18s %-20s %-18s\n";
+my $format = "%-20s %-20s %-4s %-8s";
+
+sub add_xml_root {
+ my $xml = shift;
+
+ $xml = '<data>' . $xml . '</data>';
+ return $xml;
+}
+
+
+sub read_xml_file {
+ my $file = shift;
+
+ local($/, *FD); # slurp mode
+ open FD, "<", $file or die "Couldn't open $file\n";
+ my $xml = <FD>;
+ close FD;
+ return $xml;
+}
+
+sub print_xml {
+ my $data = shift;
+ print Dumper($data);
+}
+
+sub guess_snat_dnat {
+ my ($src, $dst) = @_;
+
+ if ($src->{original} eq $dst->{reply}) {
+ return "dnat";
+ }
+ if ($dst->{original} eq $src->{reply}) {
+ return "snat";
+ }
+ return "unkn";
+}
+
+sub nat_print_xml {
+ my ($data, $type) = @_;
+
+ my $flow = 0;
+
+ my %flowh;
+ while (1) {
+ my $meta = 0;
+ last if ! defined $data->{flow}[$flow];
+ my $flow_ref = $data->{flow}[$flow];
+ my $flow_type = $flow_ref->{type};
+ my (%src, %dst, %sport, %dport, %proto);
+ my (%packets, %bytes);
+ my $timeout = undef;
+ my $uses = undef;
+ while (1) {
+ my $meta_ref = $flow_ref->{meta}[$meta];
+ last if ! defined $meta_ref;
+ my $dir = $meta_ref->{direction};
+ if ($dir eq 'original' or $dir eq 'reply') {
+ my $l3_ref = $meta_ref->{layer3}[0];
+ my $l4_ref = $meta_ref->{layer4}[0];
+ my $count_ref = $meta_ref->{counters}[0];
+ if (defined $l3_ref) {
+ $src{$dir} = $l3_ref->{src}[0];
+ $dst{$dir} = $l3_ref->{dst}[0];
+ if (defined $l4_ref) {
+ $sport{$dir} = $l4_ref->{sport}[0];
+ $dport{$dir} = $l4_ref->{dport}[0];
+ $proto{$dir} = $l4_ref->{protoname};
+ }
+ }
+ if (defined $stats and defined $count_ref) {
+ $packets{$dir} = $count_ref->{packets}[0];
+ $bytes{$dir} = $count_ref->{bytes}[0];
+ }
+ } elsif ($dir eq 'independent') {
+ $timeout = $meta_ref->{timeout}[0];
+ $uses = $meta_ref->{'use'}[0];
+ }
+ $meta++;
+ }
+ my ($proto, $in_src, $in_dst, $out_src, $out_dst);
+ $proto = $proto{original};
+ $in_src = "$src{original}";
+ $in_src .= ":$sport{original}" if defined $sport{original};
+ $in_dst = "$dst{original}";
+ $in_dst .= ":$dport{original}" if defined $dport{original};
+ $out_src = "$dst{reply}";
+ $out_src .= ":$dport{reply}" if defined $dport{reply};
+ $out_dst = "$src{reply}";
+ $out_dst .= ":$sport{reply}" if defined $sport{reply};
+ if (defined $verbose) {
+ printf($verbose_format, $in_src, $in_dst, $out_src, $out_dst);
+ }
+# if (! defined $type) {
+# $type = guess_snat_dnat(\%src, \%dst);
+# }
+ if (defined $type) {
+ my ($from, $to);
+ if ($type eq 'source') {
+ $from = "$src{original}";
+ $to = "$dst{reply}";
+ if (defined $sport{original} and defined $dport{reply}) {
+ if ($sport{original} ne $dport{reply}) {
+ $from .= ":$sport{original}";
+ $to .= ":$dport{reply}";
+ }
+ }
+ } else {
+ $from = "$dst{original}";
+ $to = "$src{reply}";
+ if (defined $dport{original} and defined $sport{reply}) {
+ if ($dport{original} ne $sport{reply}) {
+ $from .= ":$dport{original}";
+ $to .= ":$sport{reply}";
+ }
+ }
+ }
+ if (defined $verbose) {
+ print " $proto: $from ==> $to";
+ } else {
+ my $timeout2 = "";
+ if (defined $timeout) {
+ $timeout2 = $timeout;
+ }
+ printf($format, $from, $to, $proto, $timeout2);
+ print " $flow_type" if defined $flow_type;
+ print "\n";
+ }
+ }
+ if (defined $verbose) {
+ print " timeout: $timeout" if defined $timeout;
+ print " use: $uses " if defined $uses;
+ print " type: $flow_type" if defined $flow_type;
+ print "\n";
+ }
+ if (defined $stats) {
+ foreach my $dir ('original', 'reply') {
+ if (defined $packets{$dir}) {
+ printf(" %-8s: packets %s, bytes %s\n",
+ $dir, $packets{$dir}, $bytes{$dir});
+ }
+ }
+ }
+ $flow++;
+ }
+ return $flow;
+}
+
+
+#
+# main
+#
+GetOptions("verbose" => \$verbose,
+ "proto=s" => \$proto,
+ "file=s" => \$xml_file,
+ "stats" => \$stats,
+ "type=s" => \$type,
+ "ipaddr=s" => \$ipaddr,
+ "pipe" => \$pipe,
+);
+
+my $conntrack = '/usr/sbin/conntrack';
+if (! -f $conntrack) {
+ die "Package [conntrack] not installed";
+}
+
+die "Must specify NAT type!" if !defined($type);
+die "Unknown NAT type!" if (($type ne 'source') && ($type ne 'destination'));
+
+my $xs = XML::Simple->new(ForceArray => 1, KeepRoot => 0);
+my ($xml, $data);
+
+# flush stdout after every write for pipe mode
+$| = 1 if defined $pipe;
+
+if (defined $verbose) {
+ printf($verbose_format, 'Pre-NAT src', 'Pre-NAT dst',
+ 'Post-NAT src', 'Post-NAT dst');
+} else {
+ printf($format, 'Pre-NAT', 'Post-NAT', 'Prot', 'Timeout');
+ print " Type" if defined $pipe;
+ print "\n";
+}
+
+if (defined $xml_file) {
+ $xml = read_xml_file($xml_file);
+ $data = $xs->XMLin($xml);
+ if ($dump) {
+ print_xml($data);
+ exit;
+ }
+ nat_print_xml($data, 'snat');
+
+} elsif (defined $pipe) {
+ while ($xml = <STDIN>) {
+ $xml =~ s/\<\?xml version=\"1\.0\" encoding=\"utf-8\"\?\>//;
+ $xml =~ s/\<conntrack\>//;
+ $xml = add_xml_root($xml);
+ $data = $xs->XMLin($xml);
+ nat_print_xml($data, $type);
+ }
+} else {
+ if (defined $proto) {
+ $proto = "-p $proto"
+ } else {
+ $proto = "";
+ }
+ if ($type eq 'source') {
+ my $ipopt = "";
+ if (defined $ipaddr) {
+ $ipopt = "--orig-src $ipaddr";
+ }
+ $xml = `sudo $conntrack -L -n $ipopt -o xml $proto 2>/dev/null`;
+ chomp $xml;
+ $data = undef;
+ $data = $xs->XMLin($xml) if ! $xml eq '';
+ }
+ if ($type eq 'destination') {
+ my $ipopt = "";
+ if (defined $ipaddr) {
+ $ipopt = "--orig-dst $ipaddr";
+ }
+ $xml = `sudo $conntrack -L -g $ipopt -o xml $proto 2>/dev/null`;
+ chomp $xml;
+ $data = undef;
+ $data = $xs->XMLin($xml) if ! $xml eq '';
+ }
+ nat_print_xml($data, $type) if defined $data;
+}
+
+# end of file
diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd
index a655762e9..647cbc8c1 100755
--- a/src/services/vyos-hostsd
+++ b/src/services/vyos-hostsd
@@ -25,6 +25,7 @@ import traceback
import re
import logging
import zmq
+import collections
import jinja2
@@ -79,8 +80,18 @@ resolv_tmpl_source = """
### Autogenerated by VyOS ###
### Do not edit, your changes will get overwritten ###
+# name server from static configuration
{% for ns in name_servers -%}
+{%- if name_servers[ns]['tag'] == "static" %}
nameserver {{ns}}
+{%- endif %}
+{% endfor -%}
+
+{% for ns in name_servers -%}
+{%- if name_servers[ns]['tag'] != "static" %}
+# name server from {{name_servers[ns]['tag']}}
+nameserver {{ns}}
+{%- endif %}
{% endfor -%}
{%- if domain_name %}
@@ -110,7 +121,7 @@ resolv_tmpl = jinja2.Template(resolv_tmpl_source)
# and re-created without having to track what needs
# to be changed
STATE = {
- "name_servers": {},
+ "name_servers": collections.OrderedDict({}),
"hosts": {},
"host_name": "vyos",
"domain_name": "",
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
index c36cbd640..4c41fa96d 100755
--- a/src/services/vyos-http-api-server
+++ b/src/services/vyos-http-api-server
@@ -26,7 +26,8 @@ import signal
import vyos.config
-import bottle
+from flask import Flask, request
+from waitress import serve
from functools import wraps
@@ -37,7 +38,7 @@ from vyos.config import VyOSError
DEFAULT_CONFIG_FILE = '/etc/vyos/http-api.conf'
CFG_GROUP = 'vyattacfg'
-app = bottle.default_app()
+app = Flask(__name__)
# Giant lock!
lock = threading.Lock()
@@ -55,18 +56,31 @@ def check_auth(key_list, key):
return id
def error(code, msg):
- bottle.response.status = code
resp = {"success": False, "error": msg, "data": None}
- return json.dumps(resp)
+ return json.dumps(resp), code
def success(data):
resp = {"success": True, "data": data, "error": None}
return json.dumps(resp)
+def get_command(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ cmd = request.form.get("data")
+ if not cmd:
+ return error(400, "Non-empty data field is required")
+ try:
+ cmd = json.loads(cmd)
+ except Exception as e:
+ return error(400, "Failed to parse JSON: {0}".format(e))
+ return f(cmd, *args, **kwargs)
+
+ return decorated_function
+
def auth_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
- key = bottle.request.forms.get("key")
+ key = request.form.get("key")
api_keys = app.config['vyos_keys']
id = check_auth(api_keys, key)
if not id:
@@ -75,28 +89,20 @@ def auth_required(f):
return decorated_function
-@app.route('/configure', method='POST')
+@app.route('/configure', methods=['POST'])
+@get_command
@auth_required
-def configure():
+def configure_op(commands):
session = app.config['vyos_session']
env = session.get_session_env()
config = vyos.config.Config(session_env=env)
- strict_field = bottle.request.forms.get("strict")
+ strict_field = request.form.get("strict")
if strict_field == "true":
strict = True
else:
strict = False
- commands = bottle.request.forms.get("data")
- if not commands:
- return error(400, "Non-empty data field is required")
- else:
- try:
- commands = json.loads(commands)
- except Exception as e:
- return error(400, "Failed to parse JSON: {0}".format(e))
-
# Allow users to pass just one command
if not isinstance(commands, list):
commands = [commands]
@@ -186,16 +192,14 @@ def configure():
else:
return success(None)
-@app.route('/retrieve', method='POST')
+@app.route('/retrieve', methods=['POST'])
+@get_command
@auth_required
-def get_value():
+def retrieve_op(command):
session = app.config['vyos_session']
env = session.get_session_env()
config = vyos.config.Config(session_env=env)
- command = bottle.request.forms.get("data")
- command = json.loads(command)
-
try:
op = command['op']
path = " ".join(command['path'])
@@ -229,20 +233,20 @@ def get_value():
return error(400, "\"{0}\" is not a valid operation".format(op))
except VyOSError as e:
return error(400, str(e))
+ except ConfigSessionError as e:
+ return error(400, str(e))
except Exception as e:
print(traceback.format_exc(), file=sys.stderr)
return error(500, "An internal error occured. Check the logs for details.")
return success(res)
-@app.route('/config-file', method='POST')
+@app.route('/config-file', methods=['POST'])
+@get_command
@auth_required
-def config_file_op():
+def config_file_op(command):
session = app.config['vyos_session']
- command = bottle.request.forms.get("data")
- command = json.loads(command)
-
try:
op = command['op']
except KeyError:
@@ -264,7 +268,7 @@ def config_file_op():
res = session.commit()
else:
return error(400, "\"{0}\" is not a valid operation".format(op))
- except VyOSError as e:
+ except ConfigSessionError as e:
return error(400, str(e))
except Exception as e:
print(traceback.format_exc(), file=sys.stderr)
@@ -272,14 +276,12 @@ def config_file_op():
return success(res)
-@app.route('/image', method='POST')
+@app.route('/image', methods=['POST'])
+@get_command
@auth_required
-def config_file_op():
+def image_op(command):
session = app.config['vyos_session']
- command = bottle.request.forms.get("data")
- command = json.loads(command)
-
try:
op = command['op']
except KeyError:
@@ -300,7 +302,7 @@ def config_file_op():
res = session.remove_image(name)
else:
return error(400, "\"{0}\" is not a valid operation".format(op))
- except VyOSError as e:
+ except ConfigSessionError as e:
return error(400, str(e))
except Exception as e:
print(traceback.format_exc(), file=sys.stderr)
@@ -309,14 +311,12 @@ def config_file_op():
return success(res)
-@app.route('/generate', method='POST')
+@app.route('/generate', methods=['POST'])
+@get_command
@auth_required
-def generate_op():
+def generate_op(command):
session = app.config['vyos_session']
- command = bottle.request.forms.get("data")
- command = json.loads(command)
-
try:
op = command['op']
path = command['path']
@@ -339,14 +339,12 @@ def generate_op():
return success(res)
-@app.route('/show', method='POST')
+@app.route('/show', methods=['POST'])
+@get_command
@auth_required
-def show_op():
+def show_op(command):
session = app.config['vyos_session']
- command = bottle.request.forms.get("data")
- command = json.loads(command)
-
try:
op = command['op']
path = command['path']
@@ -398,4 +396,8 @@ if __name__ == '__main__':
signal.signal(signal.SIGTERM, sig_handler)
- bottle.run(app, host=server_config["listen_address"], port=server_config["port"], debug=True)
+ try:
+ serve(app, host=server_config["listen_address"],
+ port=server_config["port"])
+ except OSError as e:
+ print(f"OSError {e}")
diff --git a/src/system/on-dhcp-event.sh b/src/system/on-dhcp-event.sh
index 385ae460f..3e2b6430d 100755
--- a/src/system/on-dhcp-event.sh
+++ b/src/system/on-dhcp-event.sh
@@ -43,13 +43,13 @@ case "$action" in
exit 1
fi
# add host
- /usr/bin/vyos-hostsd-client --add-hosts --tag "DHCP-$client_ip" --host "$client_fqdn_name,$client_ip"
+ sudo /usr/bin/vyos-hostsd-client --add-hosts --tag "DHCP-$client_ip" --host "$client_fqdn_name,$client_ip"
((changes++))
;;
release) # delete mapping for released address
# delete host
- /usr/bin/vyos-hostsd-client --delete-hosts --tag "DHCP-$client_ip"
+ sudo /usr/bin/vyos-hostsd-client --delete-hosts --tag "DHCP-$client_ip"
((changes++))
;;
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/dhcp6c@.service b/src/systemd/dhcp6c@.service
new file mode 100644
index 000000000..1a4175461
--- /dev/null
+++ b/src/systemd/dhcp6c@.service
@@ -0,0 +1,16 @@
+[Unit]
+Description=WIDE DHCPv6 client on %i
+Documentation=man:dhcp6c(8) man:dhcp6c.conf(5)
+ConditionPathExists=/run/dhcp6c/dhcp6c.%i.conf
+After=vyos-router.service
+
+[Service]
+WorkingDirectory=/run/dhcp6c
+Type=forking
+PIDFile=/run/dhcp6c/dhcp6c.%i.pid
+ExecStart=/usr/sbin/dhcp6c -D -k /run/dhcp6c/dhcp6c.%i.sock -c /run/dhcp6c/dhcp6c.%i.conf -p /run/dhcp6c/dhcp6c.%i.pid %i
+
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/systemd/isc-dhcp-relay.service b/src/systemd/isc-dhcp-relay.service
index ebf4d234e..56bcec840 100644
--- a/src/systemd/isc-dhcp-relay.service
+++ b/src/systemd/isc-dhcp-relay.service
@@ -2,13 +2,19 @@
Description=ISC DHCP IPv4 relay
Documentation=man:dhcrelay(8)
Wants=network-online.target
+RequiresMountsFor=/run
ConditionPathExists=/run/dhcp-relay/dhcp.conf
After=vyos-router.service
[Service]
+Type=forking
WorkingDirectory=/run/dhcp-relay
+RuntimeDirectory=dhcp-relay
+RuntimeDirectoryPreserve=yes
EnvironmentFile=/run/dhcp-relay/dhcp.conf
-ExecStart=/usr/sbin/dhcrelay -d -4 $OPTIONS
+PIDFile=/run/dhcp-relay/dhcrelay.pid
+ExecStart=/usr/sbin/dhcrelay -4 -pf /run/dhcp-relay/dhcrelay.pid $OPTIONS
+Restart=always
[Install]
WantedBy=multi-user.target
diff --git a/src/systemd/isc-dhcp-relay6.service b/src/systemd/isc-dhcp-relay6.service
index a477618b1..85ff16e41 100644
--- a/src/systemd/isc-dhcp-relay6.service
+++ b/src/systemd/isc-dhcp-relay6.service
@@ -2,13 +2,19 @@
Description=ISC DHCP IPv6 relay
Documentation=man:dhcrelay(8)
Wants=network-online.target
+RequiresMountsFor=/run
ConditionPathExists=/run/dhcp-relay/dhcpv6.conf
After=vyos-router.service
[Service]
+Type=forking
WorkingDirectory=/run/dhcp-relay
+RuntimeDirectory=dhcp-relay
+RuntimeDirectoryPreserve=yes
EnvironmentFile=/run/dhcp-relay/dhcpv6.conf
-ExecStart=/usr/sbin/dhcrelay -d -6 $OPTIONS
+PIDFile=/run/dhcp-relay/dhcrelayv6.pid
+ExecStart=/usr/sbin/dhcrelay -6 -pf /run/dhcp-relay/dhcrelayv6.pid $OPTIONS
+Restart=always
[Install]
WantedBy=multi-user.target
diff --git a/src/systemd/isc-dhcp-server.service b/src/systemd/isc-dhcp-server.service
index d848e3df1..e13c66dc6 100644
--- a/src/systemd/isc-dhcp-server.service
+++ b/src/systemd/isc-dhcp-server.service
@@ -6,14 +6,19 @@ ConditionPathExists=/run/dhcp-server/dhcpd.conf
After=vyos-router.service
[Service]
+Type=forking
WorkingDirectory=/run/dhcp-server
-# The leases files need to be root:vyattacfg even when dropping privileges
-ExecStart=/bin/sh -ec '\
- CONFIG_FILE=/run/dhcp-server/dhcpd.conf; \
- [ -e /config/dhcpd.leases ] || touch /config/dhcpd.leases; \
- chown root:vyattacfg /config/dhcpd.leases; \
- chmod 664 /config/dhcpd.leases; \
- exec /usr/sbin/dhcpd -user nobody -group nogroup -f -4 -pf /run/dhcp-server/dhcpd.pid -cf $CONFIG_FILE -lf /config/dhcpd.leases'
+RuntimeDirectory=dhcp-server
+RuntimeDirectoryPreserve=yes
+Environment=PID_FILE=/run/dhcp-server/dhcpd.pid CONFIG_FILE=/run/dhcp-server/dhcpd.conf LEASE_FILE=/config/dhcpd.leases
+PIDFile=/run/dhcp-server/dhcpd.pid
+ExecStartPre=/bin/sh -ec '\
+touch ${LEASE_FILE}; \
+chown nobody:nogroup ${LEASE_FILE}* ; \
+chmod 664 ${LEASE_FILE}* ; \
+/usr/sbin/dhcpd -4 -t -T -q -user nobody -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} '
+ExecStart=/usr/sbin/dhcpd -4 -q -user nobody -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE}
+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..8ac861d7a 100644
--- a/src/systemd/isc-dhcp-server6.service
+++ b/src/systemd/isc-dhcp-server6.service
@@ -2,17 +2,23 @@
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]
+Type=forking
WorkingDirectory=/run/dhcp-server
-# The leases files need to be root:vyattacfg even when dropping privileges
-ExecStart=/bin/sh -ec '\
- [ -e /config/dhcpdv6.leases ] || touch /config/dhcpdv6.leases; \
- chown root:vyattacfg /config/dhcpdv6.leases; \
- chmod 664 /config/dhcpdv6.leases; \
- exec /usr/sbin/dhcpd -user nobody -group nogroup -f -6 -pf /run/dhcp-server/dhcpdv6.pid -cf /run/dhcp-server/dhcpdv6.conf -lf /config/dhcpdv6.leases'
+RuntimeDirectory=dhcp-server
+RuntimeDirectoryPreserve=yes
+Environment=PID_FILE=/run/dhcp-server/dhcpdv6.pid CONFIG_FILE=/run/dhcp-server/dhcpdv6.conf LEASE_FILE=/config/dhcpdv6.leases
+PIDFile=/run/dhcp-server/dhcpdv6.pid
+ExecStartPre=/bin/sh -ec '\
+touch ${LEASE_FILE}; \
+chown nobody:nogroup ${LEASE_FILE}* ; \
+chmod 664 ${LEASE_FILE}* ; \
+/usr/sbin/dhcpd -6 -t -T -q -user nobody -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} '
+ExecStart=/usr/sbin/dhcpd -6 -q -user nobody -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE}
+Restart=always
[Install]
WantedBy=multi-user.target
diff --git a/src/validators/file-exists b/src/validators/file-exists
index e179805ed..5cef6b199 100755
--- a/src/validators/file-exists
+++ b/src/validators/file-exists
@@ -23,40 +23,39 @@ import os
import sys
import argparse
-parser = argparse.ArgumentParser()
-parser.add_argument("-d", "--directory", type=str, help="File must be present in this directory.")
-parser.add_argument("-e", "--error", action="store_true", help="Tread warnings as errors - change exit code to '1'")
-parser.add_argument("file", type=str, help="Path of file to validate")
-args = parser.parse_args()
-
-msg_prefix = "WARNING: "
-if args.error:
- msg_prefix = "ERROR: "
-
-#
-# Always check if the given file exists
-#
-if not os.path.exists(args.file):
- print(msg_prefix + "File '{}' not found".format(args.file))
- if args.error:
- sys.exit(1)
- else:
- sys.exit(0)
-
-#
-# Optional check if the file is under a certain directory path
-#
-if args.directory:
- # remove directory path from path to verify
- rel_filename = args.file.replace(args.directory, '').lstrip('/')
-
- if not os.path.exists(args.directory + '/' + rel_filename):
- print(msg_prefix + "'{}' lies outside of '{}' directory.\n" \
- "It will not get preserved during image upgrade!".format(args.file, args.directory))
- if args.error:
- sys.exit(1)
- else:
- sys.exit(0)
-
-sys.exit(0)
+def exit(strict, message):
+ if strict:
+ sys.exit(f'ERROR: {message}')
+ print(f'WARNING: {message}', file=sys.stderr)
+ sys.exit()
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-d", "--directory", type=str, help="File must be present in this directory.")
+ parser.add_argument("-e", "--error", action="store_true", help="Tread warnings as errors - change exit code to '1'")
+ parser.add_argument("file", type=str, help="Path of file to validate")
+
+ args = parser.parse_args()
+
+ #
+ # Always check if the given file exists
+ #
+ if not os.path.exists(args.file):
+ exit(args.error, f"File '{args.file}' not found")
+
+ #
+ # Optional check if the file is under a certain directory path
+ #
+ if args.directory:
+ # remove directory path from path to verify
+ rel_filename = args.file.replace(args.directory, '').lstrip('/')
+
+ if not os.path.exists(args.directory + '/' + rel_filename):
+ exit(args.error,
+ f"'{args.file}' lies outside of '{args.directory}' directory.\n"
+ "It will not get preserved during image upgrade!"
+ )
+
+ sys.exit()
diff --git a/src/validators/fqdn b/src/validators/fqdn
index 9f4ed764f..347ffda42 100755
--- a/src/validators/fqdn
+++ b/src/validators/fqdn
@@ -14,14 +14,17 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-from re import match
-from sys import argv,exit
+import re
+import sys
-if len(argv) == 2:
- # pattern copied from: https://www.regextester.com/103452
- pattern = "(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)"
- if match(pattern, argv[1]):
- exit(0)
- else:
- exit(1)
+# pattern copied from: https://www.regextester.com/103452
+pattern = "(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)"
+
+
+if __name__ == '__main__':
+ if len(sys.argv) != 2:
+ sys.exit(1)
+ if not re.match(pattern, sys.argv[1]):
+ sys.exit(1)
+ sys.exit(0)
diff --git a/src/validators/ip-protocol b/src/validators/ip-protocol
new file mode 100755
index 000000000..078f8e319
--- /dev/null
+++ b/src/validators/ip-protocol
@@ -0,0 +1,41 @@
+#!/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 re
+from sys import argv,exit
+
+if __name__ == '__main__':
+ if len(argv) != 2:
+ exit(1)
+
+ input = argv[1]
+ try:
+ # IP protocol can be in the range 0 - 255, thus the range must end with 256
+ if int(input) in range(0, 256):
+ exit(0)
+ except ValueError:
+ pass
+
+ pattern = "!?\\b(all|ip|hopopt|icmp|igmp|ggp|ipencap|st|tcp|egp|igp|pup|udp|" \
+ "tcp_udp|hmp|xns-idp|rdp|iso-tp4|dccp|xtp|ddp|idpr-cmtp|ipv6|" \
+ "ipv6-route|ipv6-frag|idrp|rsvp|gre|esp|ah|skip|ipv6-icmp|" \
+ "ipv6-nonxt|ipv6-opts|rspf|vmtp|eigrp|ospf|ax.25|ipip|etherip|" \
+ "encap|99|pim|ipcomp|vrrp|l2tp|isis|sctp|fc|mobility-header|" \
+ "udplite|mpls-in-ip|manet|hip|shim6|wesp|rohc)\\b"
+ if re.match(pattern, input):
+ exit(0)
+
+ exit(1)
diff --git a/src/validators/ipv4-address-exclude b/src/validators/ipv4-address-exclude
new file mode 100755
index 000000000..80ad17d45
--- /dev/null
+++ b/src/validators/ipv4-address-exclude
@@ -0,0 +1,7 @@
+#!/bin/sh
+arg="$1"
+if [ "${arg:0:1}" != "!" ]; then
+ exit 1
+fi
+path=$(dirname "$0")
+${path}/ipv4-address "${arg:1}"
diff --git a/src/validators/ipv4-prefix-exclude b/src/validators/ipv4-prefix-exclude
new file mode 100755
index 000000000..4f7de400a
--- /dev/null
+++ b/src/validators/ipv4-prefix-exclude
@@ -0,0 +1,7 @@
+#!/bin/sh
+arg="$1"
+if [ "${arg:0:1}" != "!" ]; then
+ exit 1
+fi
+path=$(dirname "$0")
+${path}/ipv4-prefix "${arg:1}"
diff --git a/src/validators/ipv4-range b/src/validators/ipv4-range
new file mode 100755
index 000000000..ae3f3f163
--- /dev/null
+++ b/src/validators/ipv4-range
@@ -0,0 +1,33 @@
+#!/bin/bash
+
+# snippet from https://stackoverflow.com/questions/10768160/ip-address-converter
+ip2dec () {
+ local a b c d ip=$@
+ IFS=. read -r a b c d <<< "$ip"
+ printf '%d\n' "$((a * 256 ** 3 + b * 256 ** 2 + c * 256 + d))"
+}
+
+# Only run this if there is a hypen present in $1
+if [[ "$1" =~ "-" ]]; then
+ # This only works with real bash (<<<) - split IP addresses into array with
+ # hyphen as delimiter
+ readarray -d - -t strarr <<< $1
+
+ ipaddrcheck --is-ipv4-single ${strarr[0]}
+ if [ $? -gt 0 ]; then
+ exit 1
+ fi
+
+ ipaddrcheck --is-ipv4-single ${strarr[1]}
+ if [ $? -gt 0 ]; then
+ exit 1
+ fi
+
+ start=$(ip2dec ${strarr[0]})
+ stop=$(ip2dec ${strarr[1]})
+ if [ $start -ge $stop ]; then
+ exit 1
+ fi
+fi
+
+exit 0
diff --git a/src/validators/ipv4-range-exclude b/src/validators/ipv4-range-exclude
new file mode 100755
index 000000000..3787b4dec
--- /dev/null
+++ b/src/validators/ipv4-range-exclude
@@ -0,0 +1,7 @@
+#!/bin/sh
+arg="$1"
+if [ "${arg:0:1}" != "!" ]; then
+ exit 1
+fi
+path=$(dirname "$0")
+${path}/ipv4-range "${arg:1}"
diff --git a/src/validators/mac-address b/src/validators/mac-address
index 435920b84..b2d3496f4 100755
--- a/src/validators/mac-address
+++ b/src/validators/mac-address
@@ -15,12 +15,15 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
-from sys import exit, argv
+import sys
-if len(argv) == 2:
- pattern = "^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})$"
- if re.match(pattern, argv[1]):
- exit(0)
- else:
- exit(1)
+pattern = "^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})$"
+
+
+if __name__ == '__main__':
+ if len(sys.argv) != 2:
+ sys.exit(1)
+ if not re.match(pattern, sys.argv[1]):
+ sys.exit(1)
+ sys.exit(0)
diff --git a/src/validators/numeric b/src/validators/numeric
deleted file mode 100755
index 0a2d83d14..000000000
--- a/src/validators/numeric
+++ /dev/null
@@ -1,78 +0,0 @@
-#!/usr/bin/env python3
-#
-# numeric value validator
-#
-# Copyright (C) 2017 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
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; If not, see <http://www.gnu.org/licenses/>.
-
-import sys
-import argparse
-import re
-
-parser = argparse.ArgumentParser()
-parser.add_argument("-f", "--float", action="store_true", help="Accept floating point values")
-group = parser.add_mutually_exclusive_group()
-group.add_argument("-r", "--range", type=str, help="Check if the number is within range (inclusive), example: 1024-65535", action='append')
-group.add_argument("-n", "--non-negative", action="store_true", help="Check if the number is non-negative (>= 0)")
-group.add_argument("-p", "--positive", action="store_true", help="Check if the number is positive (> 0)")
-parser.add_argument("number", type=str, help="Number to validate")
-
-args = parser.parse_args()
-
-# Try to load the argument
-number = None
-if args.float:
- try:
- number = float(args.number)
- except:
- print("{0} is not a valid floating point number".format(args.number), file=sys.stderr)
- sys.exit(1)
-else:
- try:
- number = int(args.number)
- except:
- print("{0} is not a valid integer number".format(args.number), file=sys.stderr)
- sys.exit(1)
-
-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)
- except:
- print("{0} is not a valid number range",format(args.range), file=sys.stderr)
- sys.exit(1)
-
- if (number >= lower) and (number <= upper):
- valid = True
- # end for
-
- if not valid:
- if len(args.range) > 1:
- err_msg = "Number {0} is not in any of the ranges {1}".format(number, args.range)
- else:
- err_msg = "Number {0} is not in the range {1}".format(number, args.range[0])
- print(err_msg, file=sys.stderr)
- sys.exit(1)
-elif args.non_negative:
- if number < 0:
- print("Number should be non-negative", file=sys.stderr)
- sys.exit(1)
-elif args.positive:
- if number <= 0:
- print("Number should be positive", file=sys.stderr)
- sys.exit(1)
-
diff --git a/src/validators/script b/src/validators/script
index 689296a73..2665ec1f6 100755
--- a/src/validators/script
+++ b/src/validators/script
@@ -17,7 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with this program; If not, see <http://www.gnu.org/licenses/>.
-import re
import os
import sys
import shlex
@@ -25,22 +24,22 @@ import shlex
import vyos.util
-if len(sys.argv) < 2:
- print("Please specify script file to check")
- sys.exit(1)
+if __name__ == '__main__':
+ if len(sys.argv) < 2:
+ sys.exit('Please specify script file to check')
-#if the "script" is a script+ stowaway arugments, this removes the aguements
-script = shlex.split(sys.argv[1])[0]
+ # if the "script" is a script+ stowaway arguments, this removes the aguements
+ script = shlex.split(sys.argv[1])[0]
-if not os.path.exists(script):
- print("File {0} does not exist".format(script))
- sys.exit(1)
+ if not os.path.exists(script):
+ sys.exit(f'File {script} does not exist')
-if not (os.path.isfile(script) and os.access(script, os.X_OK)):
- print("File {0} is not an executable file".format(script))
- sys.exit(1)
+ if not (os.path.isfile(script) and os.access(script, os.X_OK)):
+ sys.exit('File {script} is not an executable file')
-# File outside the config dir is just a warning
-res, warning = vyos.util.file_is_persistent(script)
-if not res:
- print(warning)
+ # File outside the config dir is just a warning
+ if not vyos.util.file_is_persistent(script):
+ sys.exit(
+ 'Warning: file {path} is outside the / config directory\n'
+ 'It will not be automatically migrated to a new image on system update'
+ )
diff --git a/src/validators/timezone b/src/validators/timezone
index ec845e755..baf5abca2 100755
--- a/src/validators/timezone
+++ b/src/validators/timezone
@@ -15,25 +15,19 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import argparse
-
-from sys import exit
+import sys
from vyos.util import cmd
-parser = argparse.ArgumentParser()
-parser.add_argument("--validate", action="store", help="Check if timezone is valid")
if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--validate", action="store", required=True, help="Check if timezone is valid")
args = parser.parse_args()
- if args.validate:
- tz_data = cmd('find /usr/share/zoneinfo/posix -type f -or -type l | sed -e s:/usr/share/zoneinfo/posix/::')
- tz_data = tz_data.split('\n')
- # if timezone can't be found in list it's invalid
- if args.validate not in tz_data:
- exit(1)
- else:
- parser.print_help()
- exit(1)
+ tz_data = cmd('find /usr/share/zoneinfo/posix -type f -or -type l | sed -e s:/usr/share/zoneinfo/posix/::')
+ tz_data = tz_data.split('\n')
- exit(0)
+ if args.validate not in tz_data:
+ sys.exit("the timezone can't be found in the timezone list")
+ sys.exit()
diff --git a/src/validators/vrf-name b/src/validators/vrf-name
index 11c453f4d..878893c46 100755
--- a/src/validators/vrf-name
+++ b/src/validators/vrf-name
@@ -14,27 +14,30 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
import re
-from sys import exit, argv
+import sys
+
+
+if __name__ == '__main__':
+ if len(sys.argv) != 2:
+ sys.exit(1)
+
+ vrf = sys.argv[1]
+ length = len(vrf)
-if len(argv) == 2:
- len = len(argv[1])
- # VRF instance name must be 16 characters or less, python range needs to be
- # extended by one
- if not len in range(1, 17):
- exit(1)
+ if length not in range(1, 17):
+ sys.exit('VRF instance name must be 16 characters or less')
# Treat loopback interface "lo" explicitly. Adding "lo" explicitly to the
# following regex pattern would deny any VRF name starting with lo - thuse
# local-vrf would be illegal - and that we do not want.
- if argv[1] == "lo":
- exit(1)
+ if vrf == "lo":
+ exit(f'"{vrf}" is invalid as VRF name as it is an interface name')
- # VRF instances should not be named after regular interface names like bond0,
- # br10 and so on - this can cause a lot of confusion/trouble
pattern = "^(?!(bond|br|dum|eth|lan|eno|ens|enp|enx|gnv|ipoe|l2tp|l2tpeth|" \
"vtun|ppp|pppoe|peth|tun|vti|vxlan|wg|wlan|wlm)\d+(\.\d+(v.+)?)?$).*$"
- if re.match(pattern, argv[1]):
- exit(0)
+ if not re.match(pattern, vrf):
+ sys.exit(f'"{vrf}" is invalid as VRF name as it is an interface name')
-exit(1)
+ sys.exit(0)