diff options
33 files changed, 622 insertions, 136 deletions
@@ -70,6 +70,7 @@ interface_definitions: $(BUILD_DIR) $(obj) rm -f $(TMPL_DIR)/interfaces/wireless/node.tag/ipv6/node.def rm -f $(TMPL_DIR)/interfaces/wireless/node.tag/vif/node.tag/ip/node.def rm -f $(TMPL_DIR)/interfaces/wireless/node.tag/vif/node.tag/ipv6/node.def + rm -f $(TMPL_DIR)/interfaces/wirelessmodem/node.tag/ipv6/node.def rm -f $(TMPL_DIR)/protocols/node.def rm -f $(TMPL_DIR)/protocols/static/node.def rm -f $(TMPL_DIR)/system/node.def diff --git a/debian/control b/debian/control index 366e8df94..bccfc02d4 100644 --- a/debian/control +++ b/debian/control @@ -79,6 +79,7 @@ Depends: python3, frr, radvd, dbus, + usb-modeswitch, hostapd (>= 0.6.8), wpasupplicant (>= 0.6.7), iw, diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install index 5bb7ea507..5004d111f 100644 --- a/debian/vyos-1x.install +++ b/debian/vyos-1x.install @@ -3,6 +3,7 @@ etc/init.d etc/ppp etc/rsyslog.d etc/systemd +etc/udev etc/vyos lib/ opt/ diff --git a/interface-definitions/https.xml.in b/interface-definitions/https.xml.in index 1d986b2b4..49bd25b82 100644 --- a/interface-definitions/https.xml.in +++ b/interface-definitions/https.xml.in @@ -111,6 +111,12 @@ <hidden/> </properties> </leafNode> + <leafNode name="virtual-host"> + <properties> + <help>Restrict proxy to virtual host(s)</help> + <multi/> + </properties> + </leafNode> </children> </node> <node name="certificates"> diff --git a/interface-definitions/interfaces-wirelessmodem.xml.in b/interface-definitions/interfaces-wirelessmodem.xml.in new file mode 100644 index 000000000..cea8f4029 --- /dev/null +++ b/interface-definitions/interfaces-wirelessmodem.xml.in @@ -0,0 +1,80 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="interfaces"> + <children> + <tagNode name="wirelessmodem" owner="${vyos_conf_scripts_dir}/interfaces-wirelessmodem.py"> + <properties> + <help>Wireless Modem (WWAN) Interface</help> + <priority>350</priority> + <constraint> + <regex>wlm[0-9]+$</regex> + </constraint> + <constraintErrorMessage>Wireless Modem interface must be named wlmN</constraintErrorMessage> + <valueHelp> + <format>wlmN</format> + <description>Wireless modem interface name</description> + </valueHelp> + </properties> + <children> + <leafNode name="apn"> + <properties> + <help>Access Point Name (APN)</help> + </properties> + </leafNode> + <node name="backup"> + <properties> + <help>Insert backup default route</help> + </properties> + <children> + <leafNode name="distance"> + <properties> + <help>Distance backup default route</help> + <valueHelp> + <format>1-255</format> + <description>Distance of the backup route (default: 10) </description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + <constraintErrorMessage>Must be between (1-255)</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + #include <include/interface-description.xml.i> + #include <include/interface-disable.xml.i> + <leafNode name="device"> + <properties> + <help>System device name (default: ttyUSB0)</help> + <valueHelp> + <format>ttyXXX</format> + <description>System TTY device name</description> + </valueHelp> + </properties> + </leafNode> + #include <include/interface-disable-link-detect.xml.i> + #include <include/interface-mtu-68-9000.xml.i> + <node name="ipv6"> + <children> + #include <include/ipv6-address.xml.i> + #include <include/ipv6-disable-forwarding.xml.i> + #include <include/ipv6-dup-addr-detect-transmits.xml.i> + </children> + </node> + <leafNode name="no-peer-dns"> + <properties> + <help>Do not use peer supplied DNS server information</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ondemand"> + <properties> + <help>Only dial when traffic is available</help> + <valueless/> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/vrf.xml.in b/interface-definitions/vrf.xml.in index f1895598e..76748e5ae 100644 --- a/interface-definitions/vrf.xml.in +++ b/interface-definitions/vrf.xml.in @@ -29,14 +29,14 @@ <leafNode name="table"> <properties> <help>Routing table associated with this instance</help> - <constraint> - <validator name="numeric" argument="--range 1-2147483647"/> - </constraint> - <constraintErrorMessage>Invalid kernel table number</constraintErrorMessage> <valueHelp> - <format>1-2147483647</format> + <format>100-2147483647</format> <description>Routing table ID</description> </valueHelp> + <constraint> + <validator name="numeric" argument="--range 100-2147483647"/> + </constraint> + <constraintErrorMessage>VRF routing table must be in range from 100 to 2147483647</constraintErrorMessage> </properties> </leafNode> #include <include/interface-description.xml.i> diff --git a/op-mode-definitions/show-interfaces-pppoe.xml b/op-mode-definitions/show-interfaces-pppoe.xml index 3acb14486..591ec8f5b 100644 --- a/op-mode-definitions/show-interfaces-pppoe.xml +++ b/op-mode-definitions/show-interfaces-pppoe.xml @@ -27,6 +27,15 @@ </leafNode> </children> </node> + <leafNode name="statistics"> + <properties> + <help>Show specified wirelessmodem interface statistics</help> + <completionHelp> + <script>${vyos_completion_dir}/list_pppoe_peers.sh</script> + </completionHelp> + </properties> + <command>/usr/sbin/pppstats $4</command> + </leafNode> </children> </tagNode> </children> diff --git a/op-mode-definitions/show-interfaces-wirelessmodem.xml b/op-mode-definitions/show-interfaces-wirelessmodem.xml new file mode 100644 index 000000000..681f54f3d --- /dev/null +++ b/op-mode-definitions/show-interfaces-wirelessmodem.xml @@ -0,0 +1,45 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="interfaces"> + <children> + <tagNode name="wirelessmodem"> + <properties> + <help>Show Wireless Modem (WWAN) interface information</help> + <completionHelp> + <script>${vyos_completion_dir}/list_wlm_peers.sh</script> + </completionHelp> + </properties> + <command>${vyatta_bindir}/vyatta-show-interfaces.pl --intf="$4"</command> + <children> + <node name="log"> + <properties> + <help>Show PPPoE logs</help> + </properties> + <command>cat /var/log/vyatta/ppp_$4.log</command> + <children> + <leafNode name="tail"> + <properties> + <help>Watch PPPoE logs</help> + </properties> + <command>tail --follow=name /var/log/vyatta/ppp_$4.log</command> + </leafNode> + </children> + </node> + <leafNode name="statistics"> + <properties> + <help>Show specified wirelessmodem interface statistics</help> + <completionHelp> + <script>${vyos_completion_dir}/list_wlm_peers.sh</script> + </completionHelp> + </properties> + <command>/usr/sbin/pppstats $4</command> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/python/vyos/config.py b/python/vyos/config.py index 2342f7021..75055a603 100644 --- a/python/vyos/config.py +++ b/python/vyos/config.py @@ -238,6 +238,19 @@ class Config(object): str: working configuration """ + # show_config should be independent of CLI edit level. + # Set the CLI edit environment to the top level, and + # restore original on exit. + save_env = self.__session_env + + env_str = self._run(self._make_command('getEditResetEnv', '')) + env_list = re.findall(r'([A-Z_]+)=\'([^;\s]+)\'', env_str) + root_env = os.environ + for k, v in env_list: + root_env[k] = v + + self.__session_env = root_env + # FIXUP: by default, showConfig will give you a diff # if there are uncommitted changes. # The config parser obviously cannot work with diffs, @@ -253,8 +266,10 @@ class Config(object): path = " ".join(path) try: out = self._run(self._make_command('showConfig', path)) + self.__session_env = save_env return out except VyOSError: + self.__session_env = save_env return(default) def get_config_dict(self, path=[], effective=False): diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 66da52ff3..24fe174d2 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -123,6 +123,7 @@ def vlan_to_dict(conf): 'ip_enable_arp_accept': 0, 'ip_enable_arp_announce': 0, 'ip_enable_arp_ignore': 0, + 'ip_proxy_arp': 0, 'ipv6_autoconf': 0, 'ipv6_forwarding': 1, 'ipv6_dup_addr_detect': 1, @@ -190,6 +191,10 @@ def vlan_to_dict(conf): if conf.exists('ip enable-arp-ignore'): vlan['ip_enable_arp_ignore'] = 1 + # Enable Proxy ARP + if conf.exists('ip enable-proxy-arp'): + vlan['ip_proxy_arp'] = 1 + # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC) if conf.exists('ipv6 address autoconf'): vlan['ipv6_autoconf'] = 1 diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py index 0274f3573..a0b0eb3c1 100644 --- a/python/vyos/configtree.py +++ b/python/vyos/configtree.py @@ -24,58 +24,10 @@ def escape_backslash(string: str) -> str: result = p.sub(r'\\\\', string) return result -def strip_comments(s): - """ Split a config string into the config section and the trailing comments """ - INITIAL = 0 - IN_COMMENT = 1 - - i = len(s) - 1 - - state = INITIAL - - config_end = 0 - - # Find the first character of the comments section at the end, - # if it exists - while (i >= 0): - c = s[i] - - if (state == INITIAL) and re.match(r'\s', c): - # Ignore whitespace - if (i != 0): - i -= 1 - else: - config_end = 0 - break - elif (state == INITIAL) and not re.match(r'(\s|\/)', c): - # Assume there are no (more) trailing comments, - # this is an end of a node: either a brace of the last character - # of a leaf node value - config_end = i + 1 - break - elif (state == INITIAL) and (c == '/'): - # A comment begins, or it's a stray slash - if (s[i-1] == '*'): - state = IN_COMMENT - i -= 2 - else: - raise ValueError("Invalid syntax: stray slash at character {0}".format(i + 1)) - elif (state == IN_COMMENT) and (c == '*'): - # A comment ends here - try: - if (s[i-1] == '/'): - state = INITIAL - i -= 2 - except: - raise ValueError("Invalid syntax: malformed commend end at character {0}".format(i + 1)) - elif (state == IN_COMMENT) and (c != '*'): - # Ignore everything inside comments, including braces - i -= 1 - else: - # Shouldn't happen - raise ValueError("Invalid syntax at character {0}: invalid character {1}".format(i + 1, c)) - - return (s[0:config_end], s[config_end+1:]) +def extract_version(s): + """ Extract the version string from the config string """ + t = re.split('(^//)', s, maxsplit=1, flags=re.MULTILINE) + return (s, ''.join(t[1:])) def check_path(path): # Necessary type checking @@ -174,7 +126,7 @@ class ConfigTree(object): self.__destroy = self.__lib.destroy self.__destroy.argtypes = [c_void_p] - config_section, comments_section = strip_comments(config_string) + config_section, version_section = extract_version(config_string) config_section = escape_backslash(config_section) config = self.__from_string(config_section.encode()) if config is None: @@ -182,7 +134,7 @@ class ConfigTree(object): raise ValueError("Failed to parse config: {0}".format(msg)) else: self.__config = config - self.__comments = comments_section + self.__version = version_section def __del__(self): if self.__config is not None: @@ -193,7 +145,7 @@ class ConfigTree(object): def to_string(self): config_string = self.__to_string(self.__config).decode() - config_string = "{0}\n{1}".format(config_string, self.__comments) + config_string = "{0}\n{1}".format(config_string, self.__version) return config_string def to_commands(self): diff --git a/python/vyos/ifconfig/wireless.py b/python/vyos/ifconfig/wireless.py index 932d07d01..946ae1642 100644 --- a/python/vyos/ifconfig/wireless.py +++ b/python/vyos/ifconfig/wireless.py @@ -47,7 +47,7 @@ class WiFiIf(Interface): self._cmd(cmd) # wireless interface is administratively down by default - self.set_state('down') + self.set_admin_state('down') def _delete(self): cmd = 'iw dev {ifname} del' \ diff --git a/python/vyos/ifconfig_vlan.py b/python/vyos/ifconfig_vlan.py index 00270cf58..ed22646c1 100644 --- a/python/vyos/ifconfig_vlan.py +++ b/python/vyos/ifconfig_vlan.py @@ -64,6 +64,8 @@ def apply_vlan_config(vlan, config): vlan.set_arp_announce(config['ip_enable_arp_announce']) # configure ARP ignore vlan.set_arp_ignore(config['ip_enable_arp_ignore']) + # configure Proxy ARP + vlan.set_proxy_arp(config['ip_proxy_arp']) # IPv6 address autoconfiguration vlan.set_ipv6_autoconf(config['ipv6_autoconf']) # IPv6 forwarding diff --git a/python/vyos/migrator.py b/python/vyos/migrator.py index f05228041..9a5fdef2f 100644 --- a/python/vyos/migrator.py +++ b/python/vyos/migrator.py @@ -25,7 +25,7 @@ class MigratorError(Exception): pass class Migrator(object): - def __init__(self, config_file, force=False, set_vintage=None): + def __init__(self, config_file, force=False, set_vintage='vyos'): self._config_file = config_file self._force = force self._set_vintage = set_vintage @@ -61,9 +61,6 @@ class Migrator(object): if self._set_vintage: self._config_file_vintage = self._set_vintage - if not self._config_file_vintage: - self._config_file_vintage = vyos.defaults.cfg_vintage - if self._config_file_vintage not in ['vyatta', 'vyos']: raise MigratorError("Unknown vintage.") @@ -204,16 +201,12 @@ class Migrator(object): return self._changed class VirtualMigrator(Migrator): - def __init__(self, config_file, vintage='vyos'): - super().__init__(config_file, set_vintage = vintage) - def run(self): cfg_file = self._config_file cfg_versions = self.read_config_file_versions() if not cfg_versions: - raise MigratorError("Config file has no version information;" - " virtual migration not possible.") + return if self.update_vintage(): self._changed = True diff --git a/python/vyos/util.py b/python/vyos/util.py index 635b11ee5..67aa87a3a 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -84,6 +84,8 @@ def colon_separated_to_dict(data_string, uniquekeys=False): def process_running(pid_file): """ Checks if a process with PID in pid_file is running """ + if not os.path.isfile(pid_file): + return False with open(pid_file, 'r') as f: pid = f.read().strip() return psutil.pid_exists(int(pid)) diff --git a/src/completion/list_wlm_peers.sh b/src/completion/list_wlm_peers.sh new file mode 100755 index 000000000..12dd00650 --- /dev/null +++ b/src/completion/list_wlm_peers.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +if [ -d /etc/ppp/peers ]; then + cd /etc/ppp/peers + ls wlm* +fi diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index a0fe9cf2f..889b62cf4 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -96,6 +96,7 @@ server { """ default_server_block = { + 'id' : '', 'address' : '*', 'port' : '443', 'name' : ['_'], @@ -117,6 +118,7 @@ def get_config(): else: for vhost in conf.list_nodes('virtual-host'): server_block = deepcopy(default_server_block) + server_block['id'] = vhost if conf.exists(f'virtual-host {vhost} listen-address'): addr = conf.return_value(f'virtual-host {vhost} listen-address') server_block['address'] = addr @@ -156,9 +158,21 @@ def get_config(): if conf.exists('api port'): port = conf.return_value('api port') api_data['port'] = port + if conf.exists('api virtual-host'): + vhosts = conf.return_values('api virtual-host') + api_data['vhost'] = vhosts[:] + if api_data: - for block in server_block_list: - block['api'] = api_data + # we do not want to include 'vhost' key as part of + # vyos.defaults.api_data, so check for key existence + vhost_list = api_data.get('vhost') + if vhost_list is None: + for block in server_block_list: + block['api'] = api_data + else: + for block in server_block_list: + if block['id'] in vhost_list: + block['api'] = api_data https = {'server_block_list' : server_block_list, 'certbot': certbot} return https diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index 286cab88e..15e9b4185 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -359,7 +359,7 @@ def apply(eth): # if custom mac is removed if eth['mac']: e.set_mac(eth['mac']) - else: + elif eth['hw_id']: e.set_mac(eth['hw_id']) # Maximum Transmission Unit (MTU) diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 17aa4697f..fb2d6e6d9 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -24,7 +24,6 @@ from stat import S_IRUSR,S_IRWXU,S_IRGRP,S_IXGRP,S_IROTH,S_IXOTH from grp import getgrnam from ipaddress import ip_address,ip_network,IPv4Interface from netifaces import interfaces -from psutil import pid_exists from pwd import getpwnam from subprocess import Popen, PIPE from time import sleep @@ -33,6 +32,7 @@ from shutil import rmtree from vyos import ConfigError from vyos.config import Config from vyos.ifconfig import VTunIf +from vyos.util import process_running from vyos.validate import is_addr_assigned user = 'openvpn' @@ -977,17 +977,12 @@ def generate(openvpn): return None def apply(openvpn): - pid = 0 pidfile = '/var/run/openvpn/{}.pid'.format(openvpn['intf']) - if os.path.isfile(pidfile): - pid = 0 - with open(pidfile, 'r') as f: - pid = int(f.read()) # Always stop OpenVPN service. We can not send a SIGUSR1 for restart of the # service as the configuration is not re-read. Stop daemon only if it's # running - it could have died or killed by someone evil - if pid_exists(pid): + if process_running(pidfile): cmd = 'start-stop-daemon' cmd += ' --stop ' cmd += ' --quiet' diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py index f948070ee..f318614db 100755 --- a/src/conf_mode/interfaces-pppoe.py +++ b/src/conf_mode/interfaces-pppoe.py @@ -325,17 +325,32 @@ def generate(pppoe): os.unlink(ip_pre_up_script_file) else: + # PPP peers directory + dirname = os.path.dirname(config_file_pppoe) + if not os.path.isdir(dirname): + os.mkdir(dirname) + # Create PPP configuration files tmpl = Template(config_pppoe_tmpl) config_text = tmpl.render(pppoe) with open(config_file_pppoe, 'w') as f: f.write(config_text) + # PPP ip-pre-up.d scripting directory + dirname = os.path.dirname(ip_pre_up_script_file) + if not os.path.isdir(dirname): + os.mkdir(dirname) + tmpl = Template(config_pppoe_ip_pre_up_tmpl) config_text = tmpl.render(pppoe) with open(ip_pre_up_script_file, 'w') as f: f.write(config_text) + # PPP ipv6-up.d scripting directory + dirname = os.path.dirname(ipv6_if_up_script_file) + if not os.path.isdir(dirname): + os.mkdir(dirname) + tmpl = Template(config_pppoe_ipv6_up_tmpl) config_text = tmpl.render(pppoe) with open(ipv6_if_up_script_file, 'w') as f: diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index 2c67c39ae..b6e62b0aa 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -25,15 +25,15 @@ from grp import getgrnam from re import findall from subprocess import Popen, PIPE -from psutil import pid_exists from netifaces import interfaces from netaddr import * -from vyos.ifconfig import WiFiIf -from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config +from vyos import ConfigError from vyos.configdict import list_diff, vlan_to_dict from vyos.config import Config -from vyos import ConfigError +from vyos.ifconfig import WiFiIf +from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config +from vyos.util import process_running user = 'root' group = 'vyattacfg' @@ -1364,15 +1364,9 @@ def verify(wifi): return None def generate(wifi): - pid = 0 # always stop hostapd service first before reconfiguring it pidfile = get_pid('hostapd', wifi['intf']) - if os.path.isfile(pidfile): - pid = 0 - with open(pidfile, 'r') as f: - pid = int(f.read()) - - if pid_exists(pid): + if process_running(pidfile): cmd = 'start-stop-daemon' cmd += ' --stop ' cmd += ' --quiet' @@ -1382,12 +1376,7 @@ def generate(wifi): # always stop wpa_supplicant service first before reconfiguring it pidfile = get_pid('wpa_supplicant', wifi['intf']) - if os.path.isfile(pidfile): - pid = 0 - with open(pidfile, 'r') as f: - pid = int(f.read()) - - if pid_exists(pid): + if process_running(pidfile): cmd = 'start-stop-daemon' cmd += ' --stop ' cmd += ' --quiet' @@ -1409,7 +1398,10 @@ def generate(wifi): # http://wiki.stocksy.co.uk/wiki/Multiple_SSIDs_with_hostapd # generate locally administered MAC address from used phy interface with open('/sys/class/ieee80211/{}/addresses'.format(wifi['phy']), 'r') as f: - tmp = EUI(f.read().rstrip()).value + # some PHYs tend to have multiple interfaces and thus supply multiple MAC + # addresses - we only need the first one for our calculation + tmp = f.readline().rstrip() + tmp = EUI(tmp).value # mask last nibble from the MAC address tmp &= 0xfffffffffff0 # set locally administered bit in MAC address @@ -1496,7 +1488,7 @@ def apply(wifi): # if custom mac is removed if wifi['mac']: w.set_mac(wifi['mac']) - else: + elif wifi['hw_id']: w.set_mac(wifi['hw_id']) # configure ARP filter configuration diff --git a/src/conf_mode/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py new file mode 100755 index 000000000..9efad3b8d --- /dev/null +++ b/src/conf_mode/interfaces-wirelessmodem.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit +from copy import deepcopy +from jinja2 import Template +from subprocess import Popen, PIPE +from pwd import getpwnam +from grp import getgrnam + +from vyos.config import Config +from vyos import ConfigError + +# Please be careful if you edit the template. +config_wwan_tmpl = """### Autogenerated by interfaces-wirelessmodem.py ### +{% if description %} +# {{ description }} +{% endif %} +ifname {{ intf }} +ipparam "{{ intf }} {{ metric }}" +linkname {{ intf }} +{% if name_server -%} +usepeerdns +{%- endif %} +# physical device +/dev/{{ device }} +lcp-echo-failure 0 +115200 +debug +logfile {{ logfile }} +nodefaultroute +ipcp-max-failure 4 +ipcp-accept-local +ipcp-accept-remote +noauth +crtscts +lock +persist +{% if on_demand -%} +demand +{%- endif %} + +connect '/usr/sbin/chat -v -t6 -f {{ chat_script }}' + +""" + +# Please be careful if you edit the template. +chat_wwan_tmpl = """ +ABORT 'NO DIAL TONE' ABORT 'NO ANSWER' ABORT 'NO CARRIER' ABORT DELAYED +'' AT +OK ATZ +OK 'AT+CGDCONT=1,"IP","{{ apn }}"' +OK ATD*99# +CONNECT '' + +""" + +default_config_data = { + 'address': [], + 'apn': '', + 'chat_script': '', + 'deleted': False, + 'description': '', + 'device': 'ttyUSB0', + 'disable': False, + 'disable_link_detect': 1, + 'on_demand': False, + 'logfile': '', + 'metric': '10', + 'mtu': '1500', + 'name_server': True, + 'intf': '' +} + +def subprocess_cmd(command): + p = Popen(command, stdout=PIPE, shell=True) + p.communicate() + +def check_kmod(): + modules = ['option', 'usb_wwan', 'usbserial'] + for module in modules: + if not os.path.exists(f'/sys/module/{module}'): + if os.system(f'modprobe {module}') != 0: + raise ConfigError(f'Loading Kernel module {module} failed') + +def get_config(): + wwan = deepcopy(default_config_data) + conf = Config() + + # determine tagNode instance + if 'VYOS_TAGNODE_VALUE' not in os.environ: + raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') + + wwan['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + wwan['logfile'] = f"/var/log/vyatta/ppp_{wwan['intf']}.log" + wwan['chat_script'] = f"/etc/ppp/peers/chat.{wwan['intf']}" + + # Check if interface has been removed + if not conf.exists('interfaces wirelessmodem ' + wwan['intf']): + wwan['deleted'] = True + return wwan + + # set new configuration level + conf.set_level('interfaces wirelessmodem ' + wwan['intf']) + + # get metrick for backup default route + if conf.exists(['apn']): + wwan['apn'] = conf.return_value(['apn']) + + # get metrick for backup default route + if conf.exists(['backup', 'distance']): + wwan['metric'] = conf.return_value(['backup', 'distance']) + + # Retrieve interface description + if conf.exists(['description']): + wwan['description'] = conf.return_value(['description']) + + # System device name + if conf.exists(['device']): + wwan['device'] = conf.return_value(['device']) + + # disable interface + if conf.exists('disable'): + wwan['disable'] = True + + # ignore link state changes + if conf.exists('disable-link-detect'): + wwan['disable_link_detect'] = 2 + + # Do not use DNS servers provided by the peer + if conf.exists(['mtu']): + wwan['mtu'] = conf.return_value(['mtu']) + + # Do not use DNS servers provided by the peer + if conf.exists(['no-peer-dns']): + wwan['name_server'] = False + + # Access concentrator name (only connect to this concentrator) + if conf.exists(['ondemand']): + wwan['on_demand'] = True + + return wwan + +def verify(wwan): + if wwan['deleted']: + return None + + if not wwan['apn']: + raise ConfigError(f"APN for {wwan['intf']} not configured") + + # we can not use isfile() here as Linux device files are no regular files + # thus the check will return False + if not os.path.exists(f"/dev/{wwan['device']}"): + raise ConfigError(f"Device {wwan['device']} does not exist") + + return None + +def generate(wwan): + config_file_wwan = f"/etc/ppp/peers/{wwan['intf']}" + + # Always hang-up WWAN connection prior generating new configuration file + cmd = f"systemctl stop ppp@{wwan['intf']}.service" + subprocess_cmd(cmd) + + if wwan['deleted']: + # Delete PPP configuration files + if os.path.exists(config_file_wwan): + os.unlink(config_file_wwan) + if os.path.exists(wwan['chat_script']): + os.unlink(wwan['chat_script']) + + else: + # PPP peers directory + dirname = os.path.dirname(config_file_wwan) + if not os.path.isdir(dirname): + os.mkdir(dirname) + + # Create PPP configuration files + tmpl = Template(config_wwan_tmpl) + config_text = tmpl.render(wwan) + with open(config_file_wwan, 'w') as f: + f.write(config_text) + + + # Create PPP chat script + tmpl = Template(chat_wwan_tmpl) + config_text = tmpl.render(wwan) + with open(wwan['chat_script'], 'w') as f: + f.write(config_text) + + return None + +def apply(wwan): + if wwan['deleted']: + # bail out early + return None + + if not wwan['disable']: + # dial WWAN connection + cmd = f"systemctl start ppp@{wwan['intf']}.service" + subprocess_cmd(cmd) + + # make logfile owned by root / vyattacfg + if os.path.isfile(wwan['logfile']): + uid = getpwnam('root').pw_uid + gid = getgrnam('vyattacfg').gr_gid + os.chown(wwan['logfile'], uid, gid) + + return None + +if __name__ == '__main__': + try: + check_kmod() + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index 8e5c7587c..070437443 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -23,9 +23,9 @@ from subprocess import Popen, PIPE, check_output from socket import socket, AF_INET, SOCK_STREAM from copy import deepcopy from stat import S_IRUSR, S_IWUSR, S_IRGRP -from psutil import pid_exists from vyos.config import Config +from vyos.util import process_running from vyos import ConfigError pidfile = r'/var/run/accel_sstp.pid' @@ -489,14 +489,8 @@ def generate(sstp): return sstp def apply(sstp): - pid = 0 - if os.path.isfile(pidfile): - pid = 0 - with open(pidfile, 'r') as f: - pid = int(f.read()) - if sstp is None: - if pid_exists(pid): + if process_running(pidfile): cmd = 'start-stop-daemon' cmd += ' --stop ' cmd += ' --quiet' @@ -509,7 +503,7 @@ def apply(sstp): return None - if not pid_exists(pid): + if not process_running(pidfile): if os.path.exists(pidfile): os.remove(pidfile) diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 991c5cb2c..a74b79317 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -213,9 +213,10 @@ def apply(vrf_config): _cmd(f'sysctl -wq net.ipv4.tcp_l3mdev_accept={bind_all}') _cmd(f'sysctl -wq net.ipv4.udp_l3mdev_accept={bind_all}') - for vrf_name in vrf_config['vrf_remove']: - if os.path.isdir(f'/sys/class/net/{vrf_name}'): - _cmd(f'ip link delete dev {vrf_name}') + for vrf in vrf_config['vrf_remove']: + name = vrf['name'] + if os.path.isdir(f'/sys/class/net/{name}'): + _cmd(f'ip link delete dev {name}') for vrf in vrf_config['vrf_add']: name = vrf['name'] diff --git a/src/etc/ppp/ip-down.d/0020-wirelessmodem b/src/etc/ppp/ip-down.d/0020-wirelessmodem new file mode 100755 index 000000000..c93c7cabe --- /dev/null +++ b/src/etc/ppp/ip-down.d/0020-wirelessmodem @@ -0,0 +1,18 @@ +#!/bin/sh + +tty=$2 +ipparam=$6 + +# Only applicable for Wireless Modems (WWAN) +if [ -z "$(echo $tty | egrep "tty(USB|ACM)")" ]; then + exit 0 +fi + +# device name and metric are received using ipparam +device=`echo "$ipparam"|awk '{ print $1 }'` +metric=`echo "$ipparam"|awk '{ print $2 }'` + +vtysh -c "conf t" -c "no ip route 0.0.0.0/0 ${device} ${metric}" + +DIALER_PID=$(cat /var/run/${device}.pid) +logger -t pppd[$DIALER_PID] "removed default route via $device metric $metric" diff --git a/src/etc/ppp/ip-up.d/0020-wirelessmodem b/src/etc/ppp/ip-up.d/0020-wirelessmodem new file mode 100755 index 000000000..95549387b --- /dev/null +++ b/src/etc/ppp/ip-up.d/0020-wirelessmodem @@ -0,0 +1,18 @@ +#!/bin/sh + +tty=$2 +ipparam=$6 + +# Only applicable for Wireless Modems (WWAN) +if [ -z "$(echo $tty | egrep "tty(USB|ACM)")" ]; then + exit 0 +fi + +# device name and metric are received using ipparam +device=`echo "$ipparam"|awk '{ print $1 }'` +metric=`echo "$ipparam"|awk '{ print $2 }'` + +vtysh -c "conf t" -c "ip route 0.0.0.0/0 ${device} ${metric}" + +DIALER_PID=$(cat /var/run/${device}.pid) +logger -t pppd[$DIALER_PID] "added default route via $device metric $metric" diff --git a/src/etc/udev/rules.d/99-vyos-wwan.rules b/src/etc/udev/rules.d/99-vyos-wwan.rules new file mode 100644 index 000000000..67f30a3dd --- /dev/null +++ b/src/etc/udev/rules.d/99-vyos-wwan.rules @@ -0,0 +1,11 @@ +ACTION!="add|change", GOTO="mbim_to_qmi_rules_end" + +SUBSYSTEM!="usb", GOTO="mbim_to_qmi_rules_end" + +# ignore any device with only one configuration +ATTR{bNumConfigurations}=="1", GOTO="mbim_to_qmi_rules_end" + +# force Sierra Wireless MC7710 to configuration #1 +ATTR{idVendor}=="1199",ATTR{idProduct}=="68a2",ATTR{bConfigurationValue}="1" + +LABEL="mbim_to_qmi_rules_end" diff --git a/src/helpers/run-config-migration.py b/src/helpers/run-config-migration.py index a57a19cdf..3c06e38f8 100755 --- a/src/helpers/run-config-migration.py +++ b/src/helpers/run-config-migration.py @@ -69,15 +69,22 @@ def main(): sys.exit(1) if not virtual: - migration = Migrator(config_file_name, force=force_on, - set_vintage=vintage) + virtual_migration = VirtualMigrator(config_file_name) + virtual_migration.run() + + migration = Migrator(config_file_name, force=force_on) + migration.run() + + if not migration.config_changed(): + os.remove(backup_file_name) else: - migration = VirtualMigrator(config_file_name) + virtual_migration = VirtualMigrator(config_file_name, + set_vintage=vintage) - migration.run() + virtual_migration.run() - if not migration._changed: - os.remove(backup_file_name) + if not virtual_migration.config_changed(): + os.remove(backup_file_name) if __name__ == '__main__': main() diff --git a/src/helpers/vyos-load-config.py b/src/helpers/vyos-load-config.py index 4e6d67efa..693529c23 100755 --- a/src/helpers/vyos-load-config.py +++ b/src/helpers/vyos-load-config.py @@ -28,7 +28,7 @@ import tempfile import vyos.defaults import vyos.remote from vyos.config import Config, VyOSError -from vyos.migrator import Migrator, MigratorError +from vyos.migrator import Migrator, VirtualMigrator, MigratorError system_config_file = 'config.boot' @@ -73,6 +73,12 @@ with tempfile.NamedTemporaryFile() as fp: with open(fp.name, 'w') as fd: fd.write(config_file) + virtual_migration = VirtualMigrator(fp.name) + try: + virtual_migration.run() + except MigratorError as err: + sys.exit('{}'.format(err)) + migration = Migrator(fp.name) try: migration.run() diff --git a/src/helpers/vyos-merge-config.py b/src/helpers/vyos-merge-config.py index c5216daa6..10a5ea4bc 100755 --- a/src/helpers/vyos-merge-config.py +++ b/src/helpers/vyos-merge-config.py @@ -21,9 +21,9 @@ import subprocess import tempfile import vyos.defaults import vyos.remote -import vyos.migrator from vyos.config import Config from vyos.configtree import ConfigTree +from vyos.migrator import Migrator, VirtualMigrator if (len(sys.argv) < 2): @@ -61,9 +61,13 @@ with tempfile.NamedTemporaryFile() as file_to_migrate: with open(file_to_migrate.name, 'w') as fd: fd.write(config_file) - migration = vyos.migrator.Migrator(file_to_migrate.name) + virtual_migration = VirtualMigrator(file_to_migrate.name) + virtual_migration.run() + + migration = Migrator(file_to_migrate.name) migration.run() - if migration.config_changed(): + + if virtual_migration.config_changed() or migration.config_changed(): with open(file_to_migrate.name, 'r') as fd: config_file = fd.read() diff --git a/src/migration-scripts/interfaces/6-to-7 b/src/migration-scripts/interfaces/6-to-7 new file mode 100755 index 000000000..b4f59c363 --- /dev/null +++ b/src/migration-scripts/interfaces/6-to-7 @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# Remove network provider name from CLI and rather use provider APN from CLI + +import sys +from vyos.configtree import ConfigTree + +if __name__ == '__main__': + if (len(sys.argv) < 1): + print("Must specify file name!") + exit(1) + + file_name = sys.argv[1] + with open(file_name, 'r') as f: + config_file = f.read() + + config = ConfigTree(config_file) + base = ['interfaces', 'wirelessmodem'] + + if not config.exists(base): + # Nothing to do + sys.exit(0) + + # list all individual interface types like dummy, ethernet and so on + for i in config.list_nodes(base): + iface = base + [i] + + # only three carries have been supported in the past, thus + # this will be fairly simple \o/ - and only one (AT&T) did + # configure an APN + if config.exists(iface + ['network']): + network = config.return_value(iface + ['network']) + if network == "att": + apn = 'isp.cingular' + config.set(iface + ['apn'], value=apn) + + config.delete(iface + ['network']) + + # synchronize DNS configuration with PPPoE interfaces to have a + # uniform CLI experience + if config.exists(iface + ['no-dns']): + config.rename(iface + ['no-dns'], 'no-peer-dns') + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/op_mode/reset_openvpn.py b/src/op_mode/reset_openvpn.py index 176cd51cf..4c29fbbba 100755 --- a/src/op_mode/reset_openvpn.py +++ b/src/op_mode/reset_openvpn.py @@ -17,10 +17,10 @@ import sys import os -from psutil import pid_exists from subprocess import Popen, PIPE from time import sleep from netifaces import interfaces +from vyos.util import process_running def get_config_name(intf): cfg_file = r'/opt/vyatta/etc/openvpn/openvpn-{}.conf'.format(intf) @@ -42,18 +42,13 @@ if __name__ == '__main__': interface = sys.argv[1] if os.path.isfile(get_config_name(interface)): pidfile = '/var/run/openvpn/{}.pid'.format(interface) - if os.path.isfile(pidfile): - pid = 0 - with open(pidfile, 'r') as f: - pid = int(f.read()) - - if pid_exists(pid): - cmd = 'start-stop-daemon' - cmd += ' --stop' - cmd += ' --oknodo' - cmd += ' --quiet' - cmd += ' --pidfile ' + pidfile - subprocess_cmd(cmd) + if process_running(pidfile): + cmd = 'start-stop-daemon' + cmd += ' --stop' + cmd += ' --oknodo' + cmd += ' --quiet' + cmd += ' --pidfile ' + pidfile + subprocess_cmd(cmd) # When stopping OpenVPN we need to wait for the 'old' interface to # vanish from the Kernel, if it is not gone, OpenVPN will report: diff --git a/tests/data/config.valid b/tests/data/config.valid index a21c6a4d1..1fbdd1505 100644 --- a/tests/data/config.valid +++ b/tests/data/config.valid @@ -35,5 +35,5 @@ empty-node { trailing-leaf-node-without-value -/* Trailing commend */ -/* Another trailing comment */ +// Trailing comment +// Another trailing comment |