summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/conf_mode/dhcp_server.py55
-rwxr-xr-xsrc/conf_mode/dhcpv6_server.py2
-rwxr-xr-xsrc/conf_mode/dns_forwarding.py15
-rwxr-xr-xsrc/conf_mode/flow_accounting_conf.py2
-rwxr-xr-xsrc/conf_mode/host_name.py49
-rwxr-xr-xsrc/conf_mode/http-api.py12
-rwxr-xr-xsrc/conf_mode/interfaces-bridge.py91
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py50
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py47
-rwxr-xr-xsrc/conf_mode/interfaces-tunnel.py37
-rwxr-xr-xsrc/conf_mode/interfaces-wwan.py4
-rwxr-xr-xsrc/conf_mode/ipsec-settings.py39
-rwxr-xr-xsrc/conf_mode/protocols_isis.py258
-rwxr-xr-xsrc/conf_mode/protocols_rip.py3
-rwxr-xr-xsrc/conf_mode/snmp.py19
-rwxr-xr-xsrc/conf_mode/system-login-banner.py15
-rwxr-xr-xsrc/conf_mode/system-login.py6
-rwxr-xr-xsrc/conf_mode/system_console.py70
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py67
-rwxr-xr-xsrc/conf_mode/vpn_l2tp.py2
-rwxr-xr-xsrc/conf_mode/vpn_sstp.py6
-rwxr-xr-xsrc/conf_mode/vrrp.py348
-rw-r--r--src/etc/cron.d/check-wwan1
-rw-r--r--src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient25
-rw-r--r--src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup2
-rw-r--r--src/etc/systemd/system/keepalived.service.d/override.conf10
-rw-r--r--src/etc/systemd/system/openvpn@.service.d/10-override.conf14
-rw-r--r--src/etc/systemd/system/openvpn@.service.d/override.conf9
-rw-r--r--src/etc/udev/rules.d/90-vyos-serial.rules4
-rwxr-xr-xsrc/helpers/vyos-check-wwan.py37
-rwxr-xr-xsrc/migration-scripts/dhcp-server/5-to-687
-rwxr-xr-xsrc/migration-scripts/dns-forwarding/1-to-210
-rwxr-xr-xsrc/migration-scripts/interfaces/20-to-21120
-rwxr-xr-xsrc/migration-scripts/interfaces/21-to-2246
-rwxr-xr-xsrc/migration-scripts/isis/0-to-159
-rwxr-xr-xsrc/migration-scripts/system/20-to-2148
-rwxr-xr-xsrc/op_mode/connect_disconnect.py68
-rwxr-xr-xsrc/op_mode/generate_ipsec_debug_archive.sh36
-rwxr-xr-xsrc/op_mode/ping.py10
-rwxr-xr-xsrc/op_mode/powerctrl.py33
-rwxr-xr-xsrc/op_mode/restart_frr.py133
-rwxr-xr-xsrc/op_mode/show_dhcp.py8
-rwxr-xr-xsrc/op_mode/show_interfaces.py34
-rwxr-xr-xsrc/op_mode/show_ipsec_sa.py168
-rwxr-xr-xsrc/op_mode/show_system_integrity.py70
-rwxr-xr-xsrc/op_mode/show_version.py4
-rwxr-xr-xsrc/op_mode/show_wwan.py18
-rw-r--r--src/services/api/graphql/README.graphql140
-rw-r--r--src/services/api/graphql/bindings.py14
-rw-r--r--src/services/api/graphql/graphql/__init__.py0
-rw-r--r--src/services/api/graphql/graphql/directives.py32
-rw-r--r--src/services/api/graphql/graphql/mutations.py109
-rw-r--r--src/services/api/graphql/graphql/schema/config_file.graphql27
-rw-r--r--src/services/api/graphql/graphql/schema/dhcp_server.graphql35
-rw-r--r--src/services/api/graphql/graphql/schema/interface_ethernet.graphql18
-rw-r--r--src/services/api/graphql/graphql/schema/schema.graphql18
-rw-r--r--src/services/api/graphql/recipes/__init__.py0
-rw-r--r--src/services/api/graphql/recipes/config_file.py16
-rw-r--r--src/services/api/graphql/recipes/dhcp_server.py13
-rw-r--r--src/services/api/graphql/recipes/interface_ethernet.py13
-rw-r--r--src/services/api/graphql/recipes/recipe.py68
-rw-r--r--src/services/api/graphql/recipes/templates/dhcp_server.tmpl9
-rw-r--r--src/services/api/graphql/recipes/templates/interface_ethernet.tmpl5
-rw-r--r--src/services/api/graphql/state.py4
-rwxr-xr-xsrc/services/vyos-configd7
-rwxr-xr-xsrc/services/vyos-http-api-server628
-rwxr-xr-xsrc/system/keepalived-fifo.py91
-rwxr-xr-xsrc/system/on-dhcp-event.sh25
-rw-r--r--src/systemd/vyos-http-api.service3
-rwxr-xr-xsrc/validators/allowed-vlan19
-rwxr-xr-xsrc/validators/base6427
-rwxr-xr-xsrc/validators/script9
72 files changed, 2387 insertions, 1194 deletions
diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py
index cdee72e09..a8cef5ebf 100755
--- a/src/conf_mode/dhcp_server.py
+++ b/src/conf_mode/dhcp_server.py
@@ -148,20 +148,35 @@ def verify(dhcp):
'At least one DHCP shared network must be configured.')
# Inspect shared-network/subnet
- failover_names = []
listen_ok = False
subnets = []
+ failover_ok = False
+ shared_networks = len(dhcp['shared_network_name'])
+ disabled_shared_networks = 0
+
# A shared-network requires a subnet definition
for network, network_config in dhcp['shared_network_name'].items():
+ if 'disable' in network_config:
+ disabled_shared_networks += 1
+
if 'subnet' not in network_config:
raise ConfigError(f'No subnets defined for {network}. At least one\n' \
'lease subnet must be configured.')
for subnet, subnet_config in network_config['subnet'].items():
- if 'static_route' in subnet_config and len(subnet_config['static_route']) != 2:
- raise ConfigError('Missing DHCP static-route parameter(s):\n' \
- 'destination-subnet | router must be defined!')
+ # All delivered static routes require a next-hop to be set
+ if 'static_route' in subnet_config:
+ for route, route_option in subnet_config['static_route'].items():
+ if 'next_hop' not in route_option:
+ raise ConfigError(f'DHCP static-route "{route}" requires router to be defined!')
+
+ # DHCP failover needs at least one subnet that uses it
+ if 'enable_failover' in subnet_config:
+ if 'failover' not in dhcp:
+ raise ConfigError(f'Can not enable failover for "{subnet}" in "{network}".\n' \
+ 'Failover is not configured globally!')
+ failover_ok = True
# Check if DHCP address range is inside configured subnet declaration
if 'range' in subnet_config:
@@ -191,23 +206,6 @@ def verify(dhcp):
tmp = IPRange(range_config['start'], range_config['stop'])
networks.append(tmp)
- if 'failover' in subnet_config:
- for key in ['local_address', 'peer_address', 'name', 'status']:
- if key not in subnet_config['failover']:
- raise ConfigError(f'Missing DHCP failover parameter "{key}"!')
-
- # Failover names must be uniquie
- if subnet_config['failover']['name'] in failover_names:
- name = subnet_config['failover']['name']
- raise ConfigError(f'DHCP failover names must be unique:\n' \
- f'{name} has already been configured!')
- failover_names.append(subnet_config['failover']['name'])
-
- # Failover requires start/stop ranges for pool
- if 'range' not in subnet_config:
- raise ConfigError(f'DHCP failover requires at least one start-stop range to be configured\n'\
- f'within shared-network "{network}, {subnet}" for using failover!')
-
# Exclude addresses must be in bound
if 'exclude' in subnet_config:
for exclude in subnet_config['exclude']:
@@ -234,7 +232,7 @@ def verify(dhcp):
# There must be one subnet connected to a listen interface.
# This only counts if the network itself is not disabled!
if 'disable' not in network_config:
- if is_subnet_connected(subnet, primary=True):
+ if is_subnet_connected(subnet, primary=False):
listen_ok = True
# Subnets must be non overlapping
@@ -251,6 +249,19 @@ def verify(dhcp):
if net.overlaps(net2):
raise ConfigError('Conflicting subnet ranges: "{net}" overlaps "{net2}"!')
+ # Prevent 'disable' for shared-network if only one network is configured
+ if (shared_networks - disabled_shared_networks) < 1:
+ raise ConfigError(f'At least one shared network must be active!')
+
+ if 'failover' in dhcp:
+ if not failover_ok:
+ raise ConfigError('DHCP failover must be enabled for at least one subnet!')
+
+ for key in ['name', 'remote', 'source_address', 'status']:
+ if key not in dhcp['failover']:
+ tmp = key.replace('_', '-')
+ raise ConfigError(f'DHCP failover requires "{tmp}" to be specified!')
+
for address in (dict_search('listen_address', dhcp) or []):
if is_addr_assigned(address):
listen_ok = True
diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py
index 175300bb0..e6a2e4486 100755
--- a/src/conf_mode/dhcpv6_server.py
+++ b/src/conf_mode/dhcpv6_server.py
@@ -128,7 +128,7 @@ def verify(dhcpv6):
# Subnets must be unique
if subnet in subnets:
- raise ConfigError('DHCPv6 subnets must be unique! Subnet {0} defined multiple times!'.format(subnet['network']))
+ raise ConfigError(f'DHCPv6 subnets must be unique! Subnet {subnet} defined multiple times!')
subnets.append(subnet)
# DHCPv6 requires at least one configured address range or one static mapping
diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py
index c44e6c974..06366362a 100755
--- a/src/conf_mode/dns_forwarding.py
+++ b/src/conf_mode/dns_forwarding.py
@@ -66,21 +66,6 @@ def get_config(config=None):
if conf.exists(base_nameservers_dhcp):
dns.update({'system_name_server_dhcp': conf.return_values(base_nameservers_dhcp)})
- # Split the source_address property into separate IPv4 and IPv6 lists
- # NOTE: In future versions of pdns-recursor (> 4.4.0), this logic can be removed
- # as both IPv4 and IPv6 addresses can be specified in a single setting.
- source_address_v4 = []
- source_address_v6 = []
-
- for source_address in dns['source_address']:
- if is_ipv6(source_address):
- source_address_v6.append(source_address)
- else:
- source_address_v4.append(source_address)
-
- dns.update({'source_address_v4': source_address_v4})
- dns.update({'source_address_v6': source_address_v6})
-
return dns
def verify(dns):
diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py
index 9cae29481..0a4559ade 100755
--- a/src/conf_mode/flow_accounting_conf.py
+++ b/src/conf_mode/flow_accounting_conf.py
@@ -306,7 +306,7 @@ def verify(config):
source_ip_presented = True
break
if not source_ip_presented:
- raise ConfigError("Your \"netflow source-ip\" does not exist in the system")
+ print("Warning: your \"netflow source-ip\" does not exist in the system")
# check if engine-id compatible with selected protocol version
if config['netflow']['engine-id']:
diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py
index f4c75c257..a7135911d 100755
--- a/src/conf_mode/host_name.py
+++ b/src/conf_mode/host_name.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
+# Copyright (C) 2018-2021 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -14,10 +14,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/>.
-"""
-conf-mode script for 'system host-name' and 'system domain-name'.
-"""
-
import re
import sys
import copy
@@ -25,10 +21,13 @@ import copy
import vyos.util
import vyos.hostsd_client
-from vyos.config import Config
from vyos import ConfigError
-from vyos.util import cmd, call, process_named_running
-
+from vyos.config import Config
+from vyos.ifconfig import Section
+from vyos.template import is_ip
+from vyos.util import cmd
+from vyos.util import call
+from vyos.util import process_named_running
from vyos import airbag
airbag.enable()
@@ -37,7 +36,7 @@ default_config_data = {
'domain_name': '',
'domain_search': [],
'nameserver': [],
- 'nameservers_dhcp_interfaces': [],
+ 'nameservers_dhcp_interfaces': {},
'static_host_mapping': {}
}
@@ -51,29 +50,37 @@ def get_config(config=None):
hosts = copy.deepcopy(default_config_data)
- hosts['hostname'] = conf.return_value("system host-name")
+ hosts['hostname'] = conf.return_value(['system', 'host-name'])
# This may happen if the config is not loaded yet,
# e.g. if run by cloud-init
if not hosts['hostname']:
hosts['hostname'] = default_config_data['hostname']
- if conf.exists("system domain-name"):
- hosts['domain_name'] = conf.return_value("system domain-name")
+ if conf.exists(['system', 'domain-name']):
+ hosts['domain_name'] = conf.return_value(['system', 'domain-name'])
hosts['domain_search'].append(hosts['domain_name'])
- for search in conf.return_values("system domain-search domain"):
+ for search in conf.return_values(['system', 'domain-search', 'domain']):
hosts['domain_search'].append(search)
- hosts['nameserver'] = conf.return_values("system name-server")
+ if conf.exists(['system', 'name-server']):
+ for ns in conf.return_values(['system', 'name-server']):
+ if is_ip(ns):
+ hosts['nameserver'].append(ns)
+ else:
+ tmp = ''
+ if_type = Section.section(ns)
+ if conf.exists(['interfaces', if_type, ns, 'address']):
+ tmp = conf.return_values(['interfaces', if_type, ns, 'address'])
- hosts['nameservers_dhcp_interfaces'] = conf.return_values("system name-servers-dhcp")
+ hosts['nameservers_dhcp_interfaces'].update({ ns : tmp })
# system static-host-mapping
- for hn in conf.list_nodes('system static-host-mapping host-name'):
+ for hn in conf.list_nodes(['system', 'static-host-mapping', 'host-name']):
hosts['static_host_mapping'][hn] = {}
- hosts['static_host_mapping'][hn]['address'] = conf.return_value(f'system static-host-mapping host-name {hn} inet')
- hosts['static_host_mapping'][hn]['aliases'] = conf.return_values(f'system static-host-mapping host-name {hn} alias')
+ hosts['static_host_mapping'][hn]['address'] = conf.return_value(['system', 'static-host-mapping', 'host-name', hn, 'inet'])
+ hosts['static_host_mapping'][hn]['aliases'] = conf.return_values(['system', 'static-host-mapping', 'host-name', hn, 'alias'])
return hosts
@@ -103,8 +110,10 @@ def verify(hosts):
if not hostname_regex.match(a) and len(a) != 0:
raise ConfigError(f'Invalid alias "{a}" in static-host-mapping "{host}"')
- # TODO: add warnings for nameservers_dhcp_interfaces if interface doesn't
- # exist or doesn't have address dhcp(v6)
+ for interface, interface_config in hosts['nameservers_dhcp_interfaces'].items():
+ # Warnin user if interface does not have DHCP or DHCPv6 configured
+ if not set(interface_config).intersection(['dhcp', 'dhcpv6']):
+ print(f'WARNING: "{interface}" is not a DHCP interface but uses DHCP name-server option!')
return None
diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py
index 472eb77e4..7e4b117c8 100755
--- a/src/conf_mode/http-api.py
+++ b/src/conf_mode/http-api.py
@@ -19,6 +19,7 @@
import sys
import os
import json
+import time
from copy import deepcopy
import vyos.defaults
@@ -34,11 +35,6 @@ config_file = '/etc/vyos/http-api.conf'
vyos_conf_scripts_dir=vyos.defaults.directories['conf_mode']
-# XXX: this model will need to be extended for tag nodes
-dependencies = [
- 'https.py',
-]
-
def get_config(config=None):
http_api = deepcopy(vyos.defaults.api_data)
x = http_api.get('api_keys')
@@ -103,8 +99,10 @@ def apply(http_api):
else:
call('systemctl stop vyos-http-api.service')
- for dep in dependencies:
- cmd(f'{vyos_conf_scripts_dir}/{dep}', raising=ConfigError)
+ # Let uvicorn settle before restarting Nginx
+ time.sleep(2)
+
+ cmd(f'{vyos_conf_scripts_dir}/https.py', raising=ConfigError)
if __name__ == '__main__':
try:
diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py
index 5b0046a72..4d3ebc587 100755
--- a/src/conf_mode/interfaces-bridge.py
+++ b/src/conf_mode/interfaces-bridge.py
@@ -18,7 +18,6 @@ import os
from sys import exit
from netifaces import interfaces
-import re
from vyos.config import Config
from vyos.configdict import get_interface_dict
@@ -41,26 +40,6 @@ from vyos import ConfigError
from vyos import airbag
airbag.enable()
-def helper_check_removed_vlan(conf,bridge,key,key_mangling):
- key_update = re.sub(key_mangling[0], key_mangling[1], key)
- if dict_search('member.interface', bridge):
- for interface in bridge['member']['interface']:
- tmp = leaf_node_changed(conf, ['member', 'interface',interface,key])
- if tmp:
- if 'member' in bridge:
- if 'interface' in bridge['member']:
- if interface in bridge['member']['interface']:
- bridge['member']['interface'][interface].update({f'{key_update}_removed': tmp })
- else:
- bridge['member']['interface'].update({interface: {f'{key_update}_removed': tmp }})
- else:
- bridge['member'].update({ 'interface': {interface: {f'{key_update}_removed': tmp }}})
- else:
- bridge.update({'member': { 'interface': {interface: {f'{key_update}_removed': tmp }}}})
-
- return bridge
-
-
def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
@@ -80,12 +59,6 @@ def get_config(config=None):
bridge['member'].update({'interface_remove': tmp })
else:
bridge.update({'member': {'interface_remove': tmp }})
-
-
- # determine which members vlan have been removed
-
- bridge = helper_check_removed_vlan(conf,bridge,'native-vlan',('-', '_'))
- bridge = helper_check_removed_vlan(conf,bridge,'allowed-vlan',('-', '_'))
if dict_search('member.interface', bridge):
# XXX: T2665: we need a copy of the dict keys for iteration, else we will get:
@@ -99,7 +72,6 @@ def get_config(config=None):
# the default dictionary is not properly paged into the dict (see T2665)
# thus we will ammend it ourself
default_member_values = defaults(base + ['member', 'interface'])
- vlan_aware = False
for interface,interface_config in bridge['member']['interface'].items():
bridge['member']['interface'][interface] = dict_merge(
default_member_values, bridge['member']['interface'][interface])
@@ -120,19 +92,11 @@ def get_config(config=None):
# Bridge members must not have an assigned address
tmp = has_address_configured(conf, interface)
if tmp: bridge['member']['interface'][interface].update({'has_address' : ''})
-
+
# VLAN-aware bridge members must not have VLAN interface configuration
- if 'native_vlan' in interface_config:
- vlan_aware = True
-
- if 'allowed_vlan' in interface_config:
- vlan_aware = True
-
-
- if vlan_aware:
- tmp = has_vlan_subinterface_configured(conf,interface)
- if tmp:
- if tmp: bridge['member']['interface'][interface].update({'has_vlan' : ''})
+ tmp = has_vlan_subinterface_configured(conf,interface)
+ if 'enable_vlan' in bridge and tmp:
+ bridge['member']['interface'][interface].update({'has_vlan' : ''})
return bridge
@@ -142,8 +106,8 @@ def verify(bridge):
verify_dhcpv6(bridge)
verify_vrf(bridge)
-
- vlan_aware = False
+
+ ifname = bridge['ifname']
if dict_search('member.interface', bridge):
for interface, interface_config in bridge['member']['interface'].items():
@@ -166,31 +130,24 @@ def verify(bridge):
if 'has_address' in interface_config:
raise ConfigError(error_msg + 'it has an address assigned!')
-
- if 'has_vlan' in interface_config:
- raise ConfigError(error_msg + 'it has an VLAN subinterface assigned!')
-
- # VLAN-aware bridge members must not have VLAN interface configuration
- if 'native_vlan' in interface_config:
- vlan_aware = True
-
- if 'allowed_vlan' in interface_config:
- vlan_aware = True
-
- if vlan_aware and 'wlan' in interface:
- raise ConfigError(error_msg + 'VLAN aware cannot be set!')
-
- if 'allowed_vlan' in interface_config:
- for vlan in interface_config['allowed_vlan']:
- if re.search('[0-9]{1,4}-[0-9]{1,4}', vlan):
- vlan_range = vlan.split('-')
- if int(vlan_range[0]) <1 and int(vlan_range[0])>4094:
- raise ConfigError('VLAN ID must be between 1 and 4094')
- if int(vlan_range[1]) <1 and int(vlan_range[1])>4094:
- raise ConfigError('VLAN ID must be between 1 and 4094')
- else:
- if int(vlan) <1 and int(vlan)>4094:
- raise ConfigError('VLAN ID must be between 1 and 4094')
+
+ if 'enable_vlan' in bridge:
+ if 'has_vlan' in interface_config:
+ raise ConfigError(error_msg + 'it has an VLAN subinterface assigned!')
+
+ if 'wlan' in interface:
+ raise ConfigError(error_msg + 'VLAN aware cannot be set!')
+ else:
+ for option in ['allowed_vlan', 'native_vlan']:
+ if option in interface_config:
+ raise ConfigError('Can not use VLAN options on non VLAN aware bridge')
+
+ if 'enable_vlan' in bridge:
+ if dict_search('vif.1', bridge):
+ raise ConfigError(f'VLAN 1 sub interface cannot be set for VLAN aware bridge {ifname}, and VLAN 1 is always the parent interface')
+ else:
+ if dict_search('vif', bridge):
+ raise ConfigError(f'You must first activate "enable-vlan" of {ifname} bridge to use "vif"')
return None
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index 349b0e7a3..de851262b 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -62,12 +62,6 @@ def verify(ethernet):
ifname = ethernet['ifname']
verify_interface_exists(ifname)
-
- # No need to check speed and duplex keys as both have default values.
- if ((ethernet['speed'] == 'auto' and ethernet['duplex'] != 'auto') or
- (ethernet['speed'] != 'auto' and ethernet['duplex'] == 'auto')):
- raise ConfigError('Speed/Duplex missmatch. Must be both auto or manually configured')
-
verify_mtu(ethernet)
verify_mtu_ipv6(ethernet)
verify_dhcpv6(ethernet)
@@ -76,25 +70,31 @@ def verify(ethernet):
verify_eapol(ethernet)
verify_mirror(ethernet)
- # verify offloading capabilities
- if dict_search('offload.rps', ethernet) != None:
- if not os.path.exists(f'/sys/class/net/{ifname}/queues/rx-0/rps_cpus'):
- raise ConfigError('Interface does not suport RPS!')
+ ethtool = Ethtool(ifname)
+ # No need to check speed and duplex keys as both have default values.
+ if ((ethernet['speed'] == 'auto' and ethernet['duplex'] != 'auto') or
+ (ethernet['speed'] != 'auto' and ethernet['duplex'] == 'auto')):
+ raise ConfigError('Speed/Duplex missmatch. Must be both auto or manually configured')
- driver = EthernetIf(ifname).get_driver_name()
- # T3342 - Xen driver requires special treatment
- if driver == 'vif':
- if int(ethernet['mtu']) > 1500 and dict_search('offload.sg', ethernet) == None:
- raise ConfigError('Xen netback drivers requires scatter-gatter offloading '\
- 'for MTU size larger then 1500 bytes')
+ if ethernet['speed'] != 'auto' and ethernet['duplex'] != 'auto':
+ # We need to verify if the requested speed and duplex setting is
+ # supported by the underlaying NIC.
+ speed = ethernet['speed']
+ duplex = ethernet['duplex']
+ if not ethtool.check_speed_duplex(speed, duplex):
+ raise ConfigError(f'Adapter does not support changing speed and duplex '\
+ f'settings to: {speed}/{duplex}!')
+
+ if 'disable_flow_control' in ethernet:
+ if not ethtool.check_flow_control():
+ raise ConfigError('Adapter does not support changing flow-control settings!')
- ethtool = Ethtool(ifname)
if 'ring_buffer' in ethernet:
- max_rx = ethtool.get_rx_buffer()
+ max_rx = ethtool.get_ring_buffer_max('rx')
if not max_rx:
raise ConfigError('Driver does not support RX ring-buffer configuration!')
- max_tx = ethtool.get_tx_buffer()
+ max_tx = ethtool.get_ring_buffer_max('tx')
if not max_tx:
raise ConfigError('Driver does not support TX ring-buffer configuration!')
@@ -108,6 +108,18 @@ def verify(ethernet):
raise ConfigError(f'Driver only supports a maximum TX ring-buffer '\
f'size of "{max_tx}" bytes!')
+ # verify offloading capabilities
+ if dict_search('offload.rps', ethernet) != None:
+ if not os.path.exists(f'/sys/class/net/{ifname}/queues/rx-0/rps_cpus'):
+ raise ConfigError('Interface does not suport RPS!')
+
+ driver = ethtool.get_driver_name()
+ # T3342 - Xen driver requires special treatment
+ if driver == 'vif':
+ if int(ethernet['mtu']) > 1500 and dict_search('offload.sg', ethernet) == None:
+ raise ConfigError('Xen netback drivers requires scatter-gatter offloading '\
+ 'for MTU size larger then 1500 bytes')
+
if {'is_bond_member', 'mac'} <= set(ethernet):
print(f'WARNING: changing mac address "{mac}" will be ignored as "{ifname}" '
f'is a member of bond "{is_bond_member}"'.format(**ethernet))
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 0a420f7bf..ae35ed3c4 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -40,6 +40,7 @@ from vyos.util import call
from vyos.util import chown
from vyos.util import chmod_600
from vyos.util import dict_search
+from vyos.util import makedir
from vyos.validate import is_addr_assigned
from vyos import ConfigError
@@ -50,6 +51,7 @@ user = 'openvpn'
group = 'openvpn'
cfg_file = '/run/openvpn/{ifname}.conf'
+service_file = '/run/systemd/system/openvpn@{ifname}.service.d/20-override.conf'
def checkCertHeader(header, filename):
"""
@@ -79,9 +81,6 @@ def get_config(config=None):
openvpn = get_interface_dict(conf, base)
openvpn['auth_user_pass_file'] = '/run/openvpn/{ifname}.pw'.format(**openvpn)
- openvpn['daemon_user'] = user
- openvpn['daemon_group'] = group
-
return openvpn
def verify(openvpn):
@@ -425,6 +424,10 @@ def verify(openvpn):
def generate(openvpn):
interface = openvpn['ifname']
directory = os.path.dirname(cfg_file.format(**openvpn))
+ # create base config directory on demand
+ makedir(directory, user, group)
+ # enforce proper permissions on /run/openvpn
+ chown(directory, user, group)
# we can't know in advance which clients have been removed,
# thus all client configs will be removed and re-added on demand
@@ -432,22 +435,28 @@ def generate(openvpn):
if os.path.isdir(ccd_dir):
rmtree(ccd_dir, ignore_errors=True)
+ # Remove systemd directories with overrides
+ service_dir = os.path.dirname(service_file.format(**openvpn))
+ if os.path.isdir(service_dir):
+ rmtree(service_dir, ignore_errors=True)
+
if 'deleted' in openvpn or 'disable' in openvpn:
return None
# create client config directory on demand
- if not os.path.exists(ccd_dir):
- os.makedirs(ccd_dir, 0o755)
- chown(ccd_dir, user, group)
+ makedir(ccd_dir, user, group)
- # Fix file permissons for keys
- fix_permissions = []
+ # Fix file permissons for site2site shared secret
+ if dict_search('shared_secret_key_file', openvpn):
+ chmod_600(openvpn['shared_secret_key_file'])
+ chown(openvpn['shared_secret_key_file'], user, group)
- tmp = dict_search('shared_secret_key_file', openvpn)
- if tmp: fix_permissions.append(openvpn['shared_secret_key_file'])
-
- tmp = dict_search('tls.key_file', openvpn)
- if tmp: fix_permissions.append(tmp)
+ # Fix file permissons for TLS certificate and keys
+ for tls in ['auth_file', 'ca_cert_file', 'cert_file', 'crl_file',
+ 'crypt_file', 'dh_file', 'key_file']:
+ if dict_search(f'tls.{tls}', openvpn):
+ chmod_600(openvpn['tls'][tls])
+ chown(openvpn['tls'][tls], user, group)
# Generate User/Password authentication file
if 'authentication' in openvpn:
@@ -474,18 +483,20 @@ def generate(openvpn):
render(cfg_file.format(**openvpn), 'openvpn/server.conf.tmpl', openvpn,
formater=lambda _: _.replace("&quot;", '"'), user=user, group=group)
- # Fixup file permissions
- for file in fix_permissions:
- chmod_600(file)
+ # Render 20-override.conf for OpenVPN service
+ render(service_file.format(**openvpn), 'openvpn/service-override.conf.tmpl', openvpn,
+ formater=lambda _: _.replace("&quot;", '"'), user=user, group=group)
+ # Reload systemd services config to apply an override
+ call(f'systemctl daemon-reload')
return None
def apply(openvpn):
interface = openvpn['ifname']
- call(f'systemctl stop openvpn@{interface}.service')
# Do some cleanup when OpenVPN is disabled/deleted
if 'deleted' in openvpn or 'disable' in openvpn:
+ call(f'systemctl stop openvpn@{interface}.service')
for cleanup_file in glob(f'/run/openvpn/{interface}.*'):
if os.path.isfile(cleanup_file):
os.unlink(cleanup_file)
@@ -497,7 +508,7 @@ def apply(openvpn):
# No matching OpenVPN process running - maybe it got killed or none
# existed - nevertheless, spawn new OpenVPN process
- call(f'systemctl start openvpn@{interface}.service')
+ call(f'systemctl reload-or-restart openvpn@{interface}.service')
conf = VTunIf.get_config()
conf['device_type'] = openvpn['device_type']
diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
index e5958e9ae..2798d321f 100755
--- a/src/conf_mode/interfaces-tunnel.py
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -18,6 +18,7 @@ import os
from sys import exit
from netifaces import interfaces
+from ipaddress import IPv4Address
from vyos.config import Config
from vyos.configdict import dict_merge
@@ -31,6 +32,7 @@ from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_vrf
from vyos.configverify import verify_tunnel
from vyos.ifconfig import Interface
+from vyos.ifconfig import Section
from vyos.ifconfig import TunnelIf
from vyos.template import is_ipv4
from vyos.template import is_ipv6
@@ -74,6 +76,41 @@ def verify(tunnel):
verify_tunnel(tunnel)
+ # If tunnel source address any and key not set
+ if tunnel['encapsulation'] in ['gre'] and \
+ dict_search('source_address', tunnel) == '0.0.0.0' and \
+ dict_search('parameters.ip.key', tunnel) == None:
+ raise ConfigError('Tunnel parameters ip key must be set!')
+
+ if tunnel['encapsulation'] in ['gre', 'gretap']:
+ if dict_search('parameters.ip.key', tunnel) != None:
+ # Check pairs tunnel source-address/encapsulation/key with exists tunnels.
+ # Prevent the same key for 2 tunnels with same source-address/encap. T2920
+ for tunnel_if in Section.interfaces('tunnel'):
+ # It makes no sense to run the test for re-used GRE keys on our
+ # own interface we are currently working on
+ if tunnel['ifname'] == tunnel_if:
+ continue
+ tunnel_cfg = get_interface_config(tunnel_if)
+ # no match on encapsulation - bail out
+ if dict_search('linkinfo.info_kind', tunnel_cfg) != tunnel['encapsulation']:
+ continue
+ new_source_address = dict_search('source_address', tunnel)
+ # Convert tunnel key to ip key, format "ip -j link show"
+ # 1 => 0.0.0.1, 999 => 0.0.3.231
+ orig_new_key = dict_search('parameters.ip.key', tunnel)
+ new_key = IPv4Address(int(orig_new_key))
+ new_key = str(new_key)
+ if dict_search('address', tunnel_cfg) == new_source_address and \
+ dict_search('linkinfo.info_data.ikey', tunnel_cfg) == new_key:
+ raise ConfigError(f'Key "{orig_new_key}" for source-address "{new_source_address}" ' \
+ f'is already used for tunnel "{tunnel_if}"!')
+
+ # Keys are not allowed with ipip and sit tunnels
+ if tunnel['encapsulation'] in ['ipip', 'sit']:
+ if dict_search('parameters.ip.key', tunnel) != None:
+ raise ConfigError('Keys are not allowed with ipip and sit tunnels!')
+
verify_mtu_ipv6(tunnel)
verify_address(tunnel)
verify_vrf(tunnel)
diff --git a/src/conf_mode/interfaces-wwan.py b/src/conf_mode/interfaces-wwan.py
index 31c599145..cb46b3723 100755
--- a/src/conf_mode/interfaces-wwan.py
+++ b/src/conf_mode/interfaces-wwan.py
@@ -25,7 +25,9 @@ from vyos.configverify import verify_interface_exists
from vyos.configverify import verify_vrf
from vyos.ifconfig import WWANIf
from vyos.util import cmd
+from vyos.util import call
from vyos.util import dict_search
+from vyos.util import DEVNULL
from vyos.template import render
from vyos import ConfigError
from vyos import airbag
@@ -89,7 +91,7 @@ def apply(wwan):
options += ',user={user},password={password}'.format(**wwan['authentication'])
command = f'{base_cmd} --simple-connect="{options}"'
- cmd(command)
+ call(command, stdout=DEVNULL)
w.update(wwan)
return None
diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py
index 7ca2d9b44..771f635a0 100755
--- a/src/conf_mode/ipsec-settings.py
+++ b/src/conf_mode/ipsec-settings.py
@@ -18,7 +18,9 @@ import re
import os
from time import sleep
-from sys import exit
+
+# Top level import so that configd can override it
+from sys import argv
from vyos.config import Config
from vyos import ConfigError
@@ -216,6 +218,20 @@ def generate(data):
remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_file)
remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_file)
+def is_charon_responsive():
+ # Check if charon responds to strokes
+ #
+ # Sometimes it takes time to fully initialize,
+ # so waiting for the process to come to live isn't always enough
+ #
+ # There's no official "no-op" stroke so we use the "memusage" stroke as a substitute
+ from os import system
+ res = system("ipsec stroke memusage >&/dev/null")
+ if res == 0:
+ return True
+ else:
+ return False
+
def restart_ipsec():
try:
# Restart the IPsec daemon when it's running.
@@ -223,17 +239,28 @@ def restart_ipsec():
# there's a chance that this script will run before charon is up,
# so we can't assume it's running and have to check and wait if needed.
- # First, wait for charon to get started by the old ipsec.pl script.
+ # But before everything else, there's a catch!
+ # This script is run from _two_ places: "vpn ipsec options" and the top level "vpn" node
+ # When IPsec isn't set up yet, and a user wants to commit an IPsec config with some
+ # "vpn ipsec settings", this script will first be called before StrongSWAN is started by vpn-config.pl!
+ # Thus if this script is run from "settings" _and_ charon is unresponsive,
+ # we shouldn't wait for it, else there will be a deadlock.
+ # We indicate that by running the script under vyshim from "vpn ipsec options" (which sets a variable named "argv")
+ # and running it without configd from "vpn ipsec"
+ if "from-options" in argv:
+ if not is_charon_responsive():
+ return
+
+ # If we got this far, then we actually need to restart StrongSWAN
+
+ # First, wait for charon to get started by the old vpn-config.pl script.
from time import sleep, time
from os import system
now = time()
while True:
if (time() - now) > 60:
raise OSError("Timeout waiting for the IPsec process to become responsive")
- # There's no oficial "no-op" stroke,
- # so we use memusage to check if charon is alive and responsive
- res = system("ipsec stroke memusage >&/dev/null")
- if res == 0:
+ if is_charon_responsive():
break
sleep(5)
diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py
index da91f3b11..0c179b724 100755
--- a/src/conf_mode/protocols_isis.py
+++ b/src/conf_mode/protocols_isis.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-2021 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
@@ -19,12 +19,16 @@ import os
from sys import exit
from vyos.config import Config
+from vyos.configdict import dict_merge
from vyos.configdict import node_changed
-from vyos import ConfigError
-from vyos.util import call
+from vyos.configverify import verify_common_route_maps
+from vyos.configverify import verify_interface_exists
+from vyos.ifconfig import Interface
from vyos.util import dict_search
-from vyos.template import render
+from vyos.util import get_interface_config
from vyos.template import render_to_string
+from vyos.xml import defaults
+from vyos import ConfigError
from vyos import frr
from vyos import airbag
airbag.enable()
@@ -34,126 +38,172 @@ def get_config(config=None):
conf = config
else:
conf = Config()
- base = ['protocols', 'isis']
- isis = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ base = ['protocols', 'isis']
+ isis = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True)
+
+ interfaces_removed = node_changed(conf, base + ['interface'])
+ if interfaces_removed:
+ isis['interface_removed'] = list(interfaces_removed)
+
+ # Bail out early if configuration tree does not exist
+ if not conf.exists(base):
+ isis.update({'deleted' : ''})
+ return isis
+
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ # XXX: Note that we can not call defaults(base), as defaults does not work
+ # on an instance of a tag node.
+ default_values = defaults(base)
+ # merge in default values
+ isis = dict_merge(default_values, isis)
+
+ # We also need some additional information from the config, prefix-lists
+ # and route-maps for instance. They will be used in verify().
+ #
+ # XXX: one MUST always call this without the key_mangling() option! See
+ # vyos.configverify.verify_common_route_maps() for more information.
+ tmp = conf.get_config_dict(['policy'])
+ # Merge policy dict into "regular" config dict
+ isis = dict_merge(tmp, isis)
return isis
def verify(isis):
# bail out early - looks like removal from running config
- if not isis:
+ if not isis or 'deleted' in isis:
return None
- for process, isis_config in isis.items():
- # If more then one isis process is defined (Frr only supports one)
- # http://docs.frrouting.org/en/latest/isisd.html#isis-router
- if len(isis) > 1:
- raise ConfigError('Only one isis process can be defined')
-
- # If network entity title (net) not defined
- if 'net' not in isis_config:
- raise ConfigError('ISIS net format iso is mandatory!')
-
- # If interface not set
- if 'interface' not in isis_config:
- raise ConfigError('ISIS interface is mandatory!')
-
- # If md5 and plaintext-password set at the same time
- if 'area_password' in isis_config:
- if {'md5', 'plaintext_password'} <= set(isis_config['encryption']):
- raise ConfigError('Can not use both md5 and plaintext-password for ISIS area-password!')
-
- # If one param from delay set, but not set others
- if 'spf_delay_ietf' in isis_config:
- required_timers = ['holddown', 'init_delay', 'long_delay', 'short_delay', 'time_to_learn']
- exist_timers = []
- for elm_timer in required_timers:
- if elm_timer in isis_config['spf_delay_ietf']:
- exist_timers.append(elm_timer)
-
- exist_timers = set(required_timers).difference(set(exist_timers))
- if len(exist_timers) > 0:
- raise ConfigError('All types of delay must be specified: ' + ', '.join(exist_timers).replace('_', '-'))
-
- # If Redistribute set, but level don't set
- if 'redistribute' in isis_config:
- proc_level = isis_config.get('level','').replace('-','_')
- for proto, proto_config in isis_config.get('redistribute', {}).get('ipv4', {}).items():
+ if 'net' not in isis:
+ raise ConfigError('Network entity is mandatory!')
+
+ # last byte in IS-IS area address must be 0
+ tmp = isis['net'].split('.')
+ if int(tmp[-1]) != 0:
+ raise ConfigError('Last byte of IS-IS network entity title must always be 0!')
+
+ verify_common_route_maps(isis)
+
+ # If interface not set
+ if 'interface' not in isis:
+ raise ConfigError('Interface used for routing updates is mandatory!')
+
+ for interface in isis['interface']:
+ verify_interface_exists(interface)
+ # Interface MTU must be >= configured lsp-mtu
+ mtu = Interface(interface).get_mtu()
+ area_mtu = isis['lsp_mtu']
+ # Recommended maximum PDU size = interface MTU - 3 bytes
+ recom_area_mtu = mtu - 3
+ if mtu < int(area_mtu) or int(area_mtu) > recom_area_mtu:
+ raise ConfigError(f'Interface {interface} has MTU {mtu}, ' \
+ f'current area MTU is {area_mtu}! \n' \
+ f'Recommended area lsp-mtu {recom_area_mtu} or less ' \
+ '(calculated on MTU size).')
+
+ # If md5 and plaintext-password set at the same time
+ for password in ['area_password', 'domain_password']:
+ if password in isis:
+ if {'md5', 'plaintext_password'} <= set(isis[password]):
+ tmp = password.replace('_', '-')
+ raise ConfigError(f'Can use either md5 or plaintext-password for {tmp}!')
+
+ # If one param from delay set, but not set others
+ if 'spf_delay_ietf' in isis:
+ required_timers = ['holddown', 'init_delay', 'long_delay', 'short_delay', 'time_to_learn']
+ exist_timers = []
+ for elm_timer in required_timers:
+ if elm_timer in isis['spf_delay_ietf']:
+ exist_timers.append(elm_timer)
+
+ exist_timers = set(required_timers).difference(set(exist_timers))
+ if len(exist_timers) > 0:
+ raise ConfigError('All types of delay must be specified: ' + ', '.join(exist_timers).replace('_', '-'))
+
+ # If Redistribute set, but level don't set
+ if 'redistribute' in isis:
+ proc_level = isis.get('level','').replace('-','_')
+ for afi in ['ipv4', 'ipv6']:
+ if afi not in isis['redistribute']:
+ continue
+
+ for proto, proto_config in isis['redistribute'][afi].items():
if 'level_1' not in proto_config and 'level_2' not in proto_config:
- raise ConfigError('Redistribute level-1 or level-2 should be specified in \"protocols isis {} redistribute ipv4 {}\"'.format(process, proto))
- for redistribute_level in proto_config.keys():
- if proc_level and proc_level != 'level_1_2' and proc_level != redistribute_level:
- raise ConfigError('\"protocols isis {0} redistribute ipv4 {2} {3}\" cannot be used with \"protocols isis {0} level {1}\"'.format(process, proc_level, proto, redistribute_level))
-
- # Segment routing checks
- if dict_search('segment_routing', isis_config):
- if dict_search('segment_routing.global_block', isis_config):
- high_label_value = dict_search('segment_routing.global_block.high_label_value', isis_config)
- low_label_value = dict_search('segment_routing.global_block.low_label_value', isis_config)
- # If segment routing global block high value is blank, throw error
- if low_label_value and not high_label_value:
- raise ConfigError('Segment routing global block high value must not be left blank')
- # If segment routing global block low value is blank, throw error
- if high_label_value and not low_label_value:
- raise ConfigError('Segment routing global block low value must not be left blank')
- # If segment routing global block low value is higher than the high value, throw error
- if int(low_label_value) > int(high_label_value):
- raise ConfigError('Segment routing global block low value must be lower than high value')
-
- if dict_search('segment_routing.local_block', isis_config):
- high_label_value = dict_search('segment_routing.local_block.high_label_value', isis_config)
- low_label_value = dict_search('segment_routing.local_block.low_label_value', isis_config)
- # If segment routing local block high value is blank, throw error
- if low_label_value and not high_label_value:
- raise ConfigError('Segment routing local block high value must not be left blank')
- # If segment routing local block low value is blank, throw error
- if high_label_value and not low_label_value:
- raise ConfigError('Segment routing local block low value must not be left blank')
- # If segment routing local block low value is higher than the high value, throw error
- if int(low_label_value) > int(high_label_value):
- raise ConfigError('Segment routing local block low value must be lower than high value')
+ raise ConfigError(f'Redistribute level-1 or level-2 should be specified in ' \
+ f'"protocols isis {process} redistribute {afi} {proto}"!')
+
+ for redistr_level, redistr_config in proto_config.items():
+ if proc_level and proc_level != 'level_1_2' and proc_level != redistr_level:
+ raise ConfigError(f'"protocols isis {process} redistribute {afi} {proto} {redistr_level}" ' \
+ f'can not be used with \"protocols isis {process} level {proc_level}\"')
+
+ # Segment routing checks
+ if dict_search('segment_routing.global_block', isis):
+ high_label_value = dict_search('segment_routing.global_block.high_label_value', isis)
+ low_label_value = dict_search('segment_routing.global_block.low_label_value', isis)
+
+ # If segment routing global block high value is blank, throw error
+ if (low_label_value and not high_label_value) or (high_label_value and not low_label_value):
+ raise ConfigError('Segment routing global block requires both low and high value!')
+
+ # If segment routing global block low value is higher than the high value, throw error
+ if int(low_label_value) > int(high_label_value):
+ raise ConfigError('Segment routing global block low value must be lower than high value')
+
+ if dict_search('segment_routing.local_block', isis):
+ high_label_value = dict_search('segment_routing.local_block.high_label_value', isis)
+ low_label_value = dict_search('segment_routing.local_block.low_label_value', isis)
+
+ # If segment routing local block high value is blank, throw error
+ if (low_label_value and not high_label_value) or (high_label_value and not low_label_value):
+ raise ConfigError('Segment routing local block requires both high and low value!')
+
+ # If segment routing local block low value is higher than the high value, throw error
+ if int(low_label_value) > int(high_label_value):
+ raise ConfigError('Segment routing local block low value must be lower than high value')
return None
def generate(isis):
- if not isis:
- isis['new_frr_config'] = ''
+ if not isis or 'deleted' in isis:
+ isis['frr_isisd_config'] = ''
+ isis['frr_zebra_config'] = ''
return None
- # only one ISIS process is supported, so we can directly send the first key
- # of the config dict
- process = list(isis.keys())[0]
- isis[process]['process'] = process
-
- isis['new_frr_config'] = render_to_string('frr/isisd.frr.tmpl',
- isis[process])
-
+ isis['protocol'] = 'isis' # required for frr/route-map.frr.tmpl
+ isis['frr_zebra_config'] = render_to_string('frr/route-map.frr.tmpl', isis)
+ isis['frr_isisd_config'] = render_to_string('frr/isisd.frr.tmpl', isis)
return None
def apply(isis):
+ isis_daemon = 'isisd'
+ zebra_daemon = 'zebra'
+
# Save original configuration prior to starting any commit actions
frr_cfg = frr.FRRConfig()
- frr_cfg.load_configuration(daemon='isisd')
- frr_cfg.modify_section(r'interface \S+', '')
- frr_cfg.modify_section(f'router isis \S+', '')
- frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', isis['new_frr_config'])
- frr_cfg.commit_configuration(daemon='isisd')
-
- # If FRR config is blank, rerun the blank commit x times due to frr-reload
- # behavior/bug not properly clearing out on one commit.
- if isis['new_frr_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(daemon='isisd')
-
- # Debugging
- '''
- print('')
- print('--------- DEBUGGING ----------')
- print(f'Existing config:\n{frr_cfg["original_config"]}\n\n')
- print(f'Replacement config:\n{isis["new_frr_config"]}\n\n')
- print(f'Modified config:\n{frr_cfg["modified_config"]}\n\n')
- '''
+
+ # The route-map used for the FIB (zebra) is part of the zebra daemon
+ frr_cfg.load_configuration(zebra_daemon)
+ frr_cfg.modify_section(r'(\s+)?ip protocol isis route-map [-a-zA-Z0-9.]+$', '', '(\s|!)')
+ frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', isis['frr_zebra_config'])
+ frr_cfg.commit_configuration(zebra_daemon)
+
+ frr_cfg.load_configuration(isis_daemon)
+ frr_cfg.modify_section(f'^router isis VyOS$', '')
+
+ for key in ['interface', 'interface_removed']:
+ if key not in isis:
+ continue
+ for interface in isis[key]:
+ frr_cfg.modify_section(f'^interface {interface}$', '')
+
+ frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', isis['frr_isisd_config'])
+ frr_cfg.commit_configuration(isis_daemon)
+
+ # Save configuration to /run/frr/config/frr.conf
+ frr.save_configuration()
return None
diff --git a/src/conf_mode/protocols_rip.py b/src/conf_mode/protocols_rip.py
index 8ddd705f2..f36abbf90 100755
--- a/src/conf_mode/protocols_rip.py
+++ b/src/conf_mode/protocols_rip.py
@@ -125,7 +125,7 @@ def get_config(config=None):
conf.set_level(base)
- # Get distribute list interface
+ # Get distribute list interface
for dist_iface in conf.list_nodes('distribute-list interface'):
# Set level 'distribute-list interface ethX'
conf.set_level(base + ['distribute-list', 'interface', dist_iface])
@@ -301,6 +301,7 @@ def apply(rip):
if os.path.exists(config_file):
call(f'vtysh -d ripd -f {config_file}')
+ call('sudo vtysh --writeconfig --noerror')
os.remove(config_file)
else:
print("File {0} not found".format(config_file))
diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py
index 3990e5735..0fbe90cce 100755
--- a/src/conf_mode/snmp.py
+++ b/src/conf_mode/snmp.py
@@ -20,13 +20,17 @@ from sys import exit
from vyos.config import Config
from vyos.configverify import verify_vrf
-from vyos.snmpv3_hashgen import plaintext_to_md5, plaintext_to_sha1, random
+from vyos.snmpv3_hashgen import plaintext_to_md5
+from vyos.snmpv3_hashgen import plaintext_to_sha1
+from vyos.snmpv3_hashgen import random
from vyos.template import render
from vyos.template import is_ipv4
-from vyos.util import call, chmod_755
+from vyos.util import call
+from vyos.util import chmod_755
from vyos.validate import is_addr_assigned
from vyos.version import get_version_data
-from vyos import ConfigError, airbag
+from vyos import ConfigError
+from vyos import airbag
airbag.enable()
config_file_client = r'/etc/snmp/snmp.conf'
@@ -401,19 +405,20 @@ def verify(snmp):
addr = listen[0]
port = listen[1]
+ tmp = None
if is_ipv4(addr):
# example: udp:127.0.0.1:161
- listen = 'udp:' + addr + ':' + port
+ tmp = f'udp:{addr}:{port}'
elif snmp['ipv6_enabled']:
# example: udp6:[::1]:161
- listen = 'udp6:' + '[' + addr + ']' + ':' + port
+ tmp = f'udp6:[{addr}]:{port}'
# We only wan't to configure addresses that exist on the system.
# Hint the user if they don't exist
if is_addr_assigned(addr):
- snmp['listen_on'].append(listen)
+ if tmp: snmp['listen_on'].append(tmp)
else:
- print('WARNING: SNMP listen address {0} not configured!'.format(addr))
+ print(f'WARNING: SNMP listen address {addr} not configured!')
verify_vrf(snmp)
diff --git a/src/conf_mode/system-login-banner.py b/src/conf_mode/system-login-banner.py
index 569010735..2220d7b66 100755
--- a/src/conf_mode/system-login-banner.py
+++ b/src/conf_mode/system-login-banner.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-2021 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,12 +22,13 @@ from vyos import airbag
airbag.enable()
motd="""
-The programs included with the Debian GNU/Linux system are free software;
-the exact distribution terms for each program are described in the
-individual files in /usr/share/doc/*/copyright.
+Check out project news at https://blog.vyos.io
+and feel free to report bugs at https://phabricator.vyos.net
-Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
-permitted by applicable law.
+You can change this banner using "set system login banner post-login" command.
+
+VyOS is a free software distribution that includes multiple components,
+you can check individual component licenses under /usr/share/doc/*/copyright
"""
@@ -36,7 +37,7 @@ PRELOGIN_NET_FILE = r'/etc/issue.net'
POSTLOGIN_FILE = r'/etc/motd'
default_config_data = {
- 'issue': 'Welcome to VyOS - \n \l\n',
+ 'issue': 'Welcome to VyOS - \\n \\l\n\n',
'issue_net': 'Welcome to VyOS\n',
'motd': motd
}
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index 59ea1d34b..8aa43dd32 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.py
@@ -59,7 +59,7 @@ def get_config(config=None):
conf = Config()
base = ['system', 'login']
login = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True)
+ no_tag_node_value_mangle=True, get_first_key=True)
# users no longer existing in the running configuration need to be deleted
local_users = get_local_users()
@@ -246,7 +246,9 @@ def apply(login):
# XXX: Should we deny using root at all?
home_dir = getpwnam(user).pw_dir
render(f'{home_dir}/.ssh/authorized_keys', 'login/authorized_keys.tmpl',
- user_config, permission=0o600, user=user, group='users')
+ user_config, permission=0o600,
+ formater=lambda _: _.replace("&quot;", '"'),
+ user=user, group='users')
except Exception as e:
raise ConfigError(f'Adding user "{user}" raised exception: "{e}"')
diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py
index 33a546bd3..19b252513 100755
--- a/src/conf_mode/system_console.py
+++ b/src/conf_mode/system_console.py
@@ -18,9 +18,14 @@ import os
import re
from vyos.config import Config
-from vyos.util import call, read_file, write_file
+from vyos.configdict import dict_merge
+from vyos.util import call
+from vyos.util import read_file
+from vyos.util import write_file
from vyos.template import render
-from vyos import ConfigError, airbag
+from vyos.xml import defaults
+from vyos import ConfigError
+from vyos import airbag
airbag.enable()
by_bus_dir = '/dev/serial/by-bus'
@@ -36,21 +41,27 @@ def get_config(config=None):
console = conf.get_config_dict(base, get_first_key=True)
# bail out early if no serial console is configured
- if 'device' not in console.keys():
+ if 'device' not in console:
return console
# convert CLI values to system values
- for device in console['device'].keys():
- # no speed setting has been configured - use default value
- if not 'speed' in console['device'][device].keys():
- tmp = { 'speed': '' }
- if device.startswith('hvc'):
- tmp['speed'] = 38400
- else:
- tmp['speed'] = 115200
+ default_values = defaults(base + ['device'])
+ for device, device_config in console['device'].items():
+ if 'speed' not in device_config and device.startswith('hvc'):
+ # XEN console has a different default console speed
+ console['device'][device]['speed'] = 38400
+ else:
+ # Merge in XML defaults - the proper way to do it
+ console['device'][device] = dict_merge(default_values,
+ console['device'][device])
+
+ return console
- console['device'][device].update(tmp)
+def verify(console):
+ if not console or 'device' not in console:
+ return None
+ for device in console['device']:
if device.startswith('usb'):
# It is much easiert to work with the native ttyUSBn name when using
# getty, but that name may change across reboots - depending on the
@@ -58,13 +69,13 @@ def get_config(config=None):
# to its dynamic device file - and create a new dict entry for it.
by_bus_device = f'{by_bus_dir}/{device}'
if os.path.isdir(by_bus_dir) and os.path.exists(by_bus_device):
- tmp = os.path.basename(os.readlink(by_bus_device))
- # updating the dict must come as last step in the loop!
- console['device'][tmp] = console['device'].pop(device)
+ device = os.path.basename(os.readlink(by_bus_device))
- return console
+ # If the device name still starts with usbXXX no matching tty was found
+ # and it can not be used as a serial interface
+ if device.startswith('usb'):
+ raise ConfigError(f'Device {device} does not support beeing used as tty')
-def verify(console):
return None
def generate(console):
@@ -76,20 +87,29 @@ def generate(console):
call(f'systemctl stop {basename}')
os.unlink(os.path.join(root, basename))
- if not console:
+ if not console or 'device' not in console:
return None
- for device in console['device'].keys():
+ for device, device_config in console['device'].items():
+ if device.startswith('usb'):
+ # It is much easiert to work with the native ttyUSBn name when using
+ # getty, but that name may change across reboots - depending on the
+ # amount of connected devices. We will resolve the fixed device name
+ # to its dynamic device file - and create a new dict entry for it.
+ by_bus_device = f'{by_bus_dir}/{device}'
+ if os.path.isdir(by_bus_dir) and os.path.exists(by_bus_device):
+ device = os.path.basename(os.readlink(by_bus_device))
+
config_file = base_dir + f'/serial-getty@{device}.service'
getty_wants_symlink = base_dir + f'/getty.target.wants/serial-getty@{device}.service'
- render(config_file, 'getty/serial-getty.service.tmpl', console['device'][device])
+ render(config_file, 'getty/serial-getty.service.tmpl', device_config)
os.symlink(config_file, getty_wants_symlink)
# GRUB
# For existing serial line change speed (if necessary)
# Only applys to ttyS0
- if 'ttyS0' not in console['device'].keys():
+ if 'ttyS0' not in console['device']:
return None
speed = console['device']['ttyS0']['speed']
@@ -98,7 +118,6 @@ def generate(console):
return None
lines = read_file(grub_config).split('\n')
-
p = re.compile(r'^(.* console=ttyS0),[0-9]+(.*)$')
write = False
newlines = []
@@ -122,9 +141,8 @@ def generate(console):
return None
def apply(console):
- # reset screen blanking
+ # Reset screen blanking
call('/usr/bin/setterm -blank 0 -powersave off -powerdown 0 -term linux </dev/tty1 >/dev/tty1 2>&1')
-
# Reload systemd manager configuration
call('systemctl daemon-reload')
@@ -136,11 +154,11 @@ def apply(console):
call('/usr/bin/setterm -blank 15 -powersave powerdown -powerdown 60 -term linux </dev/tty1 >/dev/tty1 2>&1')
# Start getty process on configured serial interfaces
- for device in console['device'].keys():
+ for device in console['device']:
# Only start console if it exists on the running system. If a user
# detaches a USB serial console and reboots - it should not fail!
if os.path.exists(f'/dev/{device}'):
- call(f'systemctl start serial-getty@{device}.service')
+ call(f'systemctl restart serial-getty@{device}.service')
return None
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
deleted file mode 100755
index 969266c30..000000000
--- a/src/conf_mode/vpn_ipsec.py
+++ /dev/null
@@ -1,67 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2020 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-import os
-
-from sys import exit
-
-from vyos.config import Config
-from vyos.template import render
-from vyos.util import call
-from vyos.util import dict_search
-from vyos import ConfigError
-from vyos import airbag
-from pprint import pprint
-airbag.enable()
-
-def get_config(config=None):
- if config:
- conf = config
- else:
- conf = Config()
- base = ['vpn', 'nipsec']
- if not conf.exists(base):
- return None
-
- # retrieve common dictionary keys
- ipsec = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
- return ipsec
-
-def verify(ipsec):
- if not ipsec:
- return None
-
-def generate(ipsec):
- if not ipsec:
- return None
-
- return ipsec
-
-def apply(ipsec):
- if not ipsec:
- return None
-
- pprint(ipsec)
-
-if __name__ == '__main__':
- try:
- c = get_config()
- verify(c)
- generate(c)
- apply(c)
- except ConfigError as e:
- print(e)
- exit(1)
diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py
index e970d2ef5..86aa9af09 100755
--- a/src/conf_mode/vpn_l2tp.py
+++ b/src/conf_mode/vpn_l2tp.py
@@ -291,6 +291,8 @@ def get_config(config=None):
# LNS secret
if conf.exists(['lns', 'shared-secret']):
l2tp['lns_shared_secret'] = conf.return_value(['lns', 'shared-secret'])
+ if conf.exists(['lns', 'host-name']):
+ l2tp['lns_host_name'] = conf.return_value(['lns', 'host-name'])
if conf.exists(['ccp-disable']):
l2tp['ccp_disable'] = True
diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py
index 47367f125..070009722 100755
--- a/src/conf_mode/vpn_sstp.py
+++ b/src/conf_mode/vpn_sstp.py
@@ -50,16 +50,14 @@ def verify(sstp):
verify_accel_ppp_base_service(sstp)
- if not sstp['client_ip_pool']:
+ if 'client_ip_pool' not in sstp and 'client_ipv6_pool' not in sstp:
raise ConfigError('Client IP subnet required')
#
# SSL certificate checks
#
tmp = dict_search('ssl.ca_cert_file', sstp)
- if not tmp:
- raise ConfigError(f'SSL CA certificate file required!')
- else:
+ if tmp:
if not os.path.isfile(tmp):
raise ConfigError(f'SSL CA certificate "{tmp}" does not exist!')
diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py
index 3ccc7d66b..ad38adaec 100755
--- a/src/conf_mode/vrrp.py
+++ b/src/conf_mode/vrrp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
+# Copyright (C) 2018-2021 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -17,244 +17,144 @@
import os
from sys import exit
-from ipaddress import ip_address, ip_interface, IPv4Interface, IPv6Interface, IPv4Address, IPv6Address
-from json import dumps
-from pathlib import Path
-
-import vyos.config
-
-from vyos import ConfigError
-from vyos.util import call
-from vyos.template import render
+from ipaddress import ip_interface
+from ipaddress import IPv4Interface
+from ipaddress import IPv6Interface
+from vyos.config import Config
+from vyos.configdict import dict_merge
from vyos.ifconfig.vrrp import VRRP
-
+from vyos.template import render
+from vyos.template import is_ipv4
+from vyos.template import is_ipv6
+from vyos.util import call
+from vyos.util import is_systemd_service_running
+from vyos.xml import defaults
+from vyos import ConfigError
from vyos import airbag
airbag.enable()
def get_config(config=None):
- vrrp_groups = []
- sync_groups = []
-
if config:
- config = config
+ conf = config
else:
- config = vyos.config.Config()
-
- # Get the VRRP groups
- for group_name in config.list_nodes("high-availability vrrp group"):
- config.set_level("high-availability vrrp group {0}".format(group_name))
-
- # Retrieve the values
- group = {"preempt": True, "use_vmac": False, "disable": False}
-
- if config.exists("disable"):
- group["disable"] = True
-
- group["name"] = group_name
- group["vrid"] = config.return_value("vrid")
- group["interface"] = config.return_value("interface")
- group["description"] = config.return_value("description")
- group["advertise_interval"] = config.return_value("advertise-interval")
- group["priority"] = config.return_value("priority")
- group["hello_source"] = config.return_value("hello-source-address")
- group["peer_address"] = config.return_value("peer-address")
- group["sync_group"] = config.return_value("sync-group")
- group["preempt_delay"] = config.return_value("preempt-delay")
- group["virtual_addresses"] = config.return_values("virtual-address")
- group["virtual_addresses_excluded"] = config.return_values("virtual-address-excluded")
-
- group["auth_password"] = config.return_value("authentication password")
- group["auth_type"] = config.return_value("authentication type")
-
- group["health_check_script"] = config.return_value("health-check script")
- group["health_check_interval"] = config.return_value("health-check interval")
- group["health_check_count"] = config.return_value("health-check failure-count")
-
- group["master_script"] = config.return_value("transition-script master")
- group["backup_script"] = config.return_value("transition-script backup")
- group["fault_script"] = config.return_value("transition-script fault")
- group["stop_script"] = config.return_value("transition-script stop")
- group["script_mode_force"] = config.exists("transition-script mode-force")
-
- if config.exists("no-preempt"):
- group["preempt"] = False
- if config.exists("rfc3768-compatibility"):
- group["use_vmac"] = True
-
- # Substitute defaults where applicable
- if not group["advertise_interval"]:
- group["advertise_interval"] = 1
- if not group["priority"]:
- group["priority"] = 100
- if not group["preempt_delay"]:
- group["preempt_delay"] = 0
- if not group["health_check_interval"]:
- group["health_check_interval"] = 60
- if not group["health_check_count"]:
- group["health_check_count"] = 3
-
- # FIXUP: translate our option for auth type to keepalived's syntax
- # for simplicity
- if group["auth_type"]:
- if group["auth_type"] == "plaintext-password":
- group["auth_type"] = "PASS"
- else:
- group["auth_type"] = "AH"
-
- vrrp_groups.append(group)
-
- config.set_level("")
-
- # Get the sync group used for conntrack-sync
- conntrack_sync_group = None
- if config.exists("service conntrack-sync failover-mechanism vrrp"):
- conntrack_sync_group = config.return_value("service conntrack-sync failover-mechanism vrrp sync-group")
-
- # Get the sync groups
- for sync_group_name in config.list_nodes("high-availability vrrp sync-group"):
- config.set_level("high-availability vrrp sync-group {0}".format(sync_group_name))
-
- sync_group = {"conntrack_sync": False}
- sync_group["name"] = sync_group_name
- sync_group["members"] = config.return_values("member")
- if conntrack_sync_group:
- if conntrack_sync_group == sync_group_name:
- sync_group["conntrack_sync"] = True
-
- # add transition script configuration
- sync_group["master_script"] = config.return_value("transition-script master")
- sync_group["backup_script"] = config.return_value("transition-script backup")
- sync_group["fault_script"] = config.return_value("transition-script fault")
- sync_group["stop_script"] = config.return_value("transition-script stop")
-
- sync_groups.append(sync_group)
-
- # create a file with dict with proposed configuration
- with open("{}.temp".format(VRRP.location['vyos']), 'w') as dict_file:
- dict_file.write(dumps({'vrrp_groups': vrrp_groups, 'sync_groups': sync_groups}))
-
- return (vrrp_groups, sync_groups)
-
-
-def verify(data):
- vrrp_groups, sync_groups = data
-
- for group in vrrp_groups:
- # Check required fields
- if not group["vrid"]:
- raise ConfigError("vrid is required but not set in VRRP group {0}".format(group["name"]))
- if not group["interface"]:
- raise ConfigError("interface is required but not set in VRRP group {0}".format(group["name"]))
- if not group["virtual_addresses"]:
- raise ConfigError("virtual-address is required but not set in VRRP group {0}".format(group["name"]))
-
- if group["auth_password"] and (not group["auth_type"]):
- raise ConfigError("authentication type is required but not set in VRRP group {0}".format(group["name"]))
-
- # Keepalived doesn't allow mixing IPv4 and IPv6 in one group, so we mirror that restriction
-
- # XXX: filter on map object is destructive, so we force it to list.
- # Additionally, filter objects always evaluate to True, empty or not,
- # so we force them to lists as well.
- vaddrs = list(map(lambda i: ip_interface(i), group["virtual_addresses"]))
- vaddrs4 = list(filter(lambda x: isinstance(x, IPv4Interface), vaddrs))
- vaddrs6 = list(filter(lambda x: isinstance(x, IPv6Interface), vaddrs))
-
- if vaddrs4 and vaddrs6:
- raise ConfigError("VRRP group {0} mixes IPv4 and IPv6 virtual addresses, this is not allowed. Create separate groups for IPv4 and IPv6".format(group["name"]))
-
- if vaddrs4:
- if group["hello_source"]:
- hsa = ip_address(group["hello_source"])
- if isinstance(hsa, IPv6Address):
- raise ConfigError("VRRP group {0} uses IPv4 but its hello-source-address is IPv6".format(group["name"]))
- if group["peer_address"]:
- pa = ip_address(group["peer_address"])
- if isinstance(pa, IPv6Address):
- raise ConfigError("VRRP group {0} uses IPv4 but its peer-address is IPv6".format(group["name"]))
-
- if vaddrs6:
- if group["hello_source"]:
- hsa = ip_address(group["hello_source"])
- if isinstance(hsa, IPv4Address):
- raise ConfigError("VRRP group {0} uses IPv6 but its hello-source-address is IPv4".format(group["name"]))
- if group["peer_address"]:
- pa = ip_address(group["peer_address"])
- if isinstance(pa, IPv4Address):
- raise ConfigError("VRRP group {0} uses IPv6 but its peer-address is IPv4".format(group["name"]))
-
- # Warn the user about the deprecated mode-force option
- if group["script_mode_force"]:
- print("""Warning: "transition-script mode-force" VRRP option is deprecated and will be removed in VyOS 1.4.""")
- print("""It's no longer necessary, so you can safely remove it from your config now.""")
-
- # Disallow same VRID on multiple interfaces
- _groups = sorted(vrrp_groups, key=(lambda x: x["interface"]))
- count = len(_groups) - 1
- index = 0
- while (index < count):
- if (_groups[index]["vrid"] == _groups[index + 1]["vrid"]) and (_groups[index]["interface"] == _groups[index + 1]["interface"]):
- raise ConfigError("VRID {0} is used in groups {1} and {2} that both use interface {3}. Groups on the same interface must use different VRIDs".format(
- _groups[index]["vrid"], _groups[index]["name"], _groups[index + 1]["name"], _groups[index]["interface"]))
- else:
- index += 1
+ conf = Config()
+
+ base = ['high-availability', 'vrrp']
+ if not conf.exists(base):
+ return None
+
+ vrrp = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ if 'group' in vrrp:
+ default_values = defaults(base + ['group'])
+ for group in vrrp['group']:
+ vrrp['group'][group] = dict_merge(default_values, vrrp['group'][group])
+
+ ## Get the sync group used for conntrack-sync
+ conntrack_path = ['service', 'conntrack-sync', 'failover-mechanism', 'vrrp', 'sync-group']
+ if conf.exists(conntrack_path):
+ vrrp['conntrack_sync_group'] = conf.return_value(conntrack_path)
+
+ return vrrp
+
+def verify(vrrp):
+ if not vrrp:
+ return None
+
+ used_vrid_if = []
+ if 'group' in vrrp:
+ for group, group_config in vrrp['group'].items():
+ # Check required fields
+ if 'vrid' not in group_config:
+ raise ConfigError(f'VRID is required but not set in VRRP group "{group}"')
+
+ if 'interface' not in group_config:
+ raise ConfigError(f'Interface is required but not set in VRRP group "{group}"')
+
+ if 'virtual_address' not in group_config:
+ raise ConfigError(f'Virtual IP address is required but not set in VRRP group "{group}"')
+
+ if 'authentication' in group_config:
+ if not {'password', 'type'} <= set(group_config['authentication']):
+ raise ConfigError(f'Authentication requires both type and passwortd to be set in VRRP group "{group}"')
+
+ # We can not use a VRID once per interface
+ interface = group_config['interface']
+ vrid = group_config['vrid']
+ tmp = {'interface': interface, 'vrid': vrid}
+ if tmp in used_vrid_if:
+ raise ConfigError(f'VRID "{vrid}" can only be used once on interface "{interface}"!')
+ used_vrid_if.append(tmp)
+
+ # Keepalived doesn't allow mixing IPv4 and IPv6 in one group, so we mirror that restriction
+
+ # XXX: filter on map object is destructive, so we force it to list.
+ # Additionally, filter objects always evaluate to True, empty or not,
+ # so we force them to lists as well.
+ vaddrs = list(map(lambda i: ip_interface(i), group_config['virtual_address']))
+ vaddrs4 = list(filter(lambda x: isinstance(x, IPv4Interface), vaddrs))
+ vaddrs6 = list(filter(lambda x: isinstance(x, IPv6Interface), vaddrs))
+
+ if vaddrs4 and vaddrs6:
+ raise ConfigError(f'VRRP group "{group}" mixes IPv4 and IPv6 virtual addresses, this is not allowed.\n' \
+ 'Create individual groups for IPv4 and IPv6!')
+ if vaddrs4:
+ if 'hello_source_address' in group_config:
+ if is_ipv6(group_config['hello_source_address']):
+ raise ConfigError(f'VRRP group "{group}" uses IPv4 but hello-source-address is IPv6!')
+
+ if 'peer_address' in group_config:
+ if is_ipv6(group_config['peer_address']):
+ raise ConfigError(f'VRRP group "{group}" uses IPv4 but peer-address is IPv6!')
+
+ if vaddrs6:
+ if 'hello_source_address' in group_config:
+ if is_ipv4(group_config['hello_source_address']):
+ raise ConfigError(f'VRRP group "{group}" uses IPv6 but hello-source-address is IPv4!')
+
+ if 'peer_address' in group_config:
+ if is_ipv4(group_config['peer_address']):
+ raise ConfigError(f'VRRP group "{group}" uses IPv6 but peer-address is IPv4!')
+
+
+ # Warn the user about the deprecated mode-force option
+ if 'transition_script' in group_config and 'mode_force' in group_config['transition_script']:
+ print('Warning: "transition-script mode-force" VRRP option is deprecated and will be removed in VyOS 1.4.')
+ print('It is no longer necessary, so you can safely remove it from your config now.')
# Check sync groups
- vrrp_group_names = list(map(lambda x: x["name"], vrrp_groups))
-
- for sync_group in sync_groups:
- for m in sync_group["members"]:
- if not (m in vrrp_group_names):
- raise ConfigError("VRRP sync-group {0} refers to VRRP group {1}, but group {1} does not exist".format(sync_group["name"], m))
-
-
-def generate(data):
- vrrp_groups, sync_groups = data
-
- # Remove disabled groups from the sync group member lists
- for sync_group in sync_groups:
- for member in sync_group["members"]:
- g = list(filter(lambda x: x["name"] == member, vrrp_groups))[0]
- if g["disable"]:
- print("Warning: ignoring disabled VRRP group {0} in sync-group {1}".format(g["name"], sync_group["name"]))
- # Filter out disabled groups
- vrrp_groups = list(filter(lambda x: x["disable"] is not True, vrrp_groups))
-
- render(VRRP.location['config'], 'vrrp/keepalived.conf.tmpl',
- {"groups": vrrp_groups, "sync_groups": sync_groups})
- render(VRRP.location['daemon'], 'vrrp/daemon.tmpl', {})
+ if 'sync_group' in vrrp:
+ for sync_group, sync_config in vrrp['sync_group'].items():
+ if 'member' in sync_config:
+ for member in sync_config['member']:
+ if member not in vrrp['group']:
+ raise ConfigError(f'VRRP sync-group "{sync_group}" refers to VRRP group "{member}", '\
+ 'but it does not exist!')
+
+def generate(vrrp):
+ if not vrrp:
+ return None
+
+ render(VRRP.location['config'], 'vrrp/keepalived.conf.tmpl', vrrp)
return None
+def apply(vrrp):
+ service_name = 'keepalived.service'
+ if not vrrp:
+ call(f'systemctl stop {service_name}')
+ return None
-def apply(data):
- vrrp_groups, sync_groups = data
- if vrrp_groups:
- # safely rename a temporary file with configuration dict
- try:
- dict_file = Path("{}.temp".format(VRRP.location['vyos']))
- dict_file.rename(Path(VRRP.location['vyos']))
- except Exception as err:
- print("Unable to rename the file with keepalived config for FIFO pipe: {}".format(err))
-
- if not VRRP.is_running():
- print("Starting the VRRP process")
- ret = call("systemctl restart keepalived.service")
- else:
- print("Reloading the VRRP process")
- ret = call("systemctl reload keepalived.service")
-
- if ret != 0:
- raise ConfigError("keepalived failed to start")
+ # XXX: T3944 - reload keepalived configuration if service is already running
+ # to not cause any service disruption when applying changes.
+ if is_systemd_service_running(service_name):
+ call(f'systemctl reload {service_name}')
else:
- # VRRP is removed in the commit
- print("Stopping the VRRP process")
- call("systemctl stop keepalived.service")
- os.unlink(VRRP.location['daemon'])
-
+ call(f'systemctl restart {service_name}')
return None
-
if __name__ == '__main__':
try:
c = get_config()
@@ -262,5 +162,5 @@ if __name__ == '__main__':
generate(c)
apply(c)
except ConfigError as e:
- print("VRRP error: {0}".format(str(e)))
+ print(e)
exit(1)
diff --git a/src/etc/cron.d/check-wwan b/src/etc/cron.d/check-wwan
new file mode 100644
index 000000000..28190776f
--- /dev/null
+++ b/src/etc/cron.d/check-wwan
@@ -0,0 +1 @@
+*/5 * * * * root /usr/libexec/vyos/vyos-check-wwan.py
diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient b/src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient
index 939055a63..ae6bf9f16 100644
--- a/src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient
+++ b/src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient
@@ -2,26 +2,37 @@
if [ -z ${CONTROLLED_STOP} ] ; then
# stop dhclient for this interface, if it is not current one
# get PID for current dhclient
- current_dhclient=`ps --no-headers --format ppid --pid $$ | awk '{ print $1 }'`
+ current_dhclient=`ps --no-headers --format ppid --pid $$ | awk '{ print \$1 }'`
# get PID for master process (current can be a fork)
- master_dhclient=`ps --no-headers --format ppid --pid $current_dhclient | awk '{ print $1 }'`
+ master_dhclient=`ps --no-headers --format ppid --pid $current_dhclient | awk '{ print \$1 }'`
# get IP version for current dhclient
- ipversion_arg=`ps --no-headers --format args --pid $current_dhclient | awk '{ print $2 }'`
+ ipversion_arg=`ps --no-headers --format args --pid $current_dhclient | awk 'match(\$0, /\s-(4|6)\s/, IPV) { printf("%s", IPV[1]) }'`
# get list of all dhclient running for current interface
- dhclients_pids=(`pgrep -f "dhclient $ipversion_arg.* $interface(\s|$)"`)
+ if [[ $ipversion_arg == "6" ]]; then
+ dhclients_pids=(`pgrep -f "dhclient.*\s-6\s.*\s$interface(\s|$)"`)
+ else
+ dhclients_pids=(`ps --no-headers --format pid,args -C dhclient | awk "{ if(match(\\$0, /\s${interface}(\s|$)/) && !match(\\$0, /\s-6\s/)) printf(\"%s\n\", \\$1) }"`)
+ fi
logmsg info "Current dhclient PID: $current_dhclient, Parent PID: $master_dhclient, IP version: $ipversion_arg, All dhclients for interface $interface: ${dhclients_pids[@]}"
# stop all dhclients for current interface, except current one
for dhclient in ${dhclients_pids[@]}; do
if ([ $dhclient -ne $current_dhclient ] && [ $dhclient -ne $master_dhclient ]); then
- logmsg info "Stopping dhclient with PID: ${dhclient}"
# get path to PID-file of dhclient process
- local dhclient_pidfile=`ps --no-headers --format args --pid $dhclient | awk 'match($0, ".*-pf (/.*pid) .*", PF) { print PF[1] }'`
+ local dhclient_pidfile=`ps --no-headers --format args --pid $dhclient | awk 'match(\$0, ".*-pf (/.*pid) .*", PF) { print PF[1] }'`
+ # get path to lease-file of dhclient process
+ local dhclient_leasefile=`ps --no-headers --format args --pid $dhclient | awk 'match(\$0, ".*-lf (/\\\S*leases) .*", LF) { print LF[1] }'`
# stop dhclient with native command - this will run dhclient-script with correct reason unlike simple kill
- dhclient -e CONTROLLED_STOP=yes -x -pf $dhclient_pidfile
+ logmsg info "Stopping dhclient with PID: ${dhclient}, PID file: ${dhclient_pidfile}, Leases file: ${dhclient_leasefile}"
+ if [[ -e $dhclient_pidfile ]]; then
+ dhclient -e CONTROLLED_STOP=yes -x -pf $dhclient_pidfile -lf $dhclient_leasefile
+ else
+ logmsg error "PID file $dhclient_pidfile does not exists, killing dhclient with SIGTERM signal"
+ kill -s 15 ${dhclient}
+ fi
fi
done
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 edb7c7b27..f060c6ee8 100644
--- a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
+++ b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
@@ -19,7 +19,7 @@ if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then
for router in $old_routers; do
# check if we are bound to a VRF
local vrf_name=$(basename /sys/class/net/${interface}/upper_* | sed -e 's/upper_//')
- if [ -n $vrf_name ]; then
+ if [ "$vrf_name" != "*" ]; then
vrf="vrf $vrf_name"
fi
diff --git a/src/etc/systemd/system/keepalived.service.d/override.conf b/src/etc/systemd/system/keepalived.service.d/override.conf
index 9fcabf652..c18ae0c29 100644
--- a/src/etc/systemd/system/keepalived.service.d/override.conf
+++ b/src/etc/systemd/system/keepalived.service.d/override.conf
@@ -1,2 +1,12 @@
+[Unit]
+ConditionPathExists=
+ConditionPathExists=/run/keepalived/keepalived.conf
+After=
+After=vyos-router.service
+
[Service]
KillMode=process
+ExecStart=
+ExecStart=/usr/sbin/keepalived --use-file /run/keepalived/keepalived.conf --pid /run/keepalived/keepalived.pid --dont-fork --snmp
+PIDFile=
+PIDFile=/run/keepalived/keepalived.pid
diff --git a/src/etc/systemd/system/openvpn@.service.d/10-override.conf b/src/etc/systemd/system/openvpn@.service.d/10-override.conf
new file mode 100644
index 000000000..775a2d7ba
--- /dev/null
+++ b/src/etc/systemd/system/openvpn@.service.d/10-override.conf
@@ -0,0 +1,14 @@
+[Unit]
+After=
+After=vyos-router.service
+
+[Service]
+WorkingDirectory=
+WorkingDirectory=/run/openvpn
+ExecStart=
+ExecStart=/usr/sbin/openvpn --daemon openvpn-%i --config %i.conf --status %i.status 30 --writepid %i.pid
+ExecReload=/bin/kill -HUP $MAINPID
+User=openvpn
+Group=openvpn
+AmbientCapabilities=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SYS_CHROOT CAP_DAC_OVERRIDE CAP_AUDIT_WRITE
+CapabilityBoundingSet=CAP_IPC_LOCK CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_SETGID CAP_SETUID CAP_SYS_CHROOT CAP_DAC_OVERRIDE CAP_AUDIT_WRITE
diff --git a/src/etc/systemd/system/openvpn@.service.d/override.conf b/src/etc/systemd/system/openvpn@.service.d/override.conf
deleted file mode 100644
index 7946484a3..000000000
--- a/src/etc/systemd/system/openvpn@.service.d/override.conf
+++ /dev/null
@@ -1,9 +0,0 @@
-[Unit]
-After=
-After=vyos-router.service
-
-[Service]
-WorkingDirectory=
-WorkingDirectory=/run/openvpn
-ExecStart=
-ExecStart=/usr/sbin/openvpn --daemon openvpn-%i --config %i.conf --status %i.status 30 --writepid %i.pid
diff --git a/src/etc/udev/rules.d/90-vyos-serial.rules b/src/etc/udev/rules.d/90-vyos-serial.rules
index 3f10f4924..5cca89e89 100644
--- a/src/etc/udev/rules.d/90-vyos-serial.rules
+++ b/src/etc/udev/rules.d/90-vyos-serial.rules
@@ -22,7 +22,7 @@ IMPORT{builtin}="path_id", IMPORT{builtin}="usb_id"
# (tr -d -) does the replacement
# - Replace the first group after ":" to represent the bus relation (sed -e 0,/:/s//b/) indicated by "b"
# - Replace the next group after ":" to represent the port relation (sed -e 0,/:/s//p/) indicated by "p"
-ENV{ID_PATH}=="?*", ENV{.ID_PORT}=="", PROGRAM="/bin/sh -c 'D=$env{ID_PATH}; echo ${D:17} | tr -d - | sed -e 0,/:/s//b/ | sed -e 0,/:/s//p/'", SYMLINK+="serial/by-bus/$result"
-ENV{ID_PATH}=="?*", ENV{.ID_PORT}=="?*", PROGRAM="/bin/sh -c 'D=$env{ID_PATH}; echo ${D:17} | tr -d - | sed -e 0,/:/s//b/ | sed -e 0,/:/s//p/'", SYMLINK+="serial/by-bus/$result"
+ENV{ID_PATH}=="?*", ENV{.ID_PORT}=="", PROGRAM="/bin/sh -c 'echo $env{ID_PATH} | cut -d- -f3- | tr -d - | sed -e 0,/:/s//b/ | sed -e 0,/:/s//p/'", SYMLINK+="serial/by-bus/$result"
+ENV{ID_PATH}=="?*", ENV{.ID_PORT}=="?*", PROGRAM="/bin/sh -c 'echo $env{ID_PATH} | cut -d- -f3- | tr -d - | sed -e 0,/:/s//b/ | sed -e 0,/:/s//p/'", SYMLINK+="serial/by-bus/$result"
LABEL="serial_end"
diff --git a/src/helpers/vyos-check-wwan.py b/src/helpers/vyos-check-wwan.py
new file mode 100755
index 000000000..c6e6c54b7
--- /dev/null
+++ b/src/helpers/vyos-check-wwan.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 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/>.
+
+from vyos.configquery import VbashOpRun
+from vyos.configquery import ConfigTreeQuery
+
+from vyos.util import is_wwan_connected
+from vyos.util import call
+
+conf = ConfigTreeQuery()
+dict = conf.get_config_dict(['interfaces', 'wwan'], key_mangling=('-', '_'),
+ get_first_key=True)
+
+for interface, interface_config in dict.items():
+ if not is_wwan_connected(interface):
+ if 'disable' in interface_config:
+ # do not restart this interface as it's disabled by the user
+ continue
+
+ #op = VbashOpRun()
+ #op.run(['connect', 'interface', interface])
+ call(f'VYOS_TAGNODE_VALUE={interface} /usr/libexec/vyos/conf_mode/interfaces-wwan.py')
+
+exit(0)
diff --git a/src/migration-scripts/dhcp-server/5-to-6 b/src/migration-scripts/dhcp-server/5-to-6
new file mode 100755
index 000000000..aefe84737
--- /dev/null
+++ b/src/migration-scripts/dhcp-server/5-to-6
@@ -0,0 +1,87 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 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/>.
+
+# T1968: allow multiple static-routes to be configured
+# T3838: rename dns-server -> name-server
+
+import sys
+from vyos.configtree import ConfigTree
+
+if (len(sys.argv) < 1):
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+base = ['service', 'dhcp-server']
+config = ConfigTree(config_file)
+
+if not config.exists(base + ['shared-network-name']):
+ # Nothing to do
+ exit(0)
+
+# Run this for every instance if 'shared-network-name'
+for network in config.list_nodes(base + ['shared-network-name']):
+ base_network = base + ['shared-network-name', network]
+
+ if not config.exists(base_network + ['subnet']):
+ continue
+
+ # Run this for every specified 'subnet'
+ for subnet in config.list_nodes(base_network + ['subnet']):
+ base_subnet = base_network + ['subnet', subnet]
+
+ # T1968: allow multiple static-routes to be configured
+ if config.exists(base_subnet + ['static-route']):
+ prefix = config.return_value(base_subnet + ['static-route', 'destination-subnet'])
+ router = config.return_value(base_subnet + ['static-route', 'router'])
+ config.delete(base_subnet + ['static-route'])
+
+ config.set(base_subnet + ['static-route', prefix, 'next-hop'], value=router)
+ config.set_tag(base_subnet + ['static-route'])
+
+ # T3838: rename dns-server -> name-server
+ if config.exists(base_subnet + ['dns-server']):
+ config.rename(base_subnet + ['dns-server'], 'name-server')
+
+
+ # T3672: ISC DHCP server only supports one failover peer
+ if config.exists(base_subnet + ['failover']):
+ # There can only be one failover configuration, if none is present
+ # we add the first one
+ if not config.exists(base + ['failover']):
+ local = config.return_value(base_subnet + ['failover', 'local-address'])
+ remote = config.return_value(base_subnet + ['failover', 'peer-address'])
+ status = config.return_value(base_subnet + ['failover', 'status'])
+ name = config.return_value(base_subnet + ['failover', 'name'])
+
+ config.set(base + ['failover', 'remote'], value=remote)
+ config.set(base + ['failover', 'source-address'], value=local)
+ config.set(base + ['failover', 'status'], value=status)
+ config.set(base + ['failover', 'name'], value=name)
+
+ config.delete(base_subnet + ['failover'])
+ config.set(base_subnet + ['enable-failover'])
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)
diff --git a/src/migration-scripts/dns-forwarding/1-to-2 b/src/migration-scripts/dns-forwarding/1-to-2
index 8c4f4b5c7..ba10c26f2 100755
--- a/src/migration-scripts/dns-forwarding/1-to-2
+++ b/src/migration-scripts/dns-forwarding/1-to-2
@@ -67,8 +67,14 @@ if config.exists(base + ['listen-on']):
# retrieve corresponding interface addresses in CIDR format
# those need to be converted in pure IP addresses without network information
path = ['interfaces', section, intf, 'address']
- for addr in config.return_values(path):
- listen_addr.append( ip_interface(addr).ip )
+ try:
+ for addr in config.return_values(path):
+ listen_addr.append( ip_interface(addr).ip )
+ except:
+ # Some interface types do not use "address" option (e.g. OpenVPN)
+ # and may not even have a fixed address
+ print("Could not retrieve the address of the interface {} from the config".format(intf))
+ print("You will need to update your DNS forwarding configuration manually")
for addr in listen_addr:
config.set(base + ['listen-address'], value=addr, replace=False)
diff --git a/src/migration-scripts/interfaces/20-to-21 b/src/migration-scripts/interfaces/20-to-21
new file mode 100755
index 000000000..0bd858760
--- /dev/null
+++ b/src/migration-scripts/interfaces/20-to-21
@@ -0,0 +1,120 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 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/>.
+
+# T3619: mirror Linux Kernel defaults for ethernet offloading options into VyOS
+# CLI. See https://phabricator.vyos.net/T3619#102254 for all the details.
+# T3787: Remove deprecated UDP fragmentation offloading option
+
+from sys import argv
+
+from vyos.ethtool import Ethtool
+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()
+
+base = ['interfaces', 'ethernet']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+ exit(0)
+
+for ifname in config.list_nodes(base):
+ eth = Ethtool(ifname)
+
+ # If GRO is enabled by the Kernel - we reflect this on the CLI. If GRO is
+ # enabled via CLI but not supported by the NIC - we remove it from the CLI
+ configured = config.exists(base + [ifname, 'offload', 'gro'])
+ enabled, fixed = eth.get_generic_receive_offload()
+ if configured and fixed:
+ config.delete(base + [ifname, 'offload', 'gro'])
+ elif enabled and not fixed:
+ config.set(base + [ifname, 'offload', 'gro'])
+
+ # If GSO is enabled by the Kernel - we reflect this on the CLI. If GSO is
+ # enabled via CLI but not supported by the NIC - we remove it from the CLI
+ configured = config.exists(base + [ifname, 'offload', 'gso'])
+ enabled, fixed = eth.get_generic_segmentation_offload()
+ if configured and fixed:
+ config.delete(base + [ifname, 'offload', 'gso'])
+ elif enabled and not fixed:
+ config.set(base + [ifname, 'offload', 'gso'])
+
+ # If LRO is enabled by the Kernel - we reflect this on the CLI. If LRO is
+ # enabled via CLI but not supported by the NIC - we remove it from the CLI
+ configured = config.exists(base + [ifname, 'offload', 'lro'])
+ enabled, fixed = eth.get_large_receive_offload()
+ if configured and fixed:
+ config.delete(base + [ifname, 'offload', 'lro'])
+ elif enabled and not fixed:
+ config.set(base + [ifname, 'offload', 'lro'])
+
+ # If SG is enabled by the Kernel - we reflect this on the CLI. If SG is
+ # enabled via CLI but not supported by the NIC - we remove it from the CLI
+ configured = config.exists(base + [ifname, 'offload', 'sg'])
+ enabled, fixed = eth.get_scatter_gather()
+ if configured and fixed:
+ config.delete(base + [ifname, 'offload', 'sg'])
+ elif enabled and not fixed:
+ config.set(base + [ifname, 'offload', 'sg'])
+
+ # If TSO is enabled by the Kernel - we reflect this on the CLI. If TSO is
+ # enabled via CLI but not supported by the NIC - we remove it from the CLI
+ configured = config.exists(base + [ifname, 'offload', 'tso'])
+ enabled, fixed = eth.get_tcp_segmentation_offload()
+ if configured and fixed:
+ config.delete(base + [ifname, 'offload', 'tso'])
+ elif enabled and not fixed:
+ config.set(base + [ifname, 'offload', 'tso'])
+
+ # Remove deprecated UDP fragmentation offloading option
+ if config.exists(base + [ifname, 'offload', 'ufo']):
+ config.delete(base + [ifname, 'offload', 'ufo'])
+
+ # Also while processing the interface configuration, not all adapters support
+ # changing the speed and duplex settings. If the desired speed and duplex
+ # values do not work for the NIC driver, we change them back to the default
+ # value of "auto" - which will be applied if the CLI node is deleted.
+ speed_path = base + [ifname, 'speed']
+ duplex_path = base + [ifname, 'duplex']
+ # speed and duplex must always be set at the same time if not set to "auto"
+ if config.exists(speed_path) and config.exists(duplex_path):
+ speed = config.return_value(speed_path)
+ duplex = config.return_value(duplex_path)
+ if speed != 'auto' and duplex != 'auto':
+ if not eth.check_speed_duplex(speed, duplex):
+ config.delete(speed_path)
+ config.delete(duplex_path)
+
+ # Also while processing the interface configuration, not all adapters support
+ # changing disabling flow-control - or change this setting. If disabling
+ # flow-control is not supported by the NIC, we remove the setting from CLI
+ flow_control_path = base + [ifname, 'disable-flow-control']
+ if config.exists(flow_control_path):
+ if not eth.check_flow_control():
+ config.delete(flow_control_path)
+
+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/interfaces/21-to-22 b/src/migration-scripts/interfaces/21-to-22
new file mode 100755
index 000000000..098102102
--- /dev/null
+++ b/src/migration-scripts/interfaces/21-to-22
@@ -0,0 +1,46 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 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/>.
+
+from sys import argv
+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 = ['interfaces', 'tunnel']
+
+if not config.exists(base):
+ exit(0)
+
+for interface in config.list_nodes(base):
+ path = base + [interface, 'dhcp-interface']
+ if config.exists(path):
+ tmp = config.return_value(path)
+ config.delete(path)
+ config.set(base + [interface, 'source-interface'], value=tmp)
+
+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/isis/0-to-1 b/src/migration-scripts/isis/0-to-1
new file mode 100755
index 000000000..93cbbbed5
--- /dev/null
+++ b/src/migration-scripts/isis/0-to-1
@@ -0,0 +1,59 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 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/>.
+
+# T3417: migrate IS-IS tagNode to node as we can only have one IS-IS process
+
+from sys import argv
+from sys import 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()
+
+base = ['protocols', 'isis']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+
+# Only one IS-IS process is supported, thus this operation is save
+isis_base = base + config.list_nodes(base)
+
+# We need a temporary copy of the config
+tmp_base = ['protocols', 'isis2']
+config.copy(isis_base, tmp_base)
+
+# Now it's save to delete the old configuration
+config.delete(base)
+
+# Rename temporary copy to new final config (IS-IS domain key is static and no
+# longer required to be set via CLI)
+config.rename(tmp_base, 'isis')
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print(f'Failed to save the modified config: {e}')
+ exit(1)
diff --git a/src/migration-scripts/system/20-to-21 b/src/migration-scripts/system/20-to-21
new file mode 100755
index 000000000..1728995de
--- /dev/null
+++ b/src/migration-scripts/system/20-to-21
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 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/>.
+
+# T3795: merge "system name-servers-dhcp" into "system name-server"
+
+import os
+
+from sys import argv
+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()
+
+base = ['system', 'name-servers-dhcp']
+config = ConfigTree(config_file)
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+
+for interface in config.return_values(base):
+ config.set(['system', 'name-server'], value=interface, replace=False)
+
+config.delete(base)
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)
diff --git a/src/op_mode/connect_disconnect.py b/src/op_mode/connect_disconnect.py
index a773aa28e..ffc574362 100755
--- a/src/op_mode/connect_disconnect.py
+++ b/src/op_mode/connect_disconnect.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-2021 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -17,21 +17,19 @@
import os
import argparse
-from sys import exit
from psutil import process_iter
-from time import strftime, localtime, time
from vyos.util import call
+from vyos.util import DEVNULL
+from vyos.util import is_wwan_connected
-def check_interface(interface):
+def check_ppp_interface(interface):
if not os.path.isfile(f'/etc/ppp/peers/{interface}'):
- print(f'Interface {interface}: invalid!')
+ print(f'Interface {interface} does not exist!')
exit(1)
def check_ppp_running(interface):
- """
- Check if ppp process is running in the interface in question
- """
+ """ Check if PPP process is running in the interface in question """
for p in process_iter():
if "pppd" in p.name():
if interface in p.cmdline():
@@ -40,32 +38,46 @@ def check_ppp_running(interface):
return False
def connect(interface):
- """
- Connect PPP interface
- """
- check_interface(interface)
+ """ Connect dialer interface """
- # Check if interface is already dialed
- if os.path.isdir(f'/sys/class/net/{interface}'):
- print(f'Interface {interface}: already connected!')
- elif check_ppp_running(interface):
- print(f'Interface {interface}: connection is beeing established!')
+ if interface.startswith('ppp'):
+ check_ppp_interface(interface)
+ # Check if interface is already dialed
+ if os.path.isdir(f'/sys/class/net/{interface}'):
+ print(f'Interface {interface}: already connected!')
+ elif check_ppp_running(interface):
+ print(f'Interface {interface}: connection is beeing established!')
+ else:
+ print(f'Interface {interface}: connecting...')
+ call(f'systemctl restart ppp@{interface}.service')
+ elif interface.startswith('wwan'):
+ if is_wwan_connected(interface):
+ print(f'Interface {interface}: already connected!')
+ else:
+ call(f'VYOS_TAGNODE_VALUE={interface} /usr/libexec/vyos/conf_mode/interfaces-wwan.py')
else:
- print(f'Interface {interface}: connecting...')
- call(f'systemctl restart ppp@{interface}.service')
+ print(f'Unknown interface {interface}, can not connect. Aborting!')
def disconnect(interface):
- """
- Disconnect PPP interface
- """
- check_interface(interface)
+ """ Disconnect dialer interface """
- # Check if interface is already down
- if not check_ppp_running(interface):
- print(f'Interface {interface}: connection is already down')
+ if interface.startswith('ppp'):
+ check_ppp_interface(interface)
+
+ # Check if interface is already down
+ if not check_ppp_running(interface):
+ print(f'Interface {interface}: connection is already down')
+ else:
+ print(f'Interface {interface}: disconnecting...')
+ call(f'systemctl stop ppp@{interface}.service')
+ elif interface.startswith('wwan'):
+ if not is_wwan_connected(interface):
+ print(f'Interface {interface}: connection is already down')
+ else:
+ modem = interface.lstrip('wwan')
+ call(f'mmcli --modem {modem} --simple-disconnect', stdout=DEVNULL)
else:
- print(f'Interface {interface}: disconnecting...')
- call(f'systemctl stop ppp@{interface}.service')
+ print(f'Unknown interface {interface}, can not disconnect. Aborting!')
def main():
parser = argparse.ArgumentParser()
diff --git a/src/op_mode/generate_ipsec_debug_archive.sh b/src/op_mode/generate_ipsec_debug_archive.sh
new file mode 100755
index 000000000..53d0a6eaa
--- /dev/null
+++ b/src/op_mode/generate_ipsec_debug_archive.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+
+# Collecting IPSec Debug Information
+
+DATE=`date +%d-%m-%Y`
+
+a_CMD=(
+ "sudo ipsec status"
+ "sudo swanctl -L"
+ "sudo swanctl -l"
+ "sudo swanctl -P"
+ "sudo ip x sa show"
+ "sudo ip x policy show"
+ "sudo ip tunnel show"
+ "sudo ip address"
+ "sudo ip rule show"
+ "sudo ip route"
+ "sudo ip route show table 220"
+ )
+
+
+echo "DEBUG: ${DATE} on host \"$(hostname)\"" > /tmp/ipsec-status-${DATE}.txt
+date >> /tmp/ipsec-status-${DATE}.txt
+
+# Execute all DEBUG commands and save it to file
+for cmd in "${a_CMD[@]}"; do
+ echo -e "\n### ${cmd} ###" >> /tmp/ipsec-status-${DATE}.txt
+ ${cmd} >> /tmp/ipsec-status-${DATE}.txt 2>/dev/null
+done
+
+# Collect charon logs, build .tgz archive
+sudo journalctl /usr/lib/ipsec/charon > /tmp/journalctl-charon-${DATE}.txt && \
+sudo tar -zcvf /tmp/ipsec-debug-${DATE}.tgz /tmp/journalctl-charon-${DATE}.txt /tmp/ipsec-status-${DATE}.txt >& /dev/null
+sudo rm -f /tmp/journalctl-charon-${DATE}.txt /tmp/ipsec-status-${DATE}.txt
+
+echo "Debug file is generated and located in /tmp/ipsec-debug-${DATE}.tgz"
diff --git a/src/op_mode/ping.py b/src/op_mode/ping.py
index 2144ab53c..60bbc0c78 100755
--- a/src/op_mode/ping.py
+++ b/src/op_mode/ping.py
@@ -62,8 +62,8 @@ options = {
},
'interface': {
'ping': '{command} -I {value}',
- 'type': '<interface> <X.X.X.X> <h:h:h:h:h:h:h:h>',
- 'help': 'Interface to use as source for ping'
+ 'type': '<interface>',
+ 'help': 'Source interface'
},
'interval': {
'ping': '{command} -i {value}',
@@ -115,6 +115,10 @@ options = {
'type': '<bytes>',
'help': 'Number of bytes to send'
},
+ 'source-address': {
+ 'ping': '{command} -I {value}',
+ 'type': '<x.x.x.x> <h:h:h:h:h:h:h:h>',
+ },
'ttl': {
'ping': '{command} -t {value}',
'type': '<ttl>',
@@ -234,4 +238,4 @@ if __name__ == '__main__':
# print(f'{command} {host}')
os.system(f'{command} {host}')
- \ No newline at end of file
+
diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py
index c000d7d06..ffcb865a8 100755
--- a/src/op_mode/powerctrl.py
+++ b/src/op_mode/powerctrl.py
@@ -92,37 +92,40 @@ def cancel_shutdown():
try:
run('/sbin/shutdown -c --no-wall')
except OSError as e:
- exit("Could not cancel a reboot or poweroff: %s" % e)
+ exit(f'Could not cancel a reboot or poweroff: {e}')
- message = 'Scheduled {} has been cancelled {}'.format(output['MODE'], timenow)
+ mode = output['MODE']
+ message = f'Scheduled {mode} has been cancelled {timenow}'
run(f'wall {message} > /dev/null 2>&1')
else:
print("Reboot or poweroff is not scheduled")
def execute_shutdown(time, reboot=True, ask=True):
+ action = "reboot" if reboot else "poweroff"
if not ask:
- action = "reboot" if reboot else "poweroff"
- if not ask_yes_no("Are you sure you want to %s this system?" % action):
+ if not ask_yes_no(f"Are you sure you want to {action} this system?"):
exit(0)
-
- action = "-r" if reboot else "-P"
+ action_cmd = "-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)
+ out = cmd(f'/sbin/shutdown {action_cmd} 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)
+ cmd(f'/sbin/shutdown {action_cmd} {time[0]}', stderr=STDOUT)
+ # Inform all other logged in users about the reboot/shutdown
+ wall_msg = f'System {action} is scheduled {time[0]}'
+ cmd(f'/usr/bin/wall "{wall_msg}"')
else:
- exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0]))
+ exit(f'Invalid time "{time[0]}". The valid format is HH:MM')
elif len(time) == 2:
# Assume it's date and time
ts = parse_time(time[0])
@@ -131,14 +134,18 @@ def execute_shutdown(time, reboot=True, ask=True):
t = datetime.combine(ds, ts)
td = t - datetime.now()
t2 = 1 + int(td.total_seconds())//60 # Get total minutes
- cmd('/sbin/shutdown {action} {t2}', stderr=STDOUT)
+
+ cmd(f'/sbin/shutdown {action_cmd} {t2}', stderr=STDOUT)
+ # Inform all other logged in users about the reboot/shutdown
+ wall_msg = f'System {action} is scheduled {time[1]} {time[0]}'
+ cmd(f'/usr/bin/wall "{wall_msg}"')
else:
if not ts:
- exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0]))
+ exit(f'Invalid time "{time[0]}". The valid format is HH:MM')
else:
- exit("Invalid time \"{0}\". A valid format is YYYY-MM-DD [HH:MM]".format(time[1]))
+ exit(f'Invalid date "{time[1]}". A valid format is YYYY-MM-DD [HH:MM]')
else:
- exit("Could not decode date and time. Valids formats are HH:MM or YYYY-MM-DD HH:MM")
+ exit('Could not decode date and time. Valids formats are HH:MM or YYYY-MM-DD HH:MM')
check_shutdown()
diff --git a/src/op_mode/restart_frr.py b/src/op_mode/restart_frr.py
index d1b66b33f..109c8dd7b 100755
--- a/src/op_mode/restart_frr.py
+++ b/src/op_mode/restart_frr.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# Copyright (C) 2019-2021 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -13,16 +13,19 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-#
-import sys
+import os
import argparse
import logging
-from logging.handlers import SysLogHandler
-from pathlib import Path
import psutil
+from logging.handlers import SysLogHandler
+from shutil import rmtree
+
from vyos.util import call
+from vyos.util import ask_yes_no
+from vyos.util import process_named_running
+from vyos.util import makedir
# some default values
watchfrr = '/usr/lib/frr/watchfrr.sh'
@@ -40,40 +43,45 @@ logger.setLevel(logging.INFO)
def _check_safety():
try:
# print warning
- answer = input("WARNING: This is a potentially unsafe function! You may lose the connection to the router or active configuration after running this command. Use it at your own risk! Continue? [y/N]: ")
- if not answer.lower() == "y":
- logger.error("User aborted command")
+ if not ask_yes_no('WARNING: This is a potentially unsafe function!\n' \
+ 'You may lose the connection to the router or active configuration after\n' \
+ 'running this command. Use it at your own risk!\n\n'
+ 'Continue?'):
return False
# check if another restart process already running
if len([process for process in psutil.process_iter(attrs=['pid', 'name', 'cmdline']) if 'python' in process.info['name'] and 'restart_frr.py' in process.info['cmdline'][1]]) > 1:
- logger.error("Another restart_frr.py already running")
- answer = input("Another restart_frr.py process is already running. It is unsafe to continue. Do you want to process anyway? [y/N]: ")
- if not answer.lower() == "y":
+ message = 'Another restart_frr.py process is already running!'
+ logger.error(message)
+ if not ask_yes_no(f'\n{message} It is unsafe to continue.\n\n' \
+ 'Do you want to process anyway?'):
return False
# check if watchfrr.sh is running
- for process in psutil.process_iter(attrs=['pid', 'name', 'cmdline']):
- if 'bash' in process.info['name'] and watchfrr in process.info['cmdline']:
- logger.error("Another {} already running".format(watchfrr))
- answer = input("Another {} process is already running. It is unsafe to continue. Do you want to process anyway? [y/N]: ".format(watchfrr))
- if not answer.lower() == "y":
- return False
+ tmp = os.path.basename(watchfrr)
+ if process_named_running(tmp):
+ message = f'Another {tmp} process is already running.'
+ logger.error(message)
+ if not ask_yes_no(f'{message} It is unsafe to continue.\n\n' \
+ 'Do you want to process anyway?'):
+ return False
# check if vtysh is running
- for process in psutil.process_iter(attrs=['pid', 'name', 'cmdline']):
- if 'vtysh' in process.info['name']:
- logger.error("The vtysh is running by another task")
- answer = input("The vtysh is running by another task. It is unsafe to continue. Do you want to process anyway? [y/N]: ")
- if not answer.lower() == "y":
- return False
+ if process_named_running('vtysh'):
+ message = 'vtysh process is executed by another task.'
+ logger.error(message)
+ if not ask_yes_no(f'{message} It is unsafe to continue.\n\n' \
+ 'Do you want to process anyway?'):
+ return False
# check if temporary directory exists
- if Path(frrconfig_tmp).exists():
- logger.error("The temporary directory \"{}\" already exists".format(frrconfig_tmp))
- answer = input("The temporary directory \"{}\" already exists. It is unsafe to continue. Do you want to process anyway? [y/N]: ".format(frrconfig_tmp))
- if not answer.lower() == "y":
+ if os.path.exists(frrconfig_tmp):
+ message = f'Temporary directory "{frrconfig_tmp}" already exists!'
+ logger.error(message)
+ if not ask_yes_no(f'{message} It is unsafe to continue.\n\n' \
+ 'Do you want to process anyway?'):
return False
+
except:
logger.error("Something goes wrong in _check_safety()")
return False
@@ -84,94 +92,68 @@ def _check_safety():
# write active config to file
def _write_config():
# create temporary directory
- Path(frrconfig_tmp).mkdir(parents=False, exist_ok=True)
+ makedir(frrconfig_tmp)
# save frr.conf to it
- command = "{} -n -w --config_dir {} 2> /dev/null".format(vtysh, frrconfig_tmp)
+ command = f'{vtysh} -n -w --config_dir {frrconfig_tmp} 2> /dev/null'
return_code = call(command)
- if not return_code == 0:
- logger.error("Failed to save active config: \"{}\" returned exit code: {}".format(command, return_code))
+ if return_code != 0:
+ logger.error(f'Failed to save active config: "{command}" returned exit code: {return_code}')
return False
- logger.info("Active config saved to {}".format(frrconfig_tmp))
+ logger.info(f'Active config saved to {frrconfig_tmp}')
return True
# clear and remove temporary directory
def _cleanup():
- tmpdir = Path(frrconfig_tmp)
- try:
- if tmpdir.exists():
- for file in tmpdir.iterdir():
- file.unlink()
- tmpdir.rmdir()
- except:
- logger.error("Failed to remove temporary directory {}".format(frrconfig_tmp))
- print("Failed to remove temporary directory {}".format(frrconfig_tmp))
-
-# check if daemon is running
-def _daemon_check(daemon):
- command = "{} print_status {}".format(watchfrr, daemon)
- return_code = call(command)
- if not return_code == 0:
- logger.error("Daemon \"{}\" is not running".format(daemon))
- return False
-
- # return True if all checks were passed
- return True
+ if os.path.isdir(frrconfig_tmp):
+ rmtree(frrconfig_tmp)
# restart daemon
def _daemon_restart(daemon):
- command = "{} restart {}".format(watchfrr, daemon)
+ command = f'{watchfrr} restart {daemon}'
return_code = call(command)
if not return_code == 0:
- logger.error("Failed to restart daemon \"{}\"".format(daemon))
+ logger.error(f'Failed to restart daemon "{daemon}"!')
return False
# return True if restarted successfully
- logger.info("Daemon \"{}\" restarted".format(daemon))
+ logger.info(f'Daemon "{daemon}" restarted!')
return True
# reload old config
def _reload_config(daemon):
if daemon != '':
- command = "{} -n -b --config_dir {} -d {} 2> /dev/null".format(vtysh, frrconfig_tmp, daemon)
+ command = f'{vtysh} -n -b --config_dir {frrconfig_tmp} -d {daemon} 2> /dev/null'
else:
- command = "{} -n -b --config_dir {} 2> /dev/null".format(vtysh, frrconfig_tmp)
+ command = f'{vtysh} -n -b --config_dir {frrconfig_tmp} 2> /dev/null'
return_code = call(command)
if not return_code == 0:
- logger.error("Failed to reinstall configuration")
+ logger.error('Failed to re-install configuration!')
return False
# return True if restarted successfully
- logger.info("Configuration reinstalled successfully")
- return True
-
-# check all daemons if they are running
-def _check_args_daemon(daemons):
- for daemon in daemons:
- if not _daemon_check(daemon):
- return False
+ logger.info('Configuration re-installed successfully!')
return True
# define program arguments
cmd_args_parser = argparse.ArgumentParser(description='restart frr daemons')
cmd_args_parser.add_argument('--action', choices=['restart'], required=True, help='action to frr daemons')
-cmd_args_parser.add_argument('--daemon', choices=['bfdd', 'bgpd', 'ospfd', 'ospf6d', 'ripd', 'ripngd', 'staticd', 'zebra'], required=False, nargs='*', help='select single or multiple daemons')
+cmd_args_parser.add_argument('--daemon', choices=['bfdd', 'bgpd', 'ospfd', 'ospf6d', 'isisd', 'ripd', 'ripngd', 'staticd', 'zebra'], required=False, nargs='*', help='select single or multiple daemons')
# parse arguments
cmd_args = cmd_args_parser.parse_args()
-
# main logic
# restart daemon
if cmd_args.action == 'restart':
# check if it is safe to restart FRR
if not _check_safety():
print("\nOne of the safety checks was failed or user aborted command. Exiting.")
- sys.exit(1)
+ exit(1)
if not _write_config():
print("Failed to save active config")
_cleanup()
- sys.exit(1)
+ exit(1)
# a little trick to make further commands more clear
if not cmd_args.daemon:
@@ -179,19 +161,20 @@ if cmd_args.action == 'restart':
# check all daemons if they are running
if cmd_args.daemon != ['']:
- if not _check_args_daemon(cmd_args.daemon):
- print("Warning: some of listed daemons are not running")
+ for daemon in cmd_args.daemon:
+ if not process_named_running(daemon):
+ print('WARNING: some of listed daemons are not running!')
# run command to restart daemon
for daemon in cmd_args.daemon:
if not _daemon_restart(daemon):
- print("Failed to restart daemon: {}".format(daemon))
+ print('Failed to restart daemon: {daemon}')
_cleanup()
- sys.exit(1)
+ exit(1)
# reinstall old configuration
_reload_config(daemon)
# cleanup after all actions
_cleanup()
-sys.exit(0)
+exit(0)
diff --git a/src/op_mode/show_dhcp.py b/src/op_mode/show_dhcp.py
index ff1e3cc56..9f65b1bd6 100755
--- a/src/op_mode/show_dhcp.py
+++ b/src/op_mode/show_dhcp.py
@@ -178,7 +178,7 @@ if __name__ == '__main__':
group = parser.add_mutually_exclusive_group()
group.add_argument("-l", "--leases", action="store_true", help="Show DHCP leases")
group.add_argument("-s", "--statistics", action="store_true", help="Show DHCP statistics")
- group.add_argument("--allowed", type=str, choices=["pool", "sort", "state"], help="Show allowed values for argument")
+ group.add_argument("--allowed", type=str, choices=["sort", "state"], help="Show allowed values for argument")
parser.add_argument("-p", "--pool", type=str, help="Show lease for specific pool")
parser.add_argument("-S", "--sort", type=str, default='ip', help="Sort by")
@@ -189,11 +189,7 @@ if __name__ == '__main__':
conf = Config()
- if args.allowed == 'pool':
- if conf.exists_effective('service dhcp-server'):
- print(' '.join(conf.list_effective_nodes("service dhcp-server shared-network-name")))
- exit(0)
- elif args.allowed == 'sort':
+ if args.allowed == 'sort':
print(' '.join(lease_display_fields.keys()))
exit(0)
elif args.allowed == 'state':
diff --git a/src/op_mode/show_interfaces.py b/src/op_mode/show_interfaces.py
index 79bb8e2a6..281d25e30 100755
--- a/src/op_mode/show_interfaces.py
+++ b/src/op_mode/show_interfaces.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
-# Copyright 2017, 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2017-2021 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -19,9 +19,7 @@ import os
import re
import sys
import glob
-import datetime
import argparse
-import netifaces
from vyos.ifconfig import Section
from vyos.ifconfig import Interface
@@ -54,27 +52,27 @@ def filtered_interfaces(ifnames, iftypes, vif, vrrp):
ifnames: a list of interfaces names to consider, empty do not filter
return an instance of the interface class
"""
- allnames = Section.interfaces()
+ if isinstance(iftypes, list):
+ for iftype in iftypes:
+ yield from filtered_interfaces(ifnames, iftype, vif, vrrp)
- vrrp_interfaces = VRRP.active_interfaces() if vrrp else []
-
- for ifname in allnames:
+ for ifname in Section.interfaces(iftypes):
+ # Bail out early if interface name not part of our search list
if ifnames and ifname not in ifnames:
continue
- # return the class which can handle this interface name
- klass = Section.klass(ifname)
- # connect to the interface
- interface = klass(ifname, create=False, debug=False)
-
- if iftypes and interface.definition['section'] not in iftypes:
- continue
+ # As we are only "reading" from the interface - we must use the
+ # generic base class which exposes all the data via a common API
+ interface = Interface(ifname, create=False, debug=False)
+ # VLAN interfaces have a '.' in their name by convention
if vif and not '.' in ifname:
continue
- if vrrp and ifname not in vrrp_interfaces:
- continue
+ if vrrp:
+ vrrp_interfaces = VRRP.active_interfaces()
+ if ifname not in vrrp_interfaces:
+ continue
yield interface
@@ -87,10 +85,8 @@ def split_text(text, used=0):
used: number of characted already used in the screen
"""
no_tty = call('tty -s')
- if no_tty:
- return text.split()
- returned = cmd('stty size')
+ returned = cmd('stty size') if not no_tty else ''
if len(returned) == 2:
rows, columns = [int(_) for _ in returned]
else:
diff --git a/src/op_mode/show_ipsec_sa.py b/src/op_mode/show_ipsec_sa.py
index 8b8f11947..beb632fa8 100755
--- a/src/op_mode/show_ipsec_sa.py
+++ b/src/op_mode/show_ipsec_sa.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# Copyright (C) 2019-2021 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
@@ -29,38 +29,22 @@ def convert(text):
def alphanum_key(key):
return [convert(c) for c in re.split('([0-9]+)', str(key))]
-try:
- session = vici.Session()
- sas = session.list_sas()
-except PermissionError:
- print("You do not have a permission to connect to the IPsec daemon")
- sys.exit(1)
-except ConnectionRefusedError:
- print("IPsec is not runing")
- sys.exit(1)
-except Exception as e:
- print("An error occured: {0}".format(e))
- sys.exit(1)
-
-sa_data = []
-
-for sa in sas:
- # list_sas() returns a list of single-item dicts
- for peer in sa:
- parent_sa = sa[peer]
- child_sas = parent_sa["child-sas"]
- installed_sas = {k: v for k, v in child_sas.items() if v["state"] == b"INSTALLED"}
-
- # parent_sa["state"] = IKE state, child_sas["state"] = ESP state
+def format_output(conns, sas):
+ sa_data = []
+
+ for peer, parent_conn in conns.items():
+ if peer not in sas:
+ continue
+
+ parent_sa = sas[peer]
+ child_sas = parent_sa['child-sas']
+ installed_sas = {v['name'].decode(): v for k, v in child_sas.items() if v["state"] == b"INSTALLED"}
+
+ state = 'down'
+ uptime = 'N/A'
+
if parent_sa["state"] == b"ESTABLISHED" and installed_sas:
state = "up"
- else:
- state = "down"
-
- if state == "up":
- uptime = vyos.util.seconds_to_human(parent_sa["established"].decode())
- else:
- uptime = "N/A"
remote_host = parent_sa["remote-host"].decode()
remote_id = parent_sa["remote-id"].decode()
@@ -69,51 +53,79 @@ for sa in sas:
remote_id = "N/A"
# The counters can only be obtained from the child SAs
- if not installed_sas:
- data = [peer, state, "N/A", "N/A", "N/A", "N/A", "N/A", "N/A"]
- sa_data.append(data)
- else:
- for csa in installed_sas:
- isa = installed_sas[csa]
- csa_name = isa['name']
- csa_name = csa_name.decode()
-
- bytes_in = hurry.filesize.size(int(isa["bytes-in"].decode()))
- bytes_out = hurry.filesize.size(int(isa["bytes-out"].decode()))
- bytes_str = "{0}/{1}".format(bytes_in, bytes_out)
-
- pkts_in = hurry.filesize.size(int(isa["packets-in"].decode()), system=hurry.filesize.si)
- pkts_out = hurry.filesize.size(int(isa["packets-out"].decode()), system=hurry.filesize.si)
- pkts_str = "{0}/{1}".format(pkts_in, pkts_out)
- # Remove B from <1K values
- pkts_str = re.sub(r'B', r'', pkts_str)
-
- enc = isa["encr-alg"].decode()
- if "encr-keysize" in isa:
- key_size = isa["encr-keysize"].decode()
- else:
- key_size = ""
- if "integ-alg" in isa:
- hash = isa["integ-alg"].decode()
- else:
- hash = ""
- if "dh-group" in isa:
- dh_group = isa["dh-group"].decode()
- else:
- dh_group = ""
-
- proposal = enc
- if key_size:
- proposal = "{0}_{1}".format(proposal, key_size)
- if hash:
- proposal = "{0}/{1}".format(proposal, hash)
- if dh_group:
- proposal = "{0}/{1}".format(proposal, dh_group)
-
- data = [csa_name, state, uptime, bytes_str, pkts_str, remote_host, remote_id, proposal]
+ for child_conn in parent_conn['children']:
+ if child_conn not in installed_sas:
+ data = [child_conn, "down", "N/A", "N/A", "N/A", "N/A", "N/A", "N/A"]
sa_data.append(data)
-
-headers = ["Connection", "State", "Uptime", "Bytes In/Out", "Packets In/Out", "Remote address", "Remote ID", "Proposal"]
-sa_data = sorted(sa_data, key=alphanum_key)
-output = tabulate.tabulate(sa_data, headers)
-print(output)
+ continue
+
+ isa = installed_sas[child_conn]
+ csa_name = isa['name']
+ csa_name = csa_name.decode()
+
+ bytes_in = hurry.filesize.size(int(isa["bytes-in"].decode()))
+ bytes_out = hurry.filesize.size(int(isa["bytes-out"].decode()))
+ bytes_str = "{0}/{1}".format(bytes_in, bytes_out)
+
+ pkts_in = hurry.filesize.size(int(isa["packets-in"].decode()), system=hurry.filesize.si)
+ pkts_out = hurry.filesize.size(int(isa["packets-out"].decode()), system=hurry.filesize.si)
+ pkts_str = "{0}/{1}".format(pkts_in, pkts_out)
+ # Remove B from <1K values
+ pkts_str = re.sub(r'B', r'', pkts_str)
+
+ uptime = vyos.util.seconds_to_human(isa['install-time'].decode())
+
+ enc = isa["encr-alg"].decode()
+ if "encr-keysize" in isa:
+ key_size = isa["encr-keysize"].decode()
+ else:
+ key_size = ""
+ if "integ-alg" in isa:
+ hash = isa["integ-alg"].decode()
+ else:
+ hash = ""
+ if "dh-group" in isa:
+ dh_group = isa["dh-group"].decode()
+ else:
+ dh_group = ""
+
+ proposal = enc
+ if key_size:
+ proposal = "{0}_{1}".format(proposal, key_size)
+ if hash:
+ proposal = "{0}/{1}".format(proposal, hash)
+ if dh_group:
+ proposal = "{0}/{1}".format(proposal, dh_group)
+
+ data = [csa_name, state, uptime, bytes_str, pkts_str, remote_host, remote_id, proposal]
+ sa_data.append(data)
+ return sa_data
+
+if __name__ == '__main__':
+ try:
+ session = vici.Session()
+ conns = {}
+ sas = {}
+
+ for conn in session.list_conns():
+ for key in conn:
+ conns[key] = conn[key]
+
+ for sa in session.list_sas():
+ for key in sa:
+ sas[key] = sa[key]
+
+ headers = ["Connection", "State", "Uptime", "Bytes In/Out", "Packets In/Out", "Remote address", "Remote ID", "Proposal"]
+ sa_data = format_output(conns, sas)
+ sa_data = sorted(sa_data, key=alphanum_key)
+ output = tabulate.tabulate(sa_data, headers)
+ print(output)
+ except PermissionError:
+ print("You do not have a permission to connect to the IPsec daemon")
+ sys.exit(1)
+ except ConnectionRefusedError:
+ print("IPsec is not runing")
+ sys.exit(1)
+ except Exception as e:
+ print("An error occured: {0}".format(e))
+ sys.exit(1)
diff --git a/src/op_mode/show_system_integrity.py b/src/op_mode/show_system_integrity.py
deleted file mode 100755
index c34d41e80..000000000
--- a/src/op_mode/show_system_integrity.py
+++ /dev/null
@@ -1,70 +0,0 @@
-#!/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 sys
-import os
-import re
-import json
-from datetime import datetime, timedelta
-
-version_file = r'/usr/share/vyos/version.json'
-
-
-def _get_sys_build_version():
- if not os.path.exists(version_file):
- return None
- buf = open(version_file, 'r').read()
- j = json.loads(buf)
- if not 'built_on' in j:
- return None
- return datetime.strptime(j['built_on'], '%a %d %b %Y %H:%M %Z')
-
-
-def _check_pkgs(build_stamp):
- pkg_diffs = {
- 'buildtime': str(build_stamp),
- 'pkg': {}
- }
-
- pkg_info = os.listdir('/var/lib/dpkg/info/')
- for file in pkg_info:
- if re.search('\.list$', file):
- fts = os.stat('/var/lib/dpkg/info/' + file).st_mtime
- dt_str = (datetime.utcfromtimestamp(
- fts).strftime('%Y-%m-%d %H:%M:%S'))
- fdt = datetime.strptime(dt_str, '%Y-%m-%d %H:%M:%S')
- if fdt > build_stamp:
- pkg_diffs['pkg'].update(
- {str(re.sub('\.list', '', file)): str(fdt)})
-
- if len(pkg_diffs['pkg']) != 0:
- return pkg_diffs
- else:
- return None
-
-
-if __name__ == '__main__':
- built_date = _get_sys_build_version()
- if not built_date:
- sys.exit(1)
- pkgs = _check_pkgs(built_date)
- if pkgs:
- print (
- "The following packages don\'t fit the image creation time\nbuild time:\t" + pkgs['buildtime'])
- for k, v in pkgs['pkg'].items():
- print ("installed: " + v + '\t' + k)
diff --git a/src/op_mode/show_version.py b/src/op_mode/show_version.py
index 5bbc2e1f1..7962e1e7b 100755
--- a/src/op_mode/show_version.py
+++ b/src/op_mode/show_version.py
@@ -32,12 +32,12 @@ parser.add_argument("-j", "--json", action="store_true", help="Produce JSON outp
version_output_tmpl = """
Version: VyOS {{version}}
-Release Train: {{release_train}}
+Release train: {{release_train}}
Built by: {{built_by}}
Built on: {{built_on}}
Build UUID: {{build_uuid}}
-Build Commit ID: {{build_git}}
+Build commit ID: {{build_git}}
Architecture: {{system_arch}}
Boot via: {{boot_via}}
diff --git a/src/op_mode/show_wwan.py b/src/op_mode/show_wwan.py
index 249dda2a5..529b5bd0f 100755
--- a/src/op_mode/show_wwan.py
+++ b/src/op_mode/show_wwan.py
@@ -34,13 +34,17 @@ required = parser.add_argument_group('Required arguments')
required.add_argument("--interface", help="WWAN interface name, e.g. wwan0", required=True)
def qmi_cmd(device, command, silent=False):
- tmp = cmd(f'qmicli --device={device} --device-open-proxy {command}')
- tmp = tmp.replace(f'[{cdc}] ', '')
- if not silent:
- # skip first line as this only holds the info headline
- for line in tmp.splitlines()[1:]:
- print(line.lstrip())
- return tmp
+ try:
+ tmp = cmd(f'qmicli --device={device} --device-open-proxy {command}')
+ tmp = tmp.replace(f'[{cdc}] ', '')
+ if not silent:
+ # skip first line as this only holds the info headline
+ for line in tmp.splitlines()[1:]:
+ print(line.lstrip())
+ return tmp
+ except:
+ print('Command not supported by Modem')
+ exit(1)
if __name__ == '__main__':
args = parser.parse_args()
diff --git a/src/services/api/graphql/README.graphql b/src/services/api/graphql/README.graphql
new file mode 100644
index 000000000..c91b70782
--- /dev/null
+++ b/src/services/api/graphql/README.graphql
@@ -0,0 +1,140 @@
+
+Example using GraphQL mutations to configure a DHCP server:
+
+This assumes that the http-api is running:
+
+'set service https api'
+
+One can configure an address on an interface, and configure the DHCP server
+to run with that address as default router by requesting these 'mutations'
+in the GraphQL playground:
+
+mutation {
+ createInterfaceEthernet (data: {interface: "eth1",
+ address: "192.168.0.1/24",
+ description: "BOB"}) {
+ success
+ errors
+ data {
+ address
+ }
+ }
+}
+
+mutation {
+ createDhcpServer(data: {sharedNetworkName: "BOB",
+ subnet: "192.168.0.0/24",
+ defaultRouter: "192.168.0.1",
+ nameServer: "192.168.0.1",
+ domainName: "vyos.net",
+ lease: 86400,
+ range: 0,
+ start: "192.168.0.9",
+ stop: "192.168.0.254",
+ dnsForwardingAllowFrom: "192.168.0.0/24",
+ dnsForwardingCacheSize: 0,
+ dnsForwardingListenAddress: "192.168.0.1"}) {
+ success
+ errors
+ data {
+ defaultRouter
+ }
+ }
+}
+
+mutation {
+ saveConfigFile(data: {fileName: "/config/config.boot"}) {
+ success
+ errors
+ data {
+ fileName
+ }
+ }
+}
+
+N.B. fileName can be empty (fileName: "") or data can be empty (data: {}) to save to
+/config/config.boot; to save to an alternative path, specify fileName.
+
+mutation {
+ loadConfigFile(data: {fileName: "/home/vyos/config.boot"}) {
+ success
+ errors
+ data {
+ fileName
+ }
+ }
+}
+
+
+The GraphQL playground will be found at:
+
+https://{{ host_address }}/graphql
+
+An equivalent curl command to the first example above would be:
+
+curl -k 'https://192.168.100.168/graphql' -H 'Content-Type: application/json' --data-binary '{"query": "mutation {createInterfaceEthernet (data: {interface: \"eth1\", address: \"192.168.0.1/24\", description: \"BOB\"}) {success errors data {address}}}"}'
+
+Note that the 'mutation' term is prefaced by 'query' in the curl command.
+
+What's here:
+
+services
+├── api
+│   └── graphql
+│   ├── graphql
+│   │   ├── directives.py
+│   │   ├── __init__.py
+│   │   ├── mutations.py
+│   │   └── schema
+│   │   ├── dhcp_server.graphql
+│   │   ├── interface_ethernet.graphql
+│   │   └── schema.graphql
+│   ├── recipes
+│   │   ├── dhcp_server.py
+│   │   ├── __init__.py
+│   │   ├── interface_ethernet.py
+│   │   ├── recipe.py
+│   │   └── templates
+│   │   ├── dhcp_server.tmpl
+│   │   └── interface_ethernet.tmpl
+│   └── state.py
+├── vyos-configd
+├── vyos-hostsd
+└── vyos-http-api-server
+
+The GraphQL library that we are using, Ariadne, advertises itself as a
+'schema-first' implementation: define the schema; define resolvers
+(handlers) for declared Query and Mutation types (Subscription types are not
+currently used).
+
+In the current approach to a high-level API, we consider the
+Jinja2-templated collection of configuration mode 'set'/'delete' commands as
+the Ur-data; the GraphQL schema is produced from those files, located in
+'api/graphql/recipes/templates'.
+
+Resolvers for the schema Mutation fields are dynamically generated using a
+'directive' added to the respective schema field. The directive,
+'@generate', is handled by the class 'DataDirective' in
+'api/graphql/graphql/directives.py', which calls the 'make_resolver' function in
+'api/graphql/graphql/mutations.py'; the produced resolver calls the appropriate
+wrapper in 'api/graphql/recipes', with base class doing the (overridable)
+configuration steps of calling all defined 'set'/'delete' commands.
+
+Integrating the above with vyos-http-api-server is ~10 lines of code.
+
+What needs to be done:
+
+• automate generation of schema and wrappers from templated configuration
+commands
+
+• investigate whether the subclassing provided by the named wrappers in
+'api/graphql/recipes' is sufficient for use cases which need to modify data
+
+• encapsulate the manipulation of 'canonical names' which transforms the
+prefixed camel-case schema names to various snake-case file/function names
+
+• consider mechanism for migration of templates: offline vs. on-the-fly
+
+• define the naming convention for those schema fields that refer to
+configuration mode parameters: e.g. how much of the path is needed as prefix
+to uniquely define the term
diff --git a/src/services/api/graphql/bindings.py b/src/services/api/graphql/bindings.py
new file mode 100644
index 000000000..c123f68d8
--- /dev/null
+++ b/src/services/api/graphql/bindings.py
@@ -0,0 +1,14 @@
+import vyos.defaults
+from . graphql.mutations import mutation
+from . graphql.directives import DataDirective, ConfigFileDirective
+
+from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers
+
+def generate_schema():
+ api_schema_dir = vyos.defaults.directories['api_schema']
+
+ type_defs = load_schema_from_path(api_schema_dir)
+
+ schema = make_executable_schema(type_defs, mutation, snake_case_fallback_resolvers, directives={"generate": DataDirective, "configfile": ConfigFileDirective})
+
+ return schema
diff --git a/src/services/api/graphql/graphql/__init__.py b/src/services/api/graphql/graphql/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/services/api/graphql/graphql/__init__.py
diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py
new file mode 100644
index 000000000..85d514de4
--- /dev/null
+++ b/src/services/api/graphql/graphql/directives.py
@@ -0,0 +1,32 @@
+from ariadne import SchemaDirectiveVisitor, ObjectType
+from . mutations import make_resolver, make_config_file_resolver
+
+class DataDirective(SchemaDirectiveVisitor):
+ """
+ Class providing implementation of 'generate' directive in schema.
+
+ """
+ def visit_field_definition(self, field, object_type):
+ name = f'{field.type}'
+ # field.type contains the return value of the mutation; trim value
+ # to produce canonical name
+ name = name.replace('Result', '', 1)
+
+ func = make_resolver(name)
+ field.resolve = func
+ return field
+
+class ConfigFileDirective(SchemaDirectiveVisitor):
+ """
+ Class providing implementation of 'configfile' directive in schema.
+
+ """
+ def visit_field_definition(self, field, object_type):
+ name = f'{field.type}'
+ # field.type contains the return value of the mutation; trim value
+ # to produce canonical name
+ name = name.replace('Result', '', 1)
+
+ func = make_config_file_resolver(name)
+ field.resolve = func
+ return field
diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py
new file mode 100644
index 000000000..2eb0a0b4a
--- /dev/null
+++ b/src/services/api/graphql/graphql/mutations.py
@@ -0,0 +1,109 @@
+
+from importlib import import_module
+from typing import Any, Dict
+from ariadne import ObjectType, convert_kwargs_to_snake_case, convert_camel_case_to_snake
+from graphql import GraphQLResolveInfo
+from makefun import with_signature
+
+from .. import state
+
+mutation = ObjectType("Mutation")
+
+def make_resolver(mutation_name):
+ """Dynamically generate a resolver for the mutation named in the
+ schema by 'mutation_name'.
+
+ Dynamic generation is provided using the package 'makefun' (via the
+ decorator 'with_signature'), which provides signature-preserving
+ function wrappers; it provides several improvements over, say,
+ functools.wraps.
+
+ :raise Exception:
+ encapsulating ConfigErrors, or internal errors
+ """
+ class_name = mutation_name.replace('create', '', 1).replace('delete', '', 1)
+ func_base_name = convert_camel_case_to_snake(class_name)
+ resolver_name = f'resolve_create_{func_base_name}'
+ func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict)'
+
+ @mutation.field(mutation_name)
+ @convert_kwargs_to_snake_case
+ @with_signature(func_sig, func_name=resolver_name)
+ async def func_impl(*args, **kwargs):
+ try:
+ if 'data' not in kwargs:
+ return {
+ "success": False,
+ "errors": ['missing data']
+ }
+
+ data = kwargs['data']
+ session = state.settings['app'].state.vyos_session
+
+ mod = import_module(f'api.graphql.recipes.{func_base_name}')
+ klass = getattr(mod, class_name)
+ k = klass(session, data)
+ k.configure()
+
+ return {
+ "success": True,
+ "data": data
+ }
+ except Exception as error:
+ return {
+ "success": False,
+ "errors": [str(error)]
+ }
+
+ return func_impl
+
+def make_config_file_resolver(mutation_name):
+ op = ''
+ if 'save' in mutation_name:
+ op = 'save'
+ elif 'load' in mutation_name:
+ op = 'load'
+
+ class_name = mutation_name.replace('save', '', 1).replace('load', '', 1)
+ func_base_name = convert_camel_case_to_snake(class_name)
+ resolver_name = f'resolve_{func_base_name}'
+ func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict)'
+
+ @mutation.field(mutation_name)
+ @convert_kwargs_to_snake_case
+ @with_signature(func_sig, func_name=resolver_name)
+ async def func_impl(*args, **kwargs):
+ try:
+ if 'data' not in kwargs:
+ return {
+ "success": False,
+ "errors": ['missing data']
+ }
+
+ data = kwargs['data']
+ session = state.settings['app'].state.vyos_session
+
+ mod = import_module(f'api.graphql.recipes.{func_base_name}')
+ klass = getattr(mod, class_name)
+ k = klass(session, data)
+ if op == 'save':
+ k.save()
+ elif op == 'load':
+ k.load()
+ else:
+ return {
+ "success": False,
+ "errors": ["Input must be saveConfigFile | loadConfigFile"]
+ }
+
+ return {
+ "success": True,
+ "data": data
+ }
+ except Exception as error:
+ return {
+ "success": False,
+ "errors": [str(error)]
+ }
+
+ return func_impl
diff --git a/src/services/api/graphql/graphql/schema/config_file.graphql b/src/services/api/graphql/graphql/schema/config_file.graphql
new file mode 100644
index 000000000..3096cf743
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/config_file.graphql
@@ -0,0 +1,27 @@
+input saveConfigFileInput {
+ fileName: String
+}
+
+type saveConfigFile {
+ fileName: String
+}
+
+type saveConfigFileResult {
+ data: saveConfigFile
+ success: Boolean!
+ errors: [String]
+}
+
+input loadConfigFileInput {
+ fileName: String!
+}
+
+type loadConfigFile {
+ fileName: String!
+}
+
+type loadConfigFileResult {
+ data: loadConfigFile
+ success: Boolean!
+ errors: [String]
+}
diff --git a/src/services/api/graphql/graphql/schema/dhcp_server.graphql b/src/services/api/graphql/graphql/schema/dhcp_server.graphql
new file mode 100644
index 000000000..9f741a0a5
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/dhcp_server.graphql
@@ -0,0 +1,35 @@
+input dhcpServerConfigInput {
+ sharedNetworkName: String
+ subnet: String
+ defaultRouter: String
+ nameServer: String
+ domainName: String
+ lease: Int
+ range: Int
+ start: String
+ stop: String
+ dnsForwardingAllowFrom: String
+ dnsForwardingCacheSize: Int
+ dnsForwardingListenAddress: String
+}
+
+type dhcpServerConfig {
+ sharedNetworkName: String
+ subnet: String
+ defaultRouter: String
+ nameServer: String
+ domainName: String
+ lease: Int
+ range: Int
+ start: String
+ stop: String
+ dnsForwardingAllowFrom: String
+ dnsForwardingCacheSize: Int
+ dnsForwardingListenAddress: String
+}
+
+type createDhcpServerResult {
+ data: dhcpServerConfig
+ success: Boolean!
+ errors: [String]
+}
diff --git a/src/services/api/graphql/graphql/schema/interface_ethernet.graphql b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql
new file mode 100644
index 000000000..fdcf97bad
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql
@@ -0,0 +1,18 @@
+input interfaceEthernetConfigInput {
+ interface: String
+ address: String
+ replace: Boolean = true
+ description: String
+}
+
+type interfaceEthernetConfig {
+ interface: String
+ address: String
+ description: String
+}
+
+type createInterfaceEthernetResult {
+ data: interfaceEthernetConfig
+ success: Boolean!
+ errors: [String]
+}
diff --git a/src/services/api/graphql/graphql/schema/schema.graphql b/src/services/api/graphql/graphql/schema/schema.graphql
new file mode 100644
index 000000000..70fe0d726
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/schema.graphql
@@ -0,0 +1,18 @@
+schema {
+ query: Query
+ mutation: Mutation
+}
+
+type Query {
+ _dummy: String
+}
+
+directive @generate on FIELD_DEFINITION
+directive @configfile on FIELD_DEFINITION
+
+type Mutation {
+ createDhcpServer(data: dhcpServerConfigInput) : createDhcpServerResult @generate
+ createInterfaceEthernet(data: interfaceEthernetConfigInput) : createInterfaceEthernetResult @generate
+ saveConfigFile(data: saveConfigFileInput) : saveConfigFileResult @configfile
+ loadConfigFile(data: loadConfigFileInput) : loadConfigFileResult @configfile
+}
diff --git a/src/services/api/graphql/recipes/__init__.py b/src/services/api/graphql/recipes/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/services/api/graphql/recipes/__init__.py
diff --git a/src/services/api/graphql/recipes/config_file.py b/src/services/api/graphql/recipes/config_file.py
new file mode 100644
index 000000000..850e5326e
--- /dev/null
+++ b/src/services/api/graphql/recipes/config_file.py
@@ -0,0 +1,16 @@
+
+from . recipe import Recipe
+
+class ConfigFile(Recipe):
+ def __init__(self, session, command_file):
+ super().__init__(session, command_file)
+
+ # Define any custom processing of parameters here by overriding
+ # save/load:
+ #
+ # def save(self):
+ # self.data = transform_data(self.data)
+ # super().save()
+ # def load(self):
+ # self.data = transform_data(self.data)
+ # super().load()
diff --git a/src/services/api/graphql/recipes/dhcp_server.py b/src/services/api/graphql/recipes/dhcp_server.py
new file mode 100644
index 000000000..3edb3028e
--- /dev/null
+++ b/src/services/api/graphql/recipes/dhcp_server.py
@@ -0,0 +1,13 @@
+
+from . recipe import Recipe
+
+class DhcpServer(Recipe):
+ def __init__(self, session, command_file):
+ super().__init__(session, command_file)
+
+ # Define any custom processing of parameters here by overriding
+ # configure:
+ #
+ # def configure(self):
+ # self.data = transform_data(self.data)
+ # super().configure()
diff --git a/src/services/api/graphql/recipes/interface_ethernet.py b/src/services/api/graphql/recipes/interface_ethernet.py
new file mode 100644
index 000000000..f88f5924f
--- /dev/null
+++ b/src/services/api/graphql/recipes/interface_ethernet.py
@@ -0,0 +1,13 @@
+
+from . recipe import Recipe
+
+class InterfaceEthernet(Recipe):
+ def __init__(self, session, command_file):
+ super().__init__(session, command_file)
+
+ # Define any custom processing of parameters here by overriding
+ # configure:
+ #
+ # def configure(self):
+ # self.data = transform_data(self.data)
+ # super().configure()
diff --git a/src/services/api/graphql/recipes/recipe.py b/src/services/api/graphql/recipes/recipe.py
new file mode 100644
index 000000000..91d8bd67a
--- /dev/null
+++ b/src/services/api/graphql/recipes/recipe.py
@@ -0,0 +1,68 @@
+from ariadne import convert_camel_case_to_snake
+import vyos.defaults
+from vyos.template import render
+
+class Recipe(object):
+ def __init__(self, session, data):
+ self._session = session
+ self.data = data
+ self._name = convert_camel_case_to_snake(type(self).__name__)
+
+ @property
+ def data(self):
+ return self.__data
+
+ @data.setter
+ def data(self, data):
+ if isinstance(data, dict):
+ self.__data = data
+ else:
+ raise ValueError("data must be of type dict")
+
+ def configure(self):
+ session = self._session
+ data = self.data
+ func_base_name = self._name
+
+ tmpl_file = f'{func_base_name}.tmpl'
+ cmd_file = f'/tmp/{func_base_name}.cmds'
+ tmpl_dir = vyos.defaults.directories['api_templates']
+
+ try:
+ render(cmd_file, tmpl_file, data, location=tmpl_dir)
+ commands = []
+ with open(cmd_file) as f:
+ lines = f.readlines()
+ for line in lines:
+ commands.append(line.split())
+ for cmd in commands:
+ if cmd[0] == 'set':
+ session.set(cmd[1:])
+ elif cmd[0] == 'delete':
+ session.delete(cmd[1:])
+ else:
+ raise ValueError('Operation must be "set" or "delete"')
+ session.commit()
+ except Exception as error:
+ raise error
+
+ def save(self):
+ session = self._session
+ data = self.data
+ if 'file_name' not in data or not data['file_name']:
+ data['file_name'] = '/config/config.boot'
+
+ try:
+ session.save_config(data['file_name'])
+ except Exception as error:
+ raise error
+
+ def load(self):
+ session = self._session
+ data = self.data
+
+ try:
+ session.load_config(data['file_name'])
+ session.commit()
+ except Exception as error:
+ raise error
diff --git a/src/services/api/graphql/recipes/templates/dhcp_server.tmpl b/src/services/api/graphql/recipes/templates/dhcp_server.tmpl
new file mode 100644
index 000000000..70de43183
--- /dev/null
+++ b/src/services/api/graphql/recipes/templates/dhcp_server.tmpl
@@ -0,0 +1,9 @@
+set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} default-router {{ default_router }}
+set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} name-server {{ name_server }}
+set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} domain-name {{ domain_name }}
+set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} lease {{ lease }}
+set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} range {{ range }} start {{ start }}
+set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} range {{ range }} stop {{ stop }}
+set service dns forwarding allow-from {{ dns_forwarding_allow_from }}
+set service dns forwarding cache-size {{ dns_forwarding_cache_size }}
+set service dns forwarding listen-address {{ dns_forwarding_listen_address }}
diff --git a/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl b/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl
new file mode 100644
index 000000000..d9d7ed691
--- /dev/null
+++ b/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl
@@ -0,0 +1,5 @@
+{% if replace %}
+delete interfaces ethernet {{ interface }} address
+{% endif %}
+set interfaces ethernet {{ interface }} address {{ address }}
+set interfaces ethernet {{ interface }} description {{ description }}
diff --git a/src/services/api/graphql/state.py b/src/services/api/graphql/state.py
new file mode 100644
index 000000000..63db9f4ef
--- /dev/null
+++ b/src/services/api/graphql/state.py
@@ -0,0 +1,4 @@
+
+def init():
+ global settings
+ settings = {}
diff --git a/src/services/vyos-configd b/src/services/vyos-configd
index 6f770b696..670b6e66a 100755
--- a/src/services/vyos-configd
+++ b/src/services/vyos-configd
@@ -133,8 +133,7 @@ def explicit_print(path, mode, msg):
logger.critical("error explicit_print")
def run_script(script, config, args) -> int:
- if args:
- script.argv = args
+ script.argv = args
config.set_level([])
try:
c = script.get_config(config)
@@ -208,7 +207,7 @@ def process_node_data(config, data) -> int:
return R_ERROR_DAEMON
script_name = None
- args = None
+ args = []
res = re.match(r'^(VYOS_TAGNODE_VALUE=[^/]+)?.*\/([^/]+).py(.*)', data)
if res.group(1):
@@ -221,7 +220,7 @@ def process_node_data(config, data) -> int:
return R_ERROR_DAEMON
if res.group(3):
args = res.group(3).split()
- args.insert(0, f'{script_name}.py')
+ args.insert(0, f'{script_name}.py')
if script_name not in include_set:
return R_PASS
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
index 703628558..aa7ac6708 100755
--- a/src/services/vyos-http-api-server
+++ b/src/services/vyos-http-api-server
@@ -1,6 +1,6 @@
-#!/usr/bin/env python3
+#!/usr/share/vyos-http-api-tools/bin/python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# Copyright (C) 2019-2021 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
@@ -19,25 +19,43 @@
import os
import sys
import grp
+import copy
import json
+import logging
import traceback
import threading
-import signal
+from typing import List, Union, Callable, Dict
-import vyos.config
-
-from flask import Flask, request
-from waitress import serve
+import uvicorn
+from fastapi import FastAPI, Depends, Request, Response, HTTPException
+from fastapi.responses import HTMLResponse
+from fastapi.exceptions import RequestValidationError
+from fastapi.routing import APIRoute
+from pydantic import BaseModel, StrictStr, validator
+from starlette.datastructures import FormData
+from starlette.formparsers import FormParser, MultiPartParser
+from multipart.multipart import parse_options_header
-from functools import wraps
+from ariadne.asgi import GraphQL
+import vyos.config
from vyos.configsession import ConfigSession, ConfigSessionError
+import api.graphql.state
DEFAULT_CONFIG_FILE = '/etc/vyos/http-api.conf'
CFG_GROUP = 'vyattacfg'
-app = Flask(__name__)
+debug = True
+
+logger = logging.getLogger(__name__)
+logs_handler = logging.StreamHandler()
+logger.addHandler(logs_handler)
+
+if debug:
+ logger.setLevel(logging.DEBUG)
+else:
+ logger.setLevel(logging.INFO)
# Giant lock!
lock = threading.Lock()
@@ -48,63 +66,347 @@ def load_server_config():
return config
def check_auth(key_list, key):
- id = None
+ key_id = None
for k in key_list:
if k['key'] == key:
- id = k['id']
- return id
+ key_id = k['id']
+ return key_id
def error(code, msg):
resp = {"success": False, "error": msg, "data": None}
- return json.dumps(resp), code
+ resp = json.dumps(resp)
+ return HTMLResponse(resp, status_code=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 = request.form.get("key")
- api_keys = app.config['vyos_keys']
- id = check_auth(api_keys, key)
- if not id:
- return error(401, "Valid API key is required")
- return f(*args, **kwargs)
-
- return decorated_function
-
-@app.route('/configure', methods=['POST'])
-@get_command
-@auth_required
-def configure_op(commands):
- session = app.config['vyos_session']
+ resp = json.dumps(resp)
+ return HTMLResponse(resp)
+
+# Pydantic models for validation
+# Pydantic will cast when possible, so use StrictStr
+# validators added as needed for additional constraints
+# schema_extra adds anotations to OpenAPI, to add examples
+
+class ApiModel(BaseModel):
+ key: StrictStr
+
+class BaseConfigureModel(BaseModel):
+ op: StrictStr
+ path: List[StrictStr]
+ value: StrictStr = None
+
+ @validator("path", pre=True, always=True)
+ def check_non_empty(cls, path):
+ assert len(path) > 0
+ return path
+
+class ConfigureModel(ApiModel):
+ op: StrictStr
+ path: List[StrictStr]
+ value: StrictStr = None
+
+ @validator("path", pre=True, always=True)
+ def check_non_empty(cls, path):
+ assert len(path) > 0
+ return path
+
+ class Config:
+ schema_extra = {
+ "example": {
+ "key": "id_key",
+ "op": "set | delete | comment",
+ "path": ['config', 'mode', 'path'],
+ }
+ }
+
+class ConfigureListModel(ApiModel):
+ commands: List[BaseConfigureModel]
+
+ class Config:
+ schema_extra = {
+ "example": {
+ "key": "id_key",
+ "commands": "list of commands",
+ }
+ }
+
+class RetrieveModel(ApiModel):
+ op: StrictStr
+ path: List[StrictStr]
+ configFormat: StrictStr = None
+
+ class Config:
+ schema_extra = {
+ "example": {
+ "key": "id_key",
+ "op": "returnValue | returnValues | exists | showConfig",
+ "path": ['config', 'mode', 'path'],
+ "configFormat": "json (default) | json_ast | raw",
+
+ }
+ }
+
+class ConfigFileModel(ApiModel):
+ op: StrictStr
+ file: StrictStr = None
+
+ class Config:
+ schema_extra = {
+ "example": {
+ "key": "id_key",
+ "op": "save | load",
+ "file": "filename",
+ }
+ }
+
+class ImageModel(ApiModel):
+ op: StrictStr
+ url: StrictStr = None
+ name: StrictStr = None
+
+ class Config:
+ schema_extra = {
+ "example": {
+ "key": "id_key",
+ "op": "add | delete",
+ "url": "imagelocation",
+ "name": "imagename",
+ }
+ }
+
+class GenerateModel(ApiModel):
+ op: StrictStr
+ path: List[StrictStr]
+
+ class Config:
+ schema_extra = {
+ "example": {
+ "key": "id_key",
+ "op": "generate",
+ "path": ["op", "mode", "path"],
+ }
+ }
+
+class ShowModel(ApiModel):
+ op: StrictStr
+ path: List[StrictStr]
+
+ class Config:
+ schema_extra = {
+ "example": {
+ "key": "id_key",
+ "op": "show",
+ "path": ["op", "mode", "path"],
+ }
+ }
+
+class Success(BaseModel):
+ success: bool
+ data: Union[str, bool, Dict]
+ error: str
+
+class Error(BaseModel):
+ success: bool = False
+ data: Union[str, bool, Dict]
+ error: str
+
+responses = {
+ 200: {'model': Success},
+ 400: {'model': Error},
+ 422: {'model': Error, 'description': 'Validation Error'},
+ 500: {'model': Error}
+}
+
+def auth_required(data: ApiModel):
+ key = data.key
+ api_keys = app.state.vyos_keys
+ key_id = check_auth(api_keys, key)
+ if not key_id:
+ raise HTTPException(status_code=401, detail="Valid API key is required")
+ app.state.vyos_id = key_id
+
+# override Request and APIRoute classes in order to convert form request to json;
+# do all explicit validation here, for backwards compatability of error messages;
+# the explicit validation may be dropped, if desired, in favor of native
+# validation by FastAPI/Pydantic, as is used for application/json requests
+class MultipartRequest(Request):
+ ERR_MISSING_KEY = False
+ ERR_MISSING_DATA = False
+ ERR_NOT_JSON = False
+ ERR_NOT_DICT = False
+ ERR_NO_OP = False
+ ERR_NO_PATH = False
+ ERR_EMPTY_PATH = False
+ ERR_PATH_NOT_LIST = False
+ ERR_VALUE_NOT_STRING = False
+ ERR_PATH_NOT_LIST_OF_STR = False
+ offending_command = {}
+ exception = None
+
+ @property
+ def orig_headers(self):
+ self._orig_headers = super().headers
+ return self._orig_headers
+
+ @property
+ def headers(self):
+ self._headers = super().headers.mutablecopy()
+ self._headers['content-type'] = 'application/json'
+ return self._headers
+
+ async def form(self) -> FormData:
+ if not hasattr(self, "_form"):
+ assert (
+ parse_options_header is not None
+ ), "The `python-multipart` library must be installed to use form parsing."
+ content_type_header = self.orig_headers.get("Content-Type")
+ content_type, options = parse_options_header(content_type_header)
+ if content_type == b"multipart/form-data":
+ multipart_parser = MultiPartParser(self.orig_headers, self.stream())
+ self._form = await multipart_parser.parse()
+ elif content_type == b"application/x-www-form-urlencoded":
+ form_parser = FormParser(self.orig_headers, self.stream())
+ self._form = await form_parser.parse()
+ else:
+ self._form = FormData()
+ return self._form
+
+ async def body(self) -> bytes:
+ if not hasattr(self, "_body"):
+ forms = {}
+ merge = {}
+ body = await super().body()
+ self._body = body
+
+ form_data = await self.form()
+ if form_data:
+ logger.debug("processing form data")
+ for k, v in form_data.multi_items():
+ forms[k] = v
+
+ if 'data' not in forms:
+ self.ERR_MISSING_DATA = True
+ else:
+ try:
+ tmp = json.loads(forms['data'])
+ except json.JSONDecodeError as e:
+ self.ERR_NOT_JSON = True
+ self.exception = e
+ tmp = {}
+ if isinstance(tmp, list):
+ merge['commands'] = tmp
+ else:
+ merge = tmp
+
+ if 'commands' in merge:
+ cmds = merge['commands']
+ else:
+ cmds = copy.deepcopy(merge)
+ cmds = [cmds]
+
+ for c in cmds:
+ if not isinstance(c, dict):
+ self.ERR_NOT_DICT = True
+ self.offending_command = c
+ elif 'op' not in c:
+ self.ERR_NO_OP = True
+ self.offending_command = c
+ elif 'path' not in c:
+ self.ERR_NO_PATH = True
+ self.offending_command = c
+ elif not c['path']:
+ self.ERR_EMPTY_PATH = True
+ self.offending_command = c
+ elif not isinstance(c['path'], list):
+ self.ERR_PATH_NOT_LIST = True
+ self.offending_command = c
+ elif not all(isinstance(el, str) for el in c['path']):
+ self.ERR_PATH_NOT_LIST_OF_STR = True
+ self.offending_command = c
+ elif 'value' in c and not isinstance(c['value'], str):
+ self.ERR_VALUE_NOT_STRING = True
+ self.offending_command = c
+
+ if 'key' not in forms and 'key' not in merge:
+ self.ERR_MISSING_KEY = True
+ if 'key' in forms and 'key' not in merge:
+ merge['key'] = forms['key']
+
+ new_body = json.dumps(merge)
+ new_body = new_body.encode()
+ self._body = new_body
+
+ return self._body
+
+class MultipartRoute(APIRoute):
+ def get_route_handler(self) -> Callable:
+ original_route_handler = super().get_route_handler()
+
+ async def custom_route_handler(request: Request) -> Response:
+ request = MultipartRequest(request.scope, request.receive)
+ endpoint = request.url.path
+ try:
+ response: Response = await original_route_handler(request)
+ except HTTPException as e:
+ return error(e.status_code, e.detail)
+ except Exception as e:
+ if request.ERR_MISSING_KEY:
+ return error(422, "Valid API key is required")
+ if request.ERR_MISSING_DATA:
+ return error(422, "Non-empty data field is required")
+ if request.ERR_NOT_JSON:
+ return error(400, "Failed to parse JSON: {0}".format(request.exception))
+ if endpoint == '/configure':
+ if request.ERR_NOT_DICT:
+ return error(400, "Malformed command \"{0}\": any command must be a dict".format(json.dumps(request.offending_command)))
+ if request.ERR_NO_OP:
+ return error(400, "Malformed command \"{0}\": missing \"op\" field".format(json.dumps(request.offending_command)))
+ if request.ERR_NO_PATH:
+ return error(400, "Malformed command \"{0}\": missing \"path\" field".format(json.dumps(request.offending_command)))
+ if request.ERR_EMPTY_PATH:
+ return error(400, "Malformed command \"{0}\": empty path".format(json.dumps(request.offending_command)))
+ if request.ERR_PATH_NOT_LIST:
+ return error(400, "Malformed command \"{0}\": \"path\" field must be a list".format(json.dumps(request.offending_command)))
+ if request.ERR_VALUE_NOT_STRING:
+ return error(400, "Malformed command \"{0}\": \"value\" field must be a string".format(json.dumps(request.offending_command)))
+ if request.ERR_PATH_NOT_LIST_OF_STR:
+ return error(400, "Malformed command \"{0}\": \"path\" field must be a list of strings".format(json.dumps(request.offending_command)))
+ if endpoint in ('/retrieve','/generate','/show'):
+ if request.ERR_NO_OP or request.ERR_NO_PATH:
+ return error(400, "Missing required field. \"op\" and \"path\" fields are required")
+ if endpoint in ('/config-file', '/image'):
+ if request.ERR_NO_OP:
+ return error(400, "Missing required field \"op\"")
+
+ raise e
+
+ return response
+
+ return custom_route_handler
+
+app = FastAPI(debug=True,
+ title="VyOS API",
+ version="0.1.0",
+ responses={**responses},
+ dependencies=[Depends(auth_required)])
+
+app.router.route_class = MultipartRoute
+
+@app.exception_handler(RequestValidationError)
+async def validation_exception_handler(request, exc):
+ return error(400, str(exc.errors()[0]))
+
+@app.post('/configure')
+def configure_op(data: Union[ConfigureModel, ConfigureListModel]):
+ session = app.state.vyos_session
env = session.get_session_env()
config = vyos.config.Config(session_env=env)
- strict_field = request.form.get("strict")
- if strict_field == "true":
- strict = True
- else:
- strict = False
-
# Allow users to pass just one command
- if not isinstance(commands, list):
- commands = [commands]
+ if not isinstance(data, ConfigureListModel):
+ data = [data]
+ else:
+ data = data.commands
# We don't want multiple people/apps to be able to commit at once,
# or modify the shared session while someone else is doing the same,
@@ -114,53 +416,25 @@ def configure_op(commands):
status = 200
error_msg = None
try:
- for c in commands:
- # What we've got may not even be a dict
- if not isinstance(c, dict):
- raise ConfigSessionError("Malformed command \"{0}\": any command must be a dict".format(json.dumps(c)))
-
- # Missing op or path is a show stopper
- if not ('op' in c):
- raise ConfigSessionError("Malformed command \"{0}\": missing \"op\" field".format(json.dumps(c)))
- if not ('path' in c):
- raise ConfigSessionError("Malformed command \"{0}\": missing \"path\" field".format(json.dumps(c)))
-
- # Missing value is fine, substitute for empty string
- if 'value' in c:
- value = c['value']
- else:
- value = ""
-
- op = c['op']
- path = c['path']
-
- if not path:
- raise ConfigSessionError("Malformed command \"{0}\": empty path".format(json.dumps(c)))
-
- # Type checking
- if not isinstance(path, list):
- raise ConfigSessionError("Malformed command \"{0}\": \"path\" field must be a list".format(json.dumps(c)))
-
- if not isinstance(value, str):
- raise ConfigSessionError("Malformed command \"{0}\": \"value\" field must be a string".format(json.dumps(c)))
+ for c in data:
+ op = c.op
+ path = c.path
- # Account for the case when value field is present and set to null
- if not value:
+ if c.value:
+ value = c.value
+ else:
value = ""
- # For vyos.configsessios calls that have no separate value arguments,
+ # For vyos.configsession calls that have no separate value arguments,
# and for type checking too
- try:
- cfg_path = " ".join(path + [value]).strip()
- except TypeError:
- raise ConfigSessionError("Malformed command \"{0}\": \"path\" field must be a list of strings".format(json.dumps(c)))
+ cfg_path = " ".join(path + [value]).strip()
if op == 'set':
# XXX: it would be nice to do a strict check for "path already exists",
# but there's probably no way to do that
session.set(path, value=value)
elif op == 'delete':
- if strict and not config.exists(cfg_path):
+ if app.state.vyos_strict and not config.exists(cfg_path):
raise ConfigSessionError("Cannot delete [{0}]: path/value does not exist".format(cfg_path))
session.delete(path, value=value)
elif op == 'comment':
@@ -169,16 +443,16 @@ def configure_op(commands):
raise ConfigSessionError("\"{0}\" is not a valid operation".format(op))
# end for
session.commit()
- print("Configuration modified via HTTP API using key \"{0}\"".format(id))
+ logger.info(f"Configuration modified via HTTP API using key '{app.state.vyos_id}'")
except ConfigSessionError as e:
session.discard()
status = 400
- if app.config['vyos_debug']:
- print(traceback.format_exc(), file=sys.stderr)
+ if app.state.vyos_debug:
+ logger.critical(f"ConfigSessionError:\n {traceback.format_exc()}")
error_msg = str(e)
except Exception as e:
session.discard()
- print(traceback.format_exc(), file=sys.stderr)
+ logger.critical(traceback.format_exc())
status = 500
# Don't give the details away to the outer world
@@ -188,22 +462,17 @@ def configure_op(commands):
if status != 200:
return error(status, error_msg)
- else:
- return success(None)
-@app.route('/retrieve', methods=['POST'])
-@get_command
-@auth_required
-def retrieve_op(command):
- session = app.config['vyos_session']
+ return success(None)
+
+@app.post("/retrieve")
+def retrieve_op(data: RetrieveModel):
+ session = app.state.vyos_session
env = session.get_session_env()
config = vyos.config.Config(session_env=env)
- try:
- op = command['op']
- path = " ".join(command['path'])
- except KeyError:
- return error(400, "Missing required field. \"op\" and \"path\" fields are required")
+ op = data.op
+ path = " ".join(data.path)
try:
if op == 'returnValue':
@@ -214,10 +483,10 @@ def retrieve_op(command):
res = config.exists(path)
elif op == 'showConfig':
config_format = 'json'
- if 'configFormat' in command:
- config_format = command['configFormat']
+ if data.configFormat:
+ config_format = data.configFormat
- res = session.show_config(path=command['path'])
+ res = session.show_config(path=data.path)
if config_format == 'json':
config_tree = vyos.configtree.ConfigTree(res)
res = json.loads(config_tree.to_json())
@@ -233,33 +502,28 @@ def retrieve_op(command):
except ConfigSessionError as e:
return error(400, str(e))
except Exception as e:
- print(traceback.format_exc(), file=sys.stderr)
+ logger.critical(traceback.format_exc())
return error(500, "An internal error occured. Check the logs for details.")
return success(res)
-@app.route('/config-file', methods=['POST'])
-@get_command
-@auth_required
-def config_file_op(command):
- session = app.config['vyos_session']
+@app.post('/config-file')
+def config_file_op(data: ConfigFileModel):
+ session = app.state.vyos_session
- try:
- op = command['op']
- except KeyError:
- return error(400, "Missing required field \"op\"")
+ op = data.op
try:
if op == 'save':
- try:
- path = command['file']
- except KeyError:
+ if data.file:
+ path = data.file
+ else:
path = '/config/config.boot'
res = session.save_config(path)
elif op == 'load':
- try:
- path = command['file']
- except KeyError:
+ if data.file:
+ path = data.file
+ else:
return error(400, "Missing required field \"file\"")
res = session.migrate_and_load_config(path)
res = session.commit()
@@ -268,33 +532,28 @@ def config_file_op(command):
except ConfigSessionError as e:
return error(400, str(e))
except Exception as e:
- print(traceback.format_exc(), file=sys.stderr)
+ logger.critical(traceback.format_exc())
return error(500, "An internal error occured. Check the logs for details.")
return success(res)
-@app.route('/image', methods=['POST'])
-@get_command
-@auth_required
-def image_op(command):
- session = app.config['vyos_session']
+@app.post('/image')
+def image_op(data: ImageModel):
+ session = app.state.vyos_session
- try:
- op = command['op']
- except KeyError:
- return error(400, "Missing required field \"op\"")
+ op = data.op
try:
if op == 'add':
- try:
- url = command['url']
- except KeyError:
+ if data.url:
+ url = data.url
+ else:
return error(400, "Missing required field \"url\"")
res = session.install_image(url)
elif op == 'delete':
- try:
- name = command['name']
- except KeyError:
+ if data.name:
+ name = data.name
+ else:
return error(400, "Missing required field \"name\"")
res = session.remove_image(name)
else:
@@ -302,26 +561,17 @@ def image_op(command):
except ConfigSessionError as e:
return error(400, str(e))
except Exception as e:
- print(traceback.format_exc(), file=sys.stderr)
+ logger.critical(traceback.format_exc())
return error(500, "An internal error occured. Check the logs for details.")
return success(res)
+@app.post('/generate')
+def generate_op(data: GenerateModel):
+ session = app.state.vyos_session
-@app.route('/generate', methods=['POST'])
-@get_command
-@auth_required
-def generate_op(command):
- session = app.config['vyos_session']
-
- try:
- op = command['op']
- path = command['path']
- except KeyError:
- return error(400, "Missing required field. \"op\" and \"path\" fields are required")
-
- if not isinstance(path, list):
- return error(400, "Malformed command: \"path\" field must be a list of strings")
+ op = data.op
+ path = data.path
try:
if op == 'generate':
@@ -331,25 +581,17 @@ def generate_op(command):
except ConfigSessionError as e:
return error(400, str(e))
except Exception as e:
- print(traceback.format_exc(), file=sys.stderr)
+ logger.critical(traceback.format_exc())
return error(500, "An internal error occured. Check the logs for details.")
return success(res)
-@app.route('/show', methods=['POST'])
-@get_command
-@auth_required
-def show_op(command):
- session = app.config['vyos_session']
+@app.post('/show')
+def show_op(data: ShowModel):
+ session = app.state.vyos_session
- try:
- op = command['op']
- path = command['path']
- except KeyError:
- return error(400, "Missing required field. \"op\" and \"path\" fields are required")
-
- if not isinstance(path, list):
- return error(400, "Malformed command: \"path\" field must be a list of strings")
+ op = data.op
+ path = data.path
try:
if op == 'show':
@@ -359,13 +601,24 @@ def show_op(command):
except ConfigSessionError as e:
return error(400, str(e))
except Exception as e:
- print(traceback.format_exc(), file=sys.stderr)
+ logger.critical(traceback.format_exc())
return error(500, "An internal error occured. Check the logs for details.")
return success(res)
-def shutdown():
- raise KeyboardInterrupt
+###
+# GraphQL integration
+###
+
+from api.graphql.bindings import generate_schema
+
+api.graphql.state.init()
+
+schema = generate_schema()
+
+app.add_route('/graphql', GraphQL(schema, debug=True))
+
+###
if __name__ == '__main__':
# systemd's user and group options don't work, do it by hand here,
@@ -379,22 +632,23 @@ if __name__ == '__main__':
try:
server_config = load_server_config()
- except Exception as e:
- print("Failed to load the HTTP API server config: {0}".format(e))
+ except Exception as err:
+ logger.critical(f"Failed to load the HTTP API server config: {err}")
- session = ConfigSession(os.getpid())
+ config_session = ConfigSession(os.getpid())
- app.config['vyos_session'] = session
- app.config['vyos_keys'] = server_config['api_keys']
- app.config['vyos_debug'] = server_config['debug']
+ app.state.vyos_session = config_session
+ app.state.vyos_keys = server_config['api_keys']
- def sig_handler(signum, frame):
- shutdown()
+ app.state.vyos_debug = bool(server_config['debug'] == 'true')
+ app.state.vyos_strict = bool(server_config['strict'] == 'true')
- signal.signal(signal.SIGTERM, sig_handler)
+ api.graphql.state.settings['app'] = app
try:
- serve(app, host=server_config["listen_address"],
- port=server_config["port"])
- except OSError as e:
- print(f"OSError {e}")
+ uvicorn.run(app, host=server_config["listen_address"],
+ port=int(server_config["port"]),
+ proxy_headers=True)
+ except OSError as err:
+ logger.critical(f"OSError {err}")
+ sys.exit(1)
diff --git a/src/system/keepalived-fifo.py b/src/system/keepalived-fifo.py
index 7e2076820..1fba0d75b 100755
--- a/src/system/keepalived-fifo.py
+++ b/src/system/keepalived-fifo.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-2021 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -13,7 +13,6 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-#
import os
import time
@@ -22,11 +21,13 @@ import argparse
import threading
import re
import json
-from pathlib import Path
-from queue import Queue
import logging
+
+from queue import Queue
from logging.handlers import SysLogHandler
+from vyos.ifconfig.vrrp import VRRP
+from vyos.configquery import ConfigTreeQuery
from vyos.util import cmd
# configure logging
@@ -37,17 +38,20 @@ logs_handler_syslog.setFormatter(logs_format)
logger.addHandler(logs_handler_syslog)
logger.setLevel(logging.DEBUG)
+mdns_running_file = '/run/mdns_vrrp_active'
+mdns_update_command = 'sudo /usr/libexec/vyos/conf_mode/service_mdns-repeater.py'
# class for all operations
class KeepalivedFifo:
# init - read command arguments
def __init__(self):
- logger.info("Starting FIFO pipe for Keepalived")
+ logger.info('Starting FIFO pipe for Keepalived')
# define program arguments
cmd_args_parser = argparse.ArgumentParser(description='Create FIFO pipe for keepalived and process notify events', add_help=False)
cmd_args_parser.add_argument('PIPE', help='path to the FIFO pipe')
# parse arguments
cmd_args = cmd_args_parser.parse_args()
+
self._config_load()
self.pipe_path = cmd_args.PIPE
@@ -59,33 +63,34 @@ class KeepalivedFifo:
# load configuration
def _config_load(self):
try:
- # read the dictionary file with configuration
- with open('/run/keepalived_config.dict', 'r') as dict_file:
- vrrp_config_dict = json.load(dict_file)
+ base = ['high-availability', 'vrrp']
+ conf = ConfigTreeQuery()
+ if not conf.exists(base):
+ raise ValueError()
+
+ # Read VRRP configuration directly from CLI
+ vrrp_config_dict = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True)
self.vrrp_config = {'vrrp_groups': {}, 'sync_groups': {}}
- # save VRRP instances to the new dictionary
- for vrrp_group in vrrp_config_dict['vrrp_groups']:
- self.vrrp_config['vrrp_groups'][vrrp_group['name']] = {
- 'STOP': vrrp_group.get('stop_script'),
- 'FAULT': vrrp_group.get('fault_script'),
- 'BACKUP': vrrp_group.get('backup_script'),
- 'MASTER': vrrp_group.get('master_script')
- }
- # save VRRP sync groups to the new dictionary
- for sync_group in vrrp_config_dict['sync_groups']:
- self.vrrp_config['sync_groups'][sync_group['name']] = {
- 'STOP': sync_group.get('stop_script'),
- 'FAULT': sync_group.get('fault_script'),
- 'BACKUP': sync_group.get('backup_script'),
- 'MASTER': sync_group.get('master_script')
- }
- logger.debug("Loaded configuration: {}".format(self.vrrp_config))
+ for key in ['group', 'sync_group']:
+ if key not in vrrp_config_dict:
+ continue
+ for group, group_config in vrrp_config_dict[key].items():
+ if 'transition_script' not in group_config:
+ continue
+ self.vrrp_config['vrrp_groups'][group] = {
+ 'STOP': group_config['transition_script'].get('stop'),
+ 'FAULT': group_config['transition_script'].get('fault'),
+ 'BACKUP': group_config['transition_script'].get('backup'),
+ 'MASTER': group_config['transition_script'].get('master'),
+ }
+ logger.info(f'Loaded configuration: {self.vrrp_config}')
except Exception as err:
- logger.error("Unable to load configuration: {}".format(err))
+ logger.error(f'Unable to load configuration: {err}')
# run command
def _run_command(self, command):
- logger.debug("Running the command: {}".format(command))
+ logger.debug(f'Running the command: {command}')
try:
cmd(command)
except OSError as err:
@@ -93,14 +98,14 @@ class KeepalivedFifo:
# create FIFO pipe
def pipe_create(self):
- if Path(self.pipe_path).exists():
- logger.info("PIPE already exist: {}".format(self.pipe_path))
+ if os.path.exists(self.pipe_path):
+ logger.info(f'PIPE already exist: {self.pipe_path}')
else:
os.mkfifo(self.pipe_path)
# process message from pipe
def pipe_process(self):
- logger.debug("Message processing start")
+ logger.debug('Message processing start')
regex_notify = re.compile(r'^(?P<type>\w+) "(?P<name>[\w-]+)" (?P<state>\w+) (?P<priority>\d+)$', re.MULTILINE)
while self.stopme.is_set() is False:
# wait for a new message event from pipe_wait
@@ -111,16 +116,19 @@ class KeepalivedFifo:
# get all messages from queue and try to process them
while self.message_queue.empty() is not True:
message = self.message_queue.get()
- logger.debug("Received message: {}".format(message))
+ logger.debug(f'Received message: {message}')
notify_message = regex_notify.search(message)
# try to process a message if it looks valid
if notify_message:
n_type = notify_message.group('type')
n_name = notify_message.group('name')
n_state = notify_message.group('state')
- logger.info("{} {} changed state to {}".format(n_type, n_name, n_state))
+ logger.info(f'{n_type} {n_name} changed state to {n_state}')
# check and run commands for VRRP instances
if n_type == 'INSTANCE':
+ if os.path.exists(mdns_running_file):
+ cmd(mdns_update_command)
+
if n_name in self.vrrp_config['vrrp_groups'] and n_state in self.vrrp_config['vrrp_groups'][n_name]:
n_script = self.vrrp_config['vrrp_groups'][n_name].get(n_state)
if n_script:
@@ -128,6 +136,9 @@ class KeepalivedFifo:
# check and run commands for VRRP sync groups
# currently, this is not available in VyOS CLI
if n_type == 'GROUP':
+ if os.path.exists(mdns_running_file):
+ cmd(mdns_update_command)
+
if n_name in self.vrrp_config['sync_groups'] and n_state in self.vrrp_config['sync_groups'][n_name]:
n_script = self.vrrp_config['sync_groups'][n_name].get(n_state)
if n_script:
@@ -135,16 +146,16 @@ class KeepalivedFifo:
# mark task in queue as done
self.message_queue.task_done()
except Exception as err:
- logger.error("Error processing message: {}".format(err))
- logger.debug("Terminating messages processing thread")
+ logger.error(f'Error processing message: {err}')
+ logger.debug('Terminating messages processing thread')
# wait for messages
def pipe_wait(self):
- logger.debug("Message reading start")
+ logger.debug('Message reading start')
self.pipe_read = os.open(self.pipe_path, os.O_RDONLY | os.O_NONBLOCK)
while self.stopme.is_set() is False:
# sleep a bit to not produce 100% CPU load
- time.sleep(0.1)
+ time.sleep(0.250)
try:
# try to read a message from PIPE
message = os.read(self.pipe_read, 500)
@@ -157,21 +168,19 @@ class KeepalivedFifo:
except Exception as err:
# ignore the "Resource temporarily unavailable" error
if err.errno != 11:
- logger.error("Error receiving message: {}".format(err))
+ logger.error(f'Error receiving message: {err}')
- logger.debug("Closing FIFO pipe")
+ logger.debug('Closing FIFO pipe')
os.close(self.pipe_read)
-
# handle SIGTERM signal to allow finish all messages processing
def sigterm_handle(signum, frame):
- logger.info("Ending processing: Received SIGTERM signal")
+ logger.info('Ending processing: Received SIGTERM signal')
fifo.stopme.set()
thread_wait_message.join()
fifo.message_event.set()
thread_process_message.join()
-
signal.signal(signal.SIGTERM, sigterm_handle)
# init our class
diff --git a/src/system/on-dhcp-event.sh b/src/system/on-dhcp-event.sh
index a062dc810..49e53d7e1 100755
--- a/src/system/on-dhcp-event.sh
+++ b/src/system/on-dhcp-event.sh
@@ -21,21 +21,20 @@ client_mac=$4
domain=$5
hostsd_client="/usr/bin/vyos-hostsd-client"
-if [ -z "$client_name" ]; then
- logger -s -t on-dhcp-event "Client name was empty, using MAC \"$client_mac\" instead"
- client_name=$(echo "client-"$client_mac | tr : -)
-fi
-
-if [ "$domain" == "..YYZ!" ]; then
- client_fqdn_name=$client_name
- client_search_expr=$client_name
-else
- client_fqdn_name=$client_name.$domain
- client_search_expr="$client_name\\.$domain"
-fi
-
case "$action" in
commit) # add mapping for new lease
+ if [ -z "$client_name" ]; then
+ logger -s -t on-dhcp-event "Client name was empty, using MAC \"$client_mac\" instead"
+ client_name=$(echo "client-"$client_mac | tr : -)
+ fi
+
+ if [ "$domain" == "..YYZ!" ]; then
+ client_fqdn_name=$client_name
+ client_search_expr=$client_name
+ else
+ client_fqdn_name=$client_name.$domain
+ client_search_expr="$client_name\\.$domain"
+ fi
$hostsd_client --add-hosts "$client_fqdn_name,$client_ip" --tag "dhcp-server-$client_ip" --apply
exit 0
;;
diff --git a/src/systemd/vyos-http-api.service b/src/systemd/vyos-http-api.service
index 4fa68b4ff..ba5df5984 100644
--- a/src/systemd/vyos-http-api.service
+++ b/src/systemd/vyos-http-api.service
@@ -5,9 +5,8 @@ Requires=vyos-router.service
[Service]
ExecStartPre=/usr/libexec/vyos/init/vyos-config
-ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-http-api-server
+ExecStart=/usr/libexec/vyos/services/vyos-http-api-server
Type=idle
-KillMode=process
SyslogIdentifier=vyos-http-api
SyslogFacility=daemon
diff --git a/src/validators/allowed-vlan b/src/validators/allowed-vlan
new file mode 100755
index 000000000..11389390b
--- /dev/null
+++ b/src/validators/allowed-vlan
@@ -0,0 +1,19 @@
+#! /usr/bin/python3
+
+import sys
+import re
+
+if __name__ == '__main__':
+ if len(sys.argv)>1:
+ allowed_vlan = sys.argv[1]
+ if re.search('[0-9]{1,4}-[0-9]{1,4}', allowed_vlan):
+ for tmp in allowed_vlan.split('-'):
+ if int(tmp) not in range(1, 4095):
+ sys.exit(1)
+ else:
+ if int(allowed_vlan) not in range(1, 4095):
+ sys.exit(1)
+ else:
+ sys.exit(2)
+
+ sys.exit(0)
diff --git a/src/validators/base64 b/src/validators/base64
new file mode 100755
index 000000000..e2b1e730d
--- /dev/null
+++ b/src/validators/base64
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 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 base64
+from sys import argv
+
+if __name__ == '__main__':
+ if len(argv) != 2:
+ exit(1)
+ try:
+ base64.b64decode(argv[1])
+ except:
+ exit(1)
+ exit(0)
diff --git a/src/validators/script b/src/validators/script
index 2665ec1f6..1d8a27e5c 100755
--- a/src/validators/script
+++ b/src/validators/script
@@ -1,8 +1,6 @@
#!/usr/bin/env python3
#
-# numeric value validator
-#
-# Copyright (C) 2018 VyOS maintainers and contributors
+# Copyright (C) 2018-2021 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
@@ -23,7 +21,6 @@ import shlex
import vyos.util
-
if __name__ == '__main__':
if len(sys.argv) < 2:
sys.exit('Please specify script file to check')
@@ -35,11 +32,11 @@ if __name__ == '__main__':
sys.exit(f'File {script} does not exist')
if not (os.path.isfile(script) and os.access(script, os.X_OK)):
- sys.exit('File {script} is not an executable file')
+ sys.exit(f'File {script} is not an executable file')
# 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'
+ f'Warning: file {path} is outside the / config directory\n'
'It will not be automatically migrated to a new image on system update'
)