From 5d858f0e6ad05b032c88c88a08c15d0876c44e8b Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 19 Aug 2019 22:49:11 +0200 Subject: openvpn: T1548: remove authy 2fa provider According to https://github.com/twilio/authy-openvpn commit 3e5dc73: > This plugin is no longer actively maintained. If you're interested in becoming a maintainer, we welcome forks of this project. In addition this plugin was always missing in the current branch ov VyOS and did not make it into VyOS 1.2 (crux) If 2FA for OpenVPN is required we should probably opt for Google Authenticator or if possible a U2F device. --- src/conf_mode/interface-openvpn.py | 31 ------------------------------- 1 file changed, 31 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interface-openvpn.py b/src/conf_mode/interface-openvpn.py index e4bde7bb0..a8313378b 100755 --- a/src/conf_mode/interface-openvpn.py +++ b/src/conf_mode/interface-openvpn.py @@ -219,10 +219,6 @@ client-config-dir /opt/vyatta/etc/openvpn/ccd/{{ intf }} {% for option in options -%} {{ option }} {% endfor -%} - -{%- if server_2fa_authy_key %} -plugin /usr/lib/authy/authy-openvpn.so https://api.authy.com/protected/json {{ server_2fa_authy_key }} nopam -{% endif %} """ client_tmpl = """ @@ -269,8 +265,6 @@ default_config_data = { 'remote_address': '', 'remote_host': [], 'remote_port': '', - 'server_2fa_authy_key': '', - 'server_2fa_authy': [], 'client': [], 'server_domain': '', 'server_max_conn': '', @@ -453,31 +447,6 @@ def get_config(): if conf.exists('replace-default-route local'): openvpn['redirect_gateway'] = 'local def1' - # Two Factor Authentication providers - # currently limited to authy - if conf.exists('2-factor-authentication authy api-key'): - openvpn['server_2fa_authy_key'] = conf.return_value('2-factor-authentication authy api-key') - - # Authy users (must be email address) - for user in conf.list_nodes('server 2-factor-authentication authy user'): - # set configuration level - conf.set_level('interfaces openvpn ' + openvpn['intf'] + ' 2-factor-authentication authy user ' + user) - data = { - 'user': user, - 'country_code': '', - 'mobile_number': '' - } - - # Country calling codes - if conf.exists('country-calling-code'): - data['country_code'] = conf.return_value('country-calling-code') - - # Mobile phone number - if conf.exists('phone-number'): - data['mobile_number'] = conf.return_value('phone-number') - - openvpn['server_2fa_authy'].append(data) - # Topology for clients if conf.exists('server topology'): openvpn['server_topology'] = conf.return_value('server topology') -- cgit v1.2.3 From 80ee4d8c545879d2422e7027ad5245bee484ee3c Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 19 Aug 2019 23:43:08 +0200 Subject: dummy: T1580: rewrite in new style XML/Python --- interface-definitions/interfaces-dummy.xml | 51 ++++++++++++ src/conf_mode/interface-dummy.py | 122 +++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 interface-definitions/interfaces-dummy.xml create mode 100755 src/conf_mode/interface-dummy.py (limited to 'src') diff --git a/interface-definitions/interfaces-dummy.xml b/interface-definitions/interfaces-dummy.xml new file mode 100644 index 000000000..7c425480a --- /dev/null +++ b/interface-definitions/interfaces-dummy.xml @@ -0,0 +1,51 @@ + + + + + + + Dummy interface name + 300 + + dum[0-9]+$ + + Dummy interface must be named dumN + + dumN + Dummy interface name + + + + + + IP address + + ipv4net + IPv4 address and prefix length + + + ipv6net + IPv6 address and prefix length + + + + + + + Interface description + + ^.{1,256}$ + + Interface description too long (limit 256 characters) + + + + + Disable interface + + + + + + + diff --git a/src/conf_mode/interface-dummy.py b/src/conf_mode/interface-dummy.py new file mode 100755 index 000000000..668e4acc7 --- /dev/null +++ b/src/conf_mode/interface-dummy.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# + +import os +import sys +import copy + +import vyos.configinterface as VyIfconfig + +from vyos.config import Config +from vyos import ConfigError + +default_config_data = { + 'address': [], + 'address_remove': [], + 'deleted': False, + 'description': '', + 'disable': False, + 'intf': '' +} + +def diff(first, second): + second = set(second) + return [item for item in first if item not in second] + +def get_config(): + dummy = copy.deepcopy(default_config_data) + conf = Config() + + # determine tagNode instance + try: + dummy['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + except KeyError as E: + print("Interface not specified") + + # Check if interface has been removed + if not conf.exists('interfaces dummy ' + dummy['intf']): + dummy['deleted'] = True + return dummy + + # set new configuration level + conf.set_level('interfaces dummy ' + dummy['intf']) + + # retrieve configured interface addresses + if conf.exists('address'): + dummy['address'] = conf.return_values('address') + + # retrieve interface description + if conf.exists('description'): + dummy['description'] = conf.return_value('description') + + # Disable this interface + if conf.exists('disable'): + dummy['disable'] = True + + # Determine interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed from the interface + eff_addr = conf.return_effective_values('address') + act_addr = conf.return_values('address') + dummy['address_remove'] = diff(eff_addr, act_addr) + + return dummy + +def verify(dummy): + if dummy is None: + return None + + return None + +def generate(dummy): + if dummy is None: + return None + + return None + +def apply(dummy): + if dummy is None: + return None + + # Remove dummy interface + if dummy['deleted']: + VyIfconfig.remove_interface(dummy['intf']) + else: + # Interface will only be added if it yet does not exist + VyIfconfig.add_interface('dummy', dummy['intf']) + + # update interface description used e.g. within SNMP + VyIfconfig.set_description(dummy['intf'], dummy['description']) + + # Configure interface address(es) + for addr in dummy['address_remove']: + VyIfconfig.remove_interface_address(dummy['intf'], addr) + + for addr in dummy['address']: + VyIfconfig.add_interface_address(dummy['intf'], addr) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) -- cgit v1.2.3 From dc0f641956d002fa8588ef8d1213791cf36e92f2 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 20 Aug 2019 11:50:55 +0200 Subject: powerdns: T1524: support setting allow-from network Netmasks (both IPv4 and IPv6) that are allowed to use the server. The default allows access only from RFC 1918 private IP addresses. Due to the aggressive nature of the internet these days, it is highly recommended to not open up the recursor for the entire internet. Questions from IP addresses not listed here are ignored and do not get an answer. https://docs.powerdns.com/recursor/settings.html#allow-from Imagine an ISP network with non RFC1918 IP adresses - they can't make use of PowerDNS recursor. As of now VyOS hat allow-from set to 0.0.0.0/0 and ::/0 which created an open resolver. If there is no allow-from statement a config-migrator will add the appropriate nodes to the configuration, resulting in: service { dns { forwarding { allow-from 0.0.0.0/0 allow-from ::/0 cache-size 0 ignore-hosts-file listen-address 192.0.2.1 } } } --- interface-definitions/dns-forwarding.xml | 17 ++++++++++ src/conf_mode/dns_forwarding.py | 21 ++++++------ src/migration-scripts/dns-forwarding/0-to-1 | 50 +++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 9 deletions(-) create mode 100755 src/migration-scripts/dns-forwarding/0-to-1 (limited to 'src') diff --git a/interface-definitions/dns-forwarding.xml b/interface-definitions/dns-forwarding.xml index 56820608c..08a221ebe 100644 --- a/interface-definitions/dns-forwarding.xml +++ b/interface-definitions/dns-forwarding.xml @@ -97,6 +97,23 @@ + + + Networks allowed to query this server + + ipv4net + IP address and prefix length + + + ipv6net + IPv6 address and prefix length + + + + + + + Addresses to listen for DNS queries [REQUIRED] diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index 3ca77adee..86683e72c 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -44,7 +44,7 @@ config_tmpl = """ # Non-configurable defaults daemon=yes threads=1 -allow-from=0.0.0.0/0, ::/0 +allow-from={{ allow_from | join(',') }} log-common-errors=yes non-local-bind=yes query-local-address=0.0.0.0 @@ -83,6 +83,7 @@ dnssec={{ dnssec }} """ default_config_data = { + 'allow_from': [], 'cache_size': 10000, 'export_hosts_file': 'yes', 'listen_on': [], @@ -121,6 +122,9 @@ def get_config(arguments): conf.set_level('service dns forwarding') + if conf.exists('allow-from'): + dns['allow_from'] = conf.return_values('allow-from') + if conf.exists('cache-size'): cache_size = conf.return_value('cache-size') dns['cache_size'] = cache_size @@ -216,12 +220,10 @@ def get_config(arguments): return dns - def bracketize_ipv6_addrs(addrs): """Wraps each IPv6 addr in addrs in [], leaving IPv4 addrs untouched.""" return ['[{0}]'.format(a) if a.count(':') > 1 else a for a in addrs] - def verify(dns): # bail out early - looks like removal from running config if dns is None: @@ -231,6 +233,10 @@ def verify(dns): raise ConfigError( "Error: DNS forwarding requires either a listen-address (preferred) or a listen-on option") + if not dns['allow_from']: + raise ConfigError( + "Error: DNS forwarding requires an allow-from network") + if dns['domains']: for domain in dns['domains']: if not domain['servers']: @@ -239,7 +245,6 @@ def verify(dns): return None - def generate(dns): # bail out early - looks like removal from running config if dns is None: @@ -251,16 +256,14 @@ def generate(dns): f.write(config_text) return None - def apply(dns): - if dns is not None: - os.system("systemctl restart pdns-recursor") - else: + if dns is None: # DNS forwarding is removed in the commit os.system("systemctl stop pdns-recursor") if os.path.isfile(config_file): os.unlink(config_file) - + else: + os.system("systemctl restart pdns-recursor") if __name__ == '__main__': args = parser.parse_args() diff --git a/src/migration-scripts/dns-forwarding/0-to-1 b/src/migration-scripts/dns-forwarding/0-to-1 new file mode 100755 index 000000000..6e8720eef --- /dev/null +++ b/src/migration-scripts/dns-forwarding/0-to-1 @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This migration script will check if there is a allow-from directive configured +# for the dns forwarding service - if not, the node will be created with the old +# default values of 0.0.0.0/0 and ::/0 + +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() + +config = ConfigTree(config_file) + +base = ['service', 'dns', 'forwarding'] +if not config.exists(base): + # Nothing to do + sys.exit(0) +else: + if not config.exists(base + ['allow-from']): + config.set(base + ['allow-from'], value='0.0.0.0/0', replace=False) + config.set(base + ['allow-from'], value='::/0', replace=False) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) -- cgit v1.2.3 From 3f225b56f576d30bd163f975c821e8baf2be6d28 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 20 Aug 2019 11:56:08 +0200 Subject: powerdns: T1595: add config migrator to remove 'listen-on' --- src/migration-scripts/dns-forwarding/1-to-2 | 78 +++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100755 src/migration-scripts/dns-forwarding/1-to-2 (limited to 'src') diff --git a/src/migration-scripts/dns-forwarding/1-to-2 b/src/migration-scripts/dns-forwarding/1-to-2 new file mode 100755 index 000000000..31ba5573f --- /dev/null +++ b/src/migration-scripts/dns-forwarding/1-to-2 @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# This migration script will remove the deprecated 'listen-on' statement +# from the dns forwarding service and will add the corresponding +# listen-address nodes instead. This is required as PowerDNS can only listen +# on interface addresses and not on interface names. + +import sys + +from ipaddress import ip_interface +from vyos.configtree import ConfigTree +from vyos.interfaces import get_type_of_interface + +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() + +config = ConfigTree(config_file) + +base = ['service', 'dns', 'forwarding'] +if not config.exists(base): + # Nothing to do + sys.exit(0) +else: + if config.exists(base + ['listen-on']): + listen_intf = config.return_values(base + ['listen-on']) + # Delete node with abandoned command + config.delete(base + ['listen-on']) + + # retrieve interface addresses for every configured listen-on interface + listen_addr = [] + for intf in listen_intf: + # we need to treat vif and vif-s interfaces differently, + # both "real interfaces" use dots for vlan identifiers - those + # need to be exchanged with vif and vif-s identifiers + if intf.count('.') == 1: + # this is a regular VLAN interface + intf = intf.split('.')[0] + ' vif ' + intf.split('.')[1] + elif intf.count('.') == 2: + # this is a QinQ VLAN interface + intf = intf.split('.')[0] + ' vif-s ' + intf.split('.')[1] + ' vif-c ' + intf.split('.')[2] + + path = ['interfaces', get_type_of_interface(intf), intf, 'address'] + + # retrieve corresponding interface addresses in CIDR format + # those need to be converted in pure IP addresses without network information + for addr in config.return_values(path): + listen_addr.append( ip_interface(addr).ip ) + + for addr in listen_addr: + config.set(base + ['listen-address'], value=addr, replace=False) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) -- cgit v1.2.3 From dbdd50e96f5af8f59d884f03df1cdeed9bac39d1 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 20 Aug 2019 12:02:49 +0200 Subject: powerdns: T1595: remove 'listen-on' CLI option --- interface-definitions/dns-forwarding.xml | 9 ------- src/conf_mode/dns_forwarding.py | 41 -------------------------------- 2 files changed, 50 deletions(-) (limited to 'src') diff --git a/interface-definitions/dns-forwarding.xml b/interface-definitions/dns-forwarding.xml index 08a221ebe..a88c174e3 100644 --- a/interface-definitions/dns-forwarding.xml +++ b/interface-definitions/dns-forwarding.xml @@ -132,15 +132,6 @@ - - - Interface to listen for DNS queries [DEPRECATED] - - - - - - Maximum amount of time negative entries are cached diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index 86683e72c..9e81c7294 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -87,7 +87,6 @@ default_config_data = { 'cache_size': 10000, 'export_hosts_file': 'yes', 'listen_on': [], - 'interfaces': [], 'name_servers': [], 'negative_ttl': 3600, 'domains': [], @@ -168,46 +167,6 @@ def get_config(arguments): if conf.exists('dnssec'): dns['dnssec'] = conf.return_value('dnssec') - ## Hacks and tricks - - # The old VyOS syntax that comes from dnsmasq was "listen-on $interface". - # pdns wants addresses instead, so we emulate it by looking up all addresses - # of a given interface and writing them to the config - if conf.exists('listen-on'): - print("WARNING: since VyOS 1.2.0, \"service dns forwarding listen-on\" is a limited compatibility option.") - print("It will only make DNS forwarder listen on addresses assigned to the interface at the time of commit") - print("which means it will NOT work properly with VRRP/clustering or addresses received from DHCP.") - print("Please reconfigure your system with \"service dns forwarding listen-address\" instead.") - - interfaces = conf.return_values('listen-on') - - listen4 = [] - listen6 = [] - for interface in interfaces: - try: - addrs = netifaces.ifaddresses(interface) - except ValueError: - print( - "WARNING: interface {0} does not exist".format(interface)) - continue - - if netifaces.AF_INET in addrs.keys(): - for ip4 in addrs[netifaces.AF_INET]: - listen4.append(ip4['addr']) - - if netifaces.AF_INET6 in addrs.keys(): - for ip6 in addrs[netifaces.AF_INET6]: - listen6.append(ip6['addr']) - - if (not listen4) and (not (listen6)): - print( - "WARNING: interface {0} has no configured addresses".format(interface)) - - dns['listen_on'] = dns['listen_on'] + listen4 + listen6 - - # Save interfaces in the dict for the reference - dns['interfaces'] = interfaces - # Add name servers received from DHCP if conf.exists('dhcp'): interfaces = [] -- cgit v1.2.3 From e5df80a3f54da23858c382a8e491ff9901317e1e Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Tue, 20 Aug 2019 18:53:35 +0200 Subject: T1598: initial implementation of the hosts keeper daemon. --- debian/control | 1 + python/vyos/hostsd_client.py | 56 +++++++++ src/services/vyos-hostsd | 254 ++++++++++++++++++++++++++++++++++++++++ src/systemd/vyos-hostsd.service | 22 ++++ src/utils/vyos-hostsd-client | 82 +++++++++++++ 5 files changed, 415 insertions(+) create mode 100644 python/vyos/hostsd_client.py create mode 100755 src/services/vyos-hostsd create mode 100644 src/systemd/vyos-hostsd.service create mode 100755 src/utils/vyos-hostsd-client (limited to 'src') diff --git a/debian/control b/debian/control index a65d0158e..f46e5d08e 100644 --- a/debian/control +++ b/debian/control @@ -28,6 +28,7 @@ Depends: python3, python3-hurry.filesize, python3-vici (>= 5.7.2), python3-bottle, + python3-zmq, ipaddrcheck, tcpdump, tshark, diff --git a/python/vyos/hostsd_client.py b/python/vyos/hostsd_client.py new file mode 100644 index 000000000..e02aefe6f --- /dev/null +++ b/python/vyos/hostsd_client.py @@ -0,0 +1,56 @@ +import json + +import zmq + + +SOCKET_PATH = "ipc:///run/vyos-hostsd.sock" + + +class VyOSHostsdError(Exception): + pass + + +class Client(object): + def __init__(self): + context = zmq.Context() + self.__socket = context.socket(zmq.REQ) + self.__socket.RCVTIMEO = 10000 #ms + self.__socket.setsockopt(zmq.LINGER, 0) + self.__socket.connect(SOCKET_PATH) + + def _communicate(self, msg): + request = json.dumps(msg).encode() + self.__socket.send(request) + + reply_msg = self.__socket.recv().decode() + reply = json.loads(reply_msg) + if 'error' in reply: + raise VyOSHostsdError(reply['error']) + + def set_host_name(self, host_name, domain_name, search_domains): + msg = { + 'type': 'host_name', + 'op': 'set', + 'data': { + 'host_name': host_name, + 'domain_name': domain_name, + 'search_domains': search_domains + } + } + self._communicate(msg) + + def add_hosts(self, tag, hosts): + msg = {'type': 'hosts', 'op': 'add', 'tag': tag, 'data': hosts} + self._communicate(msg) + + def delete_hosts(self, tag): + msg = {'type': 'hosts', 'op': 'delete', 'tag': tag} + self._communicate(msg) + + def add_name_servers(self, tag, servers): + msg = {'type': 'name_servers', 'op': 'add', 'tag': tag, 'data': servers} + self._communicate(msg) + + def delete_name_servers(self, tag): + msg = {'type': 'name_servers', 'op': 'delete', 'tag': tag} + self._communicate(msg) diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd new file mode 100755 index 000000000..972c2e3dc --- /dev/null +++ b/src/services/vyos-hostsd @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# + +import os +import sys +import time +import json +import traceback + +import zmq + +import jinja2 + +debug = True + +DATA_DIR = "/var/lib/vyos/" +STATE_FILE = os.path.join(DATA_DIR, "hostsd.state") + +SOCKET_PATH = "ipc:///run/vyos-hostsd.sock" + +RESOLV_CONF_FILE = '/etc/resolv.conf' +HOSTS_FILE = '/etc/hosts' + +hosts_tmpl_source = """ +### Autogenerated by VyOS ### +127.0.0.1 localhost +127.0.1.1 {{ host_name }}{% if domain_name %}.{{ domain_name }}{% endif %} + +# The following lines are desirable for IPv6 capable hosts +::1 localhost ip6-localhost ip6-loopback +fe00::0 ip6-localnet +ff00::0 ip6-mcastprefix +ff02::1 ip6-allnodes +ff02::2 ip6-allrouters + + +{%- if hosts %} +{% for h in hosts -%} +{{hosts[h]['address']}}\t{{h}}\t{% for a in hosts[h]['aliases'] %} {{a}} {% endfor %} +{% endfor %} +{%- endif %} +""" + +hosts_tmpl = jinja2.Template(hosts_tmpl_source) + +resolv_tmpl_source = """ +### Autogenerated by VyOS ### +{% for ns in name_servers -%} +nameserver {{ns}} +{% endfor -%} + +{%- if domain_name %} +domain {{ domain_name }} +{%- endif %} + +{%- if search_domains %} +search {{ search_domains | join(" ") }} +{%- endif %} + +""" + +resolv_tmpl = jinja2.Template(resolv_tmpl_source) + +# The state data includes a list of name servers +# and a list of hosts entries. +# +# Name servers have the following structure: +# {"server": {"tag": }} +# +# Hosts entries are similar: +# {"host": {"tag": , "address": }} +# +# The tag is either "static" or "dhcp-" +# It's used to distinguish entries created +# by different scripts so that they can be removed +# and re-created without having to track what needs +# to be changed +STATE = { + "name_servers": {}, + "hosts": {}, + "host_name": "vyos", + "domain_name": "", + "search_domains": []} + + +def make_resolv_conf(data): + resolv_conf = resolv_tmpl.render(data) + print("Writing /etc/resolv.conf") + with open(RESOLV_CONF_FILE, 'w') as f: + f.write(resolv_conf) + +def make_hosts_file(state): + print("Writing /etc/hosts") + hosts = hosts_tmpl.render(state) + with open(HOSTS_FILE, 'w') as f: + f.write(hosts) + +def add_hosts(data, entries, tag): + hosts = data['hosts'] + + if not entries: + return + + for e in entries: + host = e['host'] + hosts[host] = {} + hosts[host]['tag'] = tag + hosts[host]['address'] = e['address'] + hosts[host]['aliases'] = e['aliases'] + +def delete_hosts(data, tag): + hosts = data['hosts'] + keys_for_deletion = [] + + # You can't delete items from a dict while iterating over it, + # so we build a list of doomed items first + for h in hosts: + if hosts[h]['tag'] == tag: + keys_for_deletion.append(h) + + for k in keys_for_deletion: + del hosts[k] + +def add_name_servers(data, entries, tag): + name_servers = data['name_servers'] + + if not entries: + return + + for e in entries: + name_servers[e] = {} + name_servers[e]['tag'] = tag + +def delete_name_servers(data, tag): + name_servers = data['name_servers'] + keys_for_deletion = [] + + for ns in name_servers: + if name_servers[ns]['tag'] == tag: + keys_for_deletion.append(ns) + + for k in keys_for_deletion: + del name_servers[k] + +def set_host_name(state, data): + if data['host_name']: + state['host_name'] = data['host_name'] + if data['domain_name']: + state['domain_name'] = data['domain_name'] + if data['search_domains']: + state['search_domains'] = data['search_domains'] + +def get_option(msg, key): + if key in msg: + return msg[key] + else: + raise ValueError("Missing required option \"{0}\"".format(key)) + +def handle_message(msg_json): + msg = json.loads(msg_json) + + op = get_option(msg, 'op') + _type = get_option(msg, 'type') + + if op == 'delete': + tag = get_option(msg, 'tag') + + if _type == 'name_servers': + delete_name_servers(STATE, tag) + elif _type == 'hosts': + delete_hosts(STATE, tag) + else: + raise ValueError("Unknown message type {0}".format(_type)) + elif op == 'add': + tag = get_option(msg, 'tag') + entries = get_option(msg, 'data') + if _type == 'name_servers': + add_name_servers(STATE, entries, tag) + elif _type == 'hosts': + add_hosts(STATE, entries, tag) + else: + raise ValueError("Unknown message type {0}".format(_type)) + elif op == 'set': + # Host name/domain name/search domain are set without a tag, + # there can be only one anyway + data = get_option(msg, 'data') + if _type == 'host_name': + set_host_name(STATE, data) + else: + raise ValueError("Unknown message type {0}".format(_type)) + else: + raise ValueError("Unknown operation {0}".format(op)) + + make_resolv_conf(STATE) + make_hosts_file(STATE) + + print("Saving state to {0}".format(STATE_FILE)) + with open(STATE_FILE, 'w') as f: + json.dump(STATE, f) + + +if __name__ == '__main__': + # Create a directory for state checkpoints + os.makedirs(DATA_DIR, exist_ok=True) + if os.path.exists(STATE_FILE): + with open(STATE_FILE, 'r') as f: + try: + data = json.load(f) + STATE = data + except: + print(traceback.format_exc()) + print("Failed to load the state file, using default") + + context = zmq.Context() + socket = context.socket(zmq.REP) + socket.bind(SOCKET_PATH) + + while True: + # Wait for next request from client + message = socket.recv().decode() + print("Received a configuration change request") + if debug: + print("Request data: {0}".format(message)) + + resp = {} + + try: + handle_message(message) + except ValueError as e: + resp['error'] = str(e) + except: + print(traceback.format_exc()) + resp['error'] = "Internal error" + + if debug: + print("Sent response: {0}".format(resp)) + + # Send reply back to client + socket.send(json.dumps(resp).encode()) diff --git a/src/systemd/vyos-hostsd.service b/src/systemd/vyos-hostsd.service new file mode 100644 index 000000000..fe6c163d7 --- /dev/null +++ b/src/systemd/vyos-hostsd.service @@ -0,0 +1,22 @@ +[Unit] +Description=VyOS DNS configuration keeper +After=auditd.service systemd-user-sessions.service time-sync.target + +[Service] +ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-hostsd +Type=idle +KillMode=process + +SyslogIdentifier=vyos-hostsd +SyslogFacility=daemon + +Restart=on-failure + +# Does't work but leave it here +User=root +Group=vyattacfg + +[Install] +# Installing in a earlier target leaves ExecStartPre waiting +WantedBy=getty.target + diff --git a/src/utils/vyos-hostsd-client b/src/utils/vyos-hostsd-client new file mode 100755 index 000000000..d3105c9cf --- /dev/null +++ b/src/utils/vyos-hostsd-client @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# + +import sys +import argparse + +import vyos.hostsd_client + +parser = argparse.ArgumentParser() +group = parser.add_mutually_exclusive_group() +group.add_argument('--add-hosts', action="store_true") +group.add_argument('--delete-hosts', action="store_true") +group.add_argument('--add-name-servers', action="store_true") +group.add_argument('--delete-name-servers', action="store_true") +group.add_argument('--set-host-name', action="store_true") + +parser.add_argument('--host', type=str, action="append") +parser.add_argument('--name-server', type=str, action="append") +parser.add_argument('--host-name', type=str) +parser.add_argument('--domain-name', type=str) +parser.add_argument('--search-domain', type=str, action="append") + +parser.add_argument('--tag', type=str) + +args = parser.parse_args() + +try: + client = vyos.hostsd_client.Client() + + if args.add_hosts: + if not args.tag: + raise ValueError("Tag is required for this operation") + data = [] + for h in args.host: + entry = {} + params = h.split(",") + if len(params) < 2: + raise ValueError("Malformed host entry") + entry['host'] = params[0] + entry['address'] = params[1] + entry['aliases'] = params[2:] + data.append(entry) + client.add_hosts(args.tag, data) + elif args.delete_hosts: + if not args.tag: + raise ValueError("Tag is required for this operation") + client.delete_hosts(args.tag) + elif args.add_name_servers: + if not args.tag: + raise ValueError("Tag is required for this operation") + client.add_name_servers(args.tag, args.name_server) + elif args.delete_name_servers: + if not args.tag: + raise ValueError("Tag is required for this operation") + client.delete_name_servers(args.tag) + elif args.set_host_name: + client.set_host_name(args.host_name, args.domain_name, args.search_domain) + else: + raise ValueError("Operation required") + +except ValueError as e: + print("Incorrect options: {0}".format(e)) + sys.exit(1) +except vyos.hostsd_client.VyOSHostsdError as e: + print("Server returned an error: {0}".format(e)) + sys.exit(1) + -- cgit v1.2.3 From 525438ac4ad1e4fba534c7d1c51283c4c2561d3e Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Tue, 20 Aug 2019 19:47:16 +0200 Subject: T1598: start vyos-hostsd before everything that may need it. --- src/systemd/vyos-hostsd.service | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/systemd/vyos-hostsd.service b/src/systemd/vyos-hostsd.service index fe6c163d7..8ff213d2b 100644 --- a/src/systemd/vyos-hostsd.service +++ b/src/systemd/vyos-hostsd.service @@ -1,6 +1,7 @@ [Unit] Description=VyOS DNS configuration keeper After=auditd.service systemd-user-sessions.service time-sync.target +Before=vyos-router.service cloud-init.service [Service] ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-hostsd -- cgit v1.2.3 From 3cbebc67f970f1add6548c5e386b40d5632f8452 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 21 Aug 2019 08:46:56 +0200 Subject: T1598: improve autogenerated file comments. --- src/services/vyos-hostsd | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd index 972c2e3dc..aec06c72c 100755 --- a/src/services/vyos-hostsd +++ b/src/services/vyos-hostsd @@ -38,6 +38,9 @@ HOSTS_FILE = '/etc/hosts' hosts_tmpl_source = """ ### Autogenerated by VyOS ### +### Do not edit, your changes will get overwritten ### + +# Local host 127.0.0.1 localhost 127.0.1.1 {{ host_name }}{% if domain_name %}.{{ domain_name }}{% endif %} @@ -48,7 +51,7 @@ ff00::0 ip6-mcastprefix ff02::1 ip6-allnodes ff02::2 ip6-allrouters - +# From DHCP and "system static host-mapping" {%- if hosts %} {% for h in hosts -%} {{hosts[h]['address']}}\t{{h}}\t{% for a in hosts[h]['aliases'] %} {{a}} {% endfor %} @@ -60,6 +63,8 @@ hosts_tmpl = jinja2.Template(hosts_tmpl_source) resolv_tmpl_source = """ ### Autogenerated by VyOS ### +### Do not edit, your changes will get overwritten ### + {% for ns in name_servers -%} nameserver {{ns}} {% endfor -%} @@ -83,7 +88,7 @@ resolv_tmpl = jinja2.Template(resolv_tmpl_source) # {"server": {"tag": }} # # Hosts entries are similar: -# {"host": {"tag": , "address": }} +# {"host": {"tag": , "address": , "aliases": }} # # The tag is either "static" or "dhcp-" # It's used to distinguish entries created -- cgit v1.2.3 From a923efc0062119189d876e37514bd24b7773509e Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 21 Aug 2019 09:08:27 +0200 Subject: T1598: clean up vyos-hostsd state dump on clean shutdown. --- src/services/vyos-hostsd | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'src') diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd index aec06c72c..1bcb2e1f5 100755 --- a/src/services/vyos-hostsd +++ b/src/services/vyos-hostsd @@ -20,6 +20,7 @@ import os import sys import time import json +import signal import traceback import zmq @@ -218,8 +219,16 @@ def handle_message(msg_json): with open(STATE_FILE, 'w') as f: json.dump(STATE, f) +def exit_handler(sig, frame): + """ Clean up the state when shutdown correctly """ + print("Cleaning up state") + os.unlink(STATE_FILE) + sys.exit(0) + if __name__ == '__main__': + signal.signal(signal.SIGTERM, exit_handler) + # Create a directory for state checkpoints os.makedirs(DATA_DIR, exist_ok=True) if os.path.exists(STATE_FILE): -- cgit v1.2.3 From 63855b5c6b674d102804b43bcda85499a43973a4 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 21 Aug 2019 09:09:07 +0200 Subject: T1598: redo host_name.py to use vyos-hostsd. --- src/conf_mode/host_name.py | 196 +++++++++------------------------------------ 1 file changed, 36 insertions(+), 160 deletions(-) (limited to 'src') diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index 2fad57db6..bb1ec9597 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -30,57 +30,12 @@ import argparse import jinja2 import vyos.util +import vyos.hostsd_client from vyos.config import Config from vyos import ConfigError -parser = argparse.ArgumentParser() -parser.add_argument("--dhclient", action="store_true", - help="Started from dhclient-script") - -config_file_hosts = '/etc/hosts' -config_file_resolv = '/etc/resolv.conf' - -config_tmpl_hosts = """ -### Autogenerated by host_name.py ### -127.0.0.1 localhost -127.0.1.1 {{ hostname }}{% if domain_name %}.{{ domain_name }} {{ hostname }}{% endif %} - -# The following lines are desirable for IPv6 capable hosts -::1 localhost ip6-localhost ip6-loopback -fe00::0 ip6-localnet -ff00::0 ip6-mcastprefix -ff02::1 ip6-allnodes -ff02::2 ip6-allrouters - -# static hostname mappings -{%- if static_host_mapping['hostnames'] %} -{% for hn in static_host_mapping['hostnames'] -%} -{{static_host_mapping['hostnames'][hn]['ipaddr']}}\t{{static_host_mapping['hostnames'][hn]['alias']}}\t{{hn}} -{% endfor -%} -{%- endif %} - -### modifications from other scripts should be added below - -""" - -config_tmpl_resolv = """ -### Autogenerated by host_name.py ### -{% for ns in nameserver -%} -nameserver {{ ns }} -{% endfor -%} - -{%- if domain_name %} -domain {{ domain_name }} -{%- endif %} - -{%- if domain_search %} -search {{ domain_search | join(" ") }} -{%- endif %} - -""" - default_config_data = { 'hostname': 'vyos', 'domain_name': '', @@ -89,32 +44,10 @@ default_config_data = { 'no_dhcp_ns': False } -# borrowed from: https://github.com/donjajo/py-world/blob/master/resolvconfReader.py, THX! -def get_resolvers(file): - resolv = {} - try: - with open(file, 'r') as resolvconf: - lines = [line.split('#', 1)[0].rstrip() - for line in resolvconf.readlines()] - resolvers = [line.split()[1] - for line in lines if 'nameserver' in line] - domains = [line.split()[1] for line in lines if 'search' in line] - resolv['resolvers'] = resolvers - resolv['domains'] = domains - return resolv - except IOError: - return [] - - -def get_config(arguments): +def get_config(): conf = Config() hosts = copy.deepcopy(default_config_data) - if arguments.dhclient: - conf.exists = conf.exists_effective - conf.return_value = conf.return_effective_value - conf.return_values = conf.return_effective_values - if conf.exists("system host-name"): hosts['hostname'] = conf.return_value("system host-name") # This may happen if the config is not loaded yet, @@ -136,19 +69,15 @@ def get_config(arguments): hosts['no_dhcp_ns'] = conf.exists('system disable-dhcp-nameservers') # system static-host-mapping - hosts['static_host_mapping'] = {'hostnames': {}} + hosts['static_host_mapping'] = [] if conf.exists('system static-host-mapping host-name'): for hn in conf.list_nodes('system static-host-mapping host-name'): - hosts['static_host_mapping']['hostnames'][hn] = { - 'ipaddr': conf.return_value('system static-host-mapping host-name ' + hn + ' inet'), - 'alias': '' - } - - if conf.exists('system static-host-mapping host-name ' + hn + ' alias'): - a = conf.return_values( - 'system static-host-mapping host-name ' + hn + ' alias') - hosts['static_host_mapping']['hostnames'][hn]['alias'] = " ".join(a) + mapping = {} + mapping['host'] = hn + mapping['address'] = conf.return_value('system static-host-mapping host-name {0} inet'.format(hn)) + mapping['aliases'] = conf.return_values('system static-host-mapping host-name {0} alias'.format(hn)) + hosts['static_host_mapping'].append(mapping) return hosts @@ -180,83 +109,43 @@ def verify(config): 'The search list is currently limited to 256 characters') # static mappings alias hostname - if config['static_host_mapping']['hostnames']: - for hn in config['static_host_mapping']['hostnames']: - if not config['static_host_mapping']['hostnames'][hn]['ipaddr']: - raise ConfigError('IP address required for ' + hn) - for hn_alias in config['static_host_mapping']['hostnames'][hn]['alias'].split(' '): - if not hostname_regex.match(hn_alias) and len(hn_alias) != 0: - raise ConfigError('Invalid hostname alias ' + hn_alias) + if config['static_host_mapping']: + for m in config['static_host_mapping']: + if not m['address']: + raise ConfigError('IP address required for ' + m['host']) + for a in m['aliases']: + if not hostname_regex.match(a) and len(a) != 0: + raise ConfigError('Invalid alias \'{0}\' in mapping {1}'.format(a, m['host'])) return None def generate(config): + pass + +def apply(config): if config is None: return None - # If "system disable-dhcp-nameservers" is __configured__ all DNS resolvers - # received via dhclient should not be added into the final 'resolv.conf'. - # - # We iterate over every resolver file and retrieve the received nameservers - # for later adjustment of the system nameservers - dhcp_ns = [] - dhcp_sd = [] - for file in glob.glob('/etc/resolv.conf.dhclient-new*'): - for key, value in get_resolvers(file).items(): - ns = [r for r in value if key == 'resolvers'] - dhcp_ns.extend(ns) - sd = [d for d in value if key == 'domains'] - dhcp_sd.extend(sd) - - if not config['no_dhcp_ns']: - config['nameserver'] += dhcp_ns - config['domain_search'] += dhcp_sd - - # Prune duplicate values - # Not order preserving, but then when multiple DHCP clients are used, - # there can't be guarantees about the order anyway - dhcp_ns = list(set(dhcp_ns)) - dhcp_sd = list(set(dhcp_sd)) - - # We have third party scripts altering /etc/hosts, too. - # One example are the DHCP hostname update scripts thus we need to cache in - # every modification first - so changing domain-name, domain-search or hostname - # during runtime works - old_hosts = "" - with open(config_file_hosts, 'r') as f: - # Skips text before the beginning of our marker. - # NOTE: Marker __MUST__ match the one specified in config_tmpl_hosts - for line in f: - if line.strip() == '### modifications from other scripts should be added below': - break - - for line in f: - # This additional line.strip() filters empty lines - if line.strip(): - old_hosts += line - - # Add an additional newline - old_hosts += '\n' - - tmpl = jinja2.Template(config_tmpl_hosts) - config_text = tmpl.render(config) - - with open(config_file_hosts, 'w') as f: - f.write(config_text) - f.write(old_hosts) - - tmpl = jinja2.Template(config_tmpl_resolv) - config_text = tmpl.render(config) - with open(config_file_resolv, 'w') as f: - f.write(config_text) + ## Send the updated data to vyos-hostsd - return None + # vyos-hostsd uses "tags" to identify data sources + tag = "static" + try: + client = vyos.hostsd_client.Client() -def apply(config): - if config is None: - return None + client.set_host_name(config['hostname'], config['domain_name'], config['domain_search']) + + client.delete_name_servers(tag) + client.add_name_servers(tag, config['nameserver']) + + client.delete_hosts(tag) + client.add_hosts(tag, config['static_host_mapping']) + except vyos.hostsd_client.VyOSHostsdError as e: + raise ConfigError(str(e)) + + ## Actually update the hostname -- vyos-hostsd doesn't do that # No domain name -- the Debian way. hostname_new = config['hostname'] @@ -283,22 +172,9 @@ def apply(config): if __name__ == '__main__': - args = parser.parse_args() - - if args.dhclient: - # There's a big chance it was triggered by a commit still in progress - # so we need to wait until the new values are in the running config - vyos.util.wait_for_commit_lock() - - try: - c = get_config(args) - # If it's called from dhclient, then either: - # a) verification was already done at commit time - # b) it's run on an unconfigured system, e.g. by cloud-init - # Therefore, verification is either redundant or useless - if not args.dhclient: - verify(c) + c = get_config() + verify(c) generate(c) apply(c) except ConfigError as e: -- cgit v1.2.3 From fe343f428a9740cdd3c6cf966f84238a14cd49fc Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 21 Aug 2019 18:31:42 +0200 Subject: loopback: T1601: rewrite using XML/Python definitions --- interface-definitions/interfaces-loopback.xml | 46 ++++++++++++ src/conf_mode/interface-loopback.py | 102 ++++++++++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 interface-definitions/interfaces-loopback.xml create mode 100755 src/conf_mode/interface-loopback.py (limited to 'src') diff --git a/interface-definitions/interfaces-loopback.xml b/interface-definitions/interfaces-loopback.xml new file mode 100644 index 000000000..267731b1c --- /dev/null +++ b/interface-definitions/interfaces-loopback.xml @@ -0,0 +1,46 @@ + + + + + + + Loopback interface + 300 + + lo$ + + Loopback interface must be named lo + + lo + Loopback interface + + + + + + IP address + + ipv4net + IPv4 address and prefix length + + + ipv6net + IPv6 address and prefix length + + + + + + + Interface description + + ^.{1,256}$ + + Interface description too long (limit 256 characters) + + + + + + + diff --git a/src/conf_mode/interface-loopback.py b/src/conf_mode/interface-loopback.py new file mode 100755 index 000000000..445a9af64 --- /dev/null +++ b/src/conf_mode/interface-loopback.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# + +import os +import sys +import copy + +import vyos.configinterface as VyIfconfig + +from vyos.config import Config +from vyos import ConfigError + +default_config_data = { + 'address': [], + 'address_remove': [], + 'deleted': False, + 'description': '', +} + +def diff(first, second): + second = set(second) + return [item for item in first if item not in second] + +def get_config(): + loopback = copy.deepcopy(default_config_data) + conf = Config() + + # determine tagNode instance + try: + loopback['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + except KeyError as E: + print("Interface not specified") + + # Check if interface has been removed + if not conf.exists('interfaces loopback ' + loopback['intf']): + loopback['deleted'] = True + + # set new configuration level + conf.set_level('interfaces loopback ' + loopback['intf']) + + # retrieve configured interface addresses + if conf.exists('address'): + loopback['address'] = conf.return_values('address') + + # retrieve interface description + if conf.exists('description'): + loopback['description'] = conf.return_value('description') + + # Determine interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed from the interface + eff_addr = conf.return_effective_values('address') + act_addr = conf.return_values('address') + loopback['address_remove'] = diff(eff_addr, act_addr) + + return loopback + +def verify(loopback): + return None + +def generate(loopback): + return None + +def apply(loopback): + # Remove loopback interface + if not loopback['deleted']: + # update interface description used e.g. within SNMP + VyIfconfig.set_description(loopback['intf'], loopback['description']) + + # Configure interface address(es) + for addr in loopback['address']: + VyIfconfig.add_interface_address(loopback['intf'], addr) + + # Remove interface address(es) + for addr in loopback['address_remove']: + VyIfconfig.remove_interface_address(loopback['intf'], addr) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) -- cgit v1.2.3 From 63cdf781f4604ed38591d0be71fa92ae3667da73 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 21 Aug 2019 18:33:06 +0200 Subject: bridge: T1556: remove superfluous if statements --- src/conf_mode/interface-bridge.py | 9 --------- 1 file changed, 9 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index 543349e7b..65b5c4066 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -178,9 +178,6 @@ def get_config(): return bridge def verify(bridge): - if bridge is None: - return None - conf = Config() for br in conf.list_nodes('interfaces bridge'): # it makes no sense to verify ourself in this case @@ -195,15 +192,9 @@ def verify(bridge): return None def generate(bridge): - if bridge is None: - return None - return None def apply(bridge): - if bridge is None: - return None - cmd = '' if bridge['deleted']: # bridges need to be shutdown first -- cgit v1.2.3 From d553b380d59961093f777ce55f7e98cc8bd1844b Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 21 Aug 2019 18:33:30 +0200 Subject: dummy: T1580: remove superfluous if statements --- src/conf_mode/interface-dummy.py | 9 --------- 1 file changed, 9 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interface-dummy.py b/src/conf_mode/interface-dummy.py index 668e4acc7..ff9d57c89 100755 --- a/src/conf_mode/interface-dummy.py +++ b/src/conf_mode/interface-dummy.py @@ -77,21 +77,12 @@ def get_config(): return dummy def verify(dummy): - if dummy is None: - return None - return None def generate(dummy): - if dummy is None: - return None - return None def apply(dummy): - if dummy is None: - return None - # Remove dummy interface if dummy['deleted']: VyIfconfig.remove_interface(dummy['intf']) -- cgit v1.2.3 From db67a7e9e6f448567203ceb5f6364e78c7bb3300 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Thu, 22 Aug 2019 12:51:12 -0500 Subject: T1606: change vyos-hostsd systemd target to avoid boot problems --- src/systemd/vyos-hostsd.service | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/systemd/vyos-hostsd.service b/src/systemd/vyos-hostsd.service index 8ff213d2b..3b0fadb5c 100644 --- a/src/systemd/vyos-hostsd.service +++ b/src/systemd/vyos-hostsd.service @@ -1,7 +1,8 @@ [Unit] Description=VyOS DNS configuration keeper -After=auditd.service systemd-user-sessions.service time-sync.target -Before=vyos-router.service cloud-init.service +After=auditd.service time-sync.target +Before=network-pre.target vyos-router.service +Wants=network-pre.target [Service] ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-hostsd @@ -18,6 +19,6 @@ User=root Group=vyattacfg [Install] -# Installing in a earlier target leaves ExecStartPre waiting -WantedBy=getty.target +# +WantedBy=network.target -- cgit v1.2.3 From 87be8bbd814f2e387c3b60fbd3f44e55a21b2bce Mon Sep 17 00:00:00 2001 From: DmitriyEshenko Date: Fri, 23 Aug 2019 13:26:08 +0000 Subject: [dummy] T1609 Fixing dummy interface state --- interface-definitions/interfaces-dummy.xml | 1 + src/conf_mode/interface-dummy.py | 6 ++++++ 2 files changed, 7 insertions(+) (limited to 'src') diff --git a/interface-definitions/interfaces-dummy.xml b/interface-definitions/interfaces-dummy.xml index 7c425480a..4450b69b7 100644 --- a/interface-definitions/interfaces-dummy.xml +++ b/interface-definitions/interfaces-dummy.xml @@ -42,6 +42,7 @@ Disable interface + diff --git a/src/conf_mode/interface-dummy.py b/src/conf_mode/interface-dummy.py index ff9d57c89..8c939ce95 100755 --- a/src/conf_mode/interface-dummy.py +++ b/src/conf_mode/interface-dummy.py @@ -22,6 +22,7 @@ import copy import vyos.configinterface as VyIfconfig +from vyos.interfaceconfig import Interface from vyos.config import Config from vyos import ConfigError @@ -100,6 +101,11 @@ def apply(dummy): for addr in dummy['address']: VyIfconfig.add_interface_address(dummy['intf'], addr) + if dummy['disable']: + Interface(dummy['intf']).linkstate = 'down' + else: + Interface(dummy['intf']).linkstate = 'up' + return None if __name__ == '__main__': -- cgit v1.2.3 From ca1caa6c6effb82b22dad0db4c4f47247c3722ad Mon Sep 17 00:00:00 2001 From: Dmytro Aleksandrov Date: Thu, 22 Aug 2019 17:26:30 +0300 Subject: [op-mode] T1607 rewrite 'reset conntrack', 'reset & show ip[v6]' to python/xml syntax --- Makefile | 4 +- op-mode-definitions/dns-forwarding.xml | 3 + op-mode-definitions/ipv4-route.xml | 125 ++++++++++++++++++++++++++++++ op-mode-definitions/ipv6-route.xml | 133 ++++++++++++++++++++++++++++++++ op-mode-definitions/openvpn.xml | 3 + op-mode-definitions/reset-conntrack.xml | 16 ++++ python/vyos/util.py | 15 ++++ src/op_mode/clear_conntrack.py | 26 +++++++ src/op_mode/powerctrl.py | 17 +--- 9 files changed, 326 insertions(+), 16 deletions(-) create mode 100644 op-mode-definitions/ipv4-route.xml create mode 100644 op-mode-definitions/ipv6-route.xml create mode 100644 op-mode-definitions/reset-conntrack.xml create mode 100755 src/op_mode/clear_conntrack.py (limited to 'src') diff --git a/Makefile b/Makefile index ee01e5ad3..186e63678 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,9 @@ op_mode_definitions: rm -f $(OP_TMPL_DIR)/show/node.def rm -f $(OP_TMPL_DIR)/show/interfaces/node.def rm -f $(OP_TMPL_DIR)/show/ip/node.def - rm -f $(OP_TMPL_DIR)/reset/node.def + rm -f $(OP_TMPL_DIR)/show/ip/route/node.def + rm -f $(OP_TMPL_DIR)/show/ipv6/node.def + rm -f $(OP_TMPL_DIR)/show/ipv6/route/node.def rm -f $(OP_TMPL_DIR)/restart/node.def rm -f $(OP_TMPL_DIR)/monitor/node.def rm -f $(OP_TMPL_DIR)/generate/node.def diff --git a/op-mode-definitions/dns-forwarding.xml b/op-mode-definitions/dns-forwarding.xml index ac141174f..785a05e9c 100644 --- a/op-mode-definitions/dns-forwarding.xml +++ b/op-mode-definitions/dns-forwarding.xml @@ -42,6 +42,9 @@ + + Reset a service + diff --git a/op-mode-definitions/ipv4-route.xml b/op-mode-definitions/ipv4-route.xml new file mode 100644 index 000000000..d2846a6f2 --- /dev/null +++ b/op-mode-definitions/ipv4-route.xml @@ -0,0 +1,125 @@ + + + + + Show system information + + + + + Show IPv4 information + + + + + Show IP multicast group membership + + netstat -gn4 + + + + + Show IP routes + + + + + Show kernel route cache + + ip -s route list cache + + + + Show kernel route cache for a given route + + <x.x.x.x> <x.x.x.x/x> + + + ip -s route list cache $5 + + + + Show kernel route table + + ip route list + + + + Show kernel route table for a given route + + <x.x.x.x> <x.x.x.x/x> + + + ip -s route list $5 + + + + + + + + + + + Reset a service + + + + + Reset Internet Protocol (IP) parameters + + + + + Reset Address Resolution Protocol (ARP) cache + + + + + Reset ARP cache for an IPv4 address + + <x.x.x.x> + + + sudo /sbin/ip neigh flush to "$5" + + + + Reset ARP cache for interface + + + + + sudo /sbin/ip neigh flush dev "$5" + + + + + + + Reset IP route + + + + + Flush the kernel route cache + + sudo /sbin/ip route flush cache + + + + + Flush the kernel route cache for a given route + + <x.x.x.x> <x.x.x.x/x> + + + sudo /sbin/ip route flush cache "$5" + + + + + + + + diff --git a/op-mode-definitions/ipv6-route.xml b/op-mode-definitions/ipv6-route.xml new file mode 100644 index 000000000..fbf6489ba --- /dev/null +++ b/op-mode-definitions/ipv6-route.xml @@ -0,0 +1,133 @@ + + + + + Show system information + + + + + Show IPv6 routing information + + + + + Show IPv6 multicast group membership + + netstat -gn6 + + + + + Show IPv6 Neighbor Discovery (ND) information + + ip -f inet6 neigh list + + + + + Show IPv6 routes + + + + + Show kernel IPv6 route cache + + ip -s -f inet6 route list cache + + + + Show kernel IPv6 route cache for a given route + + <h:h:h:h:h:h:h:h> <h:h:h:h:h:h:h:h/x> + + + ip -s -f inet6 route list cache $5 + + + + Show kernel IPv6 route table + + ip -f inet6 route list + + + + Show kernel IPv6 route table for a given route + + <h:h:h:h:h:h:h:h> <h:h:h:h:h:h:h:h/x> + + + ip -s -f inet6 route list $5 + + + + + + + + + + + + Reset a service + + + + + Reset Internet Protocol version 6 (IPv6) parameters + + + + + Reset IPv6 Neighbor Discovery (ND) cache + + + + + Reset ND cache for an IPv6 address + + <h:h:h:h:h:h:h:h> + + + sudo ip -f inet6 neigh flush to "$5" + + + + Reset IPv6 ND cache for interface + + + + + sudo ip -f inet6 neigh flush dev "$5" + + + + + + + Reset IPv6 route + + + + + Flush the kernel IPv6 route cache + + sudo ip -f inet6 route flush cache + + + + + Flush the kernel IPv6 route cache for a given route + + <h:h:h:h:h:h:h:h> <h:h:h:h:h:h:h:h/x> + + + sudo ip -f inet6 route flush cache "$5" + + + + + + + + diff --git a/op-mode-definitions/openvpn.xml b/op-mode-definitions/openvpn.xml index 4c958257a..ac0c42789 100644 --- a/op-mode-definitions/openvpn.xml +++ b/op-mode-definitions/openvpn.xml @@ -46,6 +46,9 @@ + + Reset a service + diff --git a/op-mode-definitions/reset-conntrack.xml b/op-mode-definitions/reset-conntrack.xml new file mode 100644 index 000000000..827ba4af4 --- /dev/null +++ b/op-mode-definitions/reset-conntrack.xml @@ -0,0 +1,16 @@ + + + + + Reset a service + + + + + Reset all currently tracked connections + + sudo ${vyos_op_scripts_dir}/clear_conntrack.py + + + + diff --git a/python/vyos/util.py b/python/vyos/util.py index 6ab606983..88bb0c8f4 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -18,6 +18,7 @@ import re import grp import time import subprocess +import sys import psutil @@ -176,3 +177,17 @@ def wait_for_commit_lock(): while commit_in_progress(): time.sleep(1) +def ask_yes_no(question, default=False) -> bool: + """Ask a yes/no question via input() and return their answer.""" + default_msg = "[Y/n]" if default else "[y/N]" + while True: + sys.stdout.write("%s %s " % (question, default_msg)) + c = input().lower() + if c == '': + return default + elif c in ("y", "ye", "yes"): + return True + elif c in ("n", "no"): + return False + else: + sys.stdout.write("Please respond with yes/y or no/n\n") diff --git a/src/op_mode/clear_conntrack.py b/src/op_mode/clear_conntrack.py new file mode 100755 index 000000000..0e52b9086 --- /dev/null +++ b/src/op_mode/clear_conntrack.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import subprocess +import sys + +from vyos.util import ask_yes_no + +if not ask_yes_no("This will clear all currently tracked and expected connections. Continue?"): + sys.exit(1) +else: + subprocess.check_call(['/usr/sbin/conntrack -F'], shell=True, stderr=subprocess.DEVNULL) + subprocess.check_call(['/usr/sbin/conntrack -F expect'], shell=True, stderr=subprocess.DEVNULL) diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py index 2f6112fb7..e3644e063 100755 --- a/src/op_mode/powerctrl.py +++ b/src/op_mode/powerctrl.py @@ -22,20 +22,7 @@ import re from datetime import datetime, timedelta, time as type_time, date as type_date from subprocess import check_output, CalledProcessError, STDOUT - -def yn(msg, default=False): - default_msg = "[Y/n]" if default else "[y/N]" - while True: - sys.stdout.write("%s %s " % (msg,default_msg)) - c = input().lower() - if c == '': - return default - elif c in ("y", "ye","yes"): - return True - elif c in ("n", "no"): - return False - else: - sys.stdout.write("Please respond with yes/y or no/n\n") +from vyos.util import ask_yes_no def valid_time(s): @@ -80,7 +67,7 @@ def cancel_shutdown(): def execute_shutdown(time, reboot = True, ask=True): if not ask: action = "reboot" if reboot else "poweroff" - if not yn("Are you sure you want to %s this system?" % action): + if not ask_yes_no("Are you sure you want to %s this system?" % action): sys.exit(0) action = "-r" if reboot else "-P" -- cgit v1.2.3 From 84957a3418db23716b9e80f38733ed5e0bd4252e Mon Sep 17 00:00:00 2001 From: DmitriyEshenko Date: Fri, 23 Aug 2019 22:00:57 +0000 Subject: [dummy] T1609 migrate to vyos.interfaceconfig, adding check ip-cidr, adding vyos.interfaceconfig common ipv4/ipv6 functions --- interface-definitions/interfaces-dummy.xml | 3 ++ python/vyos/interfaceconfig.py | 56 ++++++++++++++++++++++++++++++ src/conf_mode/interface-dummy.py | 19 +++++----- 3 files changed, 69 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/interface-definitions/interfaces-dummy.xml b/interface-definitions/interfaces-dummy.xml index 4450b69b7..c9860fe3b 100644 --- a/interface-definitions/interfaces-dummy.xml +++ b/interface-definitions/interfaces-dummy.xml @@ -28,6 +28,9 @@ IPv6 address and prefix length + + + diff --git a/python/vyos/interfaceconfig.py b/python/vyos/interfaceconfig.py index 83e7c03c0..56e2d515c 100644 --- a/python/vyos/interfaceconfig.py +++ b/python/vyos/interfaceconfig.py @@ -21,6 +21,7 @@ import re import json import socket import subprocess +import ipaddress dhclient_conf_dir = r'/var/lib/dhcp/dhclient_' @@ -188,6 +189,29 @@ class Interface: self._debug(e) return None + def get_addr(self, ret_prefix=None): + """ + universal: reads all IPs assigned to an interface and returns it in a list, + or None if no IP address is assigned to the interface. Also may return + in prefix format if set ret_prefix + """ + ips = [] + try: + ret = subprocess.check_output(['ip -j addr show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() + j = json.loads(ret) + for i in j: + if len(i) != 0: + for addr in i['addr_info']: + if ret_prefix: + ips.append(addr['local'] + "/" + str(addr['prefixlen'])) + else: + ips.append(addr['local']) + return ips + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + def get_ipv4_addr(self): """ reads all IPs assigned to an interface and returns it in a list, @@ -227,6 +251,38 @@ class Interface: self._debug(e) return None + def add_addr(self, ipaddr=[]): + """ + universal: add ipv4/ipv6 addresses on the interface + """ + for ip in ipaddr: + proto = '-4' + if ipaddress.ip_address(ip.split(r'/')[0]).version == 6: + proto = '-6' + + try: + ret = subprocess.check_output(['ip ' + proto + ' address add ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + return True + + def del_addr(self, ipaddr=[]): + """ + universal: delete ipv4/ipv6 addresses on the interface + """ + for ip in ipaddr: + proto = '-4' + if ipaddress.ip_address(ip.split(r'/')[0]).version == 6: + proto = '-6' + try: + ret = subprocess.check_output(['ip ' + proto + ' address del ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + return True def add_ipv4_addr(self, ipaddr=[]): """ diff --git a/src/conf_mode/interface-dummy.py b/src/conf_mode/interface-dummy.py index 8c939ce95..c7c5ac99c 100755 --- a/src/conf_mode/interface-dummy.py +++ b/src/conf_mode/interface-dummy.py @@ -20,8 +20,6 @@ import os import sys import copy -import vyos.configinterface as VyIfconfig - from vyos.interfaceconfig import Interface from vyos.config import Config from vyos import ConfigError @@ -86,20 +84,23 @@ def generate(dummy): def apply(dummy): # Remove dummy interface if dummy['deleted']: - VyIfconfig.remove_interface(dummy['intf']) + Interface(dummy['intf']).remove_interface() else: # Interface will only be added if it yet does not exist - VyIfconfig.add_interface('dummy', dummy['intf']) + Interface(dummy['intf'], 'dummy') # update interface description used e.g. within SNMP - VyIfconfig.set_description(dummy['intf'], dummy['description']) + if dummy['description']: + Interface(dummy['intf']).ifalias = dummy['description'] # Configure interface address(es) - for addr in dummy['address_remove']: - VyIfconfig.remove_interface_address(dummy['intf'], addr) + if len(dummy['address_remove']) > 0: + Interface(dummy['intf']).del_addr(dummy['address_remove']) - for addr in dummy['address']: - VyIfconfig.add_interface_address(dummy['intf'], addr) + if len(dummy['address']) > 0: + # delete already existing addreses from list + addresess = diff(dummy['address'], Interface(dummy['intf']).get_addr(1)) + Interface(dummy['intf']).add_addr(addresess) if dummy['disable']: Interface(dummy['intf']).linkstate = 'down' -- cgit v1.2.3 From a235b4453b4b0dde6d8da1d8b01aac1c5cdf3491 Mon Sep 17 00:00:00 2001 From: Jernej Jakob Date: Sat, 24 Aug 2019 23:49:53 +0200 Subject: T1611: check if config node exists before getting value --- src/migration-scripts/interfaces/0-to-1 | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/migration-scripts/interfaces/0-to-1 b/src/migration-scripts/interfaces/0-to-1 index 38f2bd8f5..96e18b5d5 100755 --- a/src/migration-scripts/interfaces/0-to-1 +++ b/src/migration-scripts/interfaces/0-to-1 @@ -30,20 +30,22 @@ else: # for br in config.list_nodes(base): # STP: check if enabled - stp_val = config.return_value(base + [br, 'stp']) - # STP: delete node with old syntax - config.delete(base + [br, 'stp']) - # STP: set new node - if enabled - if stp_val == "true": - config.set(base + [br, 'stp'], value=None) + if config.exists(base + [br, 'stp']): + stp_val = config.return_value(base + [br, 'stp']) + # STP: delete node with old syntax + config.delete(base + [br, 'stp']) + # STP: set new node - if enabled + if stp_val == "true": + config.set(base + [br, 'stp'], value=None) # igmp-snooping: check if enabled - igmp_val = config.return_value(base + [br, 'igmp-snooping', 'querier']) - # igmp-snooping: delete node with old syntax - config.delete(base + [br, 'igmp-snooping', 'querier']) - # igmp-snooping: set new node - if enabled - if igmp_val == "enable": - config.set(base + [br, 'igmp', 'querier'], value=None) + if config.exists(base + [br, 'igmp-snooping', 'querier']): + igmp_val = config.return_value(base + [br, 'igmp-snooping', 'querier']) + # igmp-snooping: delete node with old syntax + config.delete(base + [br, 'igmp-snooping', 'querier']) + # igmp-snooping: set new node - if enabled + if igmp_val == "enable": + config.set(base + [br, 'igmp', 'querier'], value=None) # # move interface based bridge-group to actual bridge (de-nest) -- cgit v1.2.3 From c83d12cf79635a14f87696fa9817840cc2f6fa8d Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 26 Aug 2019 13:14:35 +0200 Subject: openvpn: T1548: add 'show openvpn' command vyos@vyos:~$ show openvpn site-to-site OpenVPN status on vtun1 Client CN Remote Host Local Host TX bytes RX bytes Connected Since --------- ----------- ---------- -------- -------- --------------- None (PSK) N/A 172.18.201.10:1195 3.3 KiB 3.3 KiB N/A vyos@vyos:~$ show openvpn server OpenVPN status on vtun10 Client CN Remote Host Local Host TX bytes RX bytes Connected Since --------- ----------- ---------- -------- -------- --------------- client1 172.18.202.10:58644 172.18.201.10:1194 63.6 KiB 63.4 KiB Mon Aug 26 11:47:56 2019 client3 172.18.204.10:52641 172.18.201.10:1194 63.1 KiB 62.7 KiB Mon Aug 26 11:47:58 2019 OpenVPN status on vtun11 Client CN Remote Host Local Host TX bytes RX bytes Connected Since --------- ----------- ---------- -------- -------- --------------- client2 172.18.203.10:39472 172.18.201.10:1200 61.2 KiB 61.5 KiB Mon Aug 26 11:50:30 2019 --- op-mode-definitions/openvpn.xml | 25 ++++++ src/op_mode/show_openvpn.py | 170 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100755 src/op_mode/show_openvpn.py (limited to 'src') diff --git a/op-mode-definitions/openvpn.xml b/op-mode-definitions/openvpn.xml index 2adbfba53..368cc9115 100644 --- a/op-mode-definitions/openvpn.xml +++ b/op-mode-definitions/openvpn.xml @@ -110,6 +110,31 @@ + + + Show OpenVPN information + + + + + Show tunnel status for OpenVPN client interfaces + + sudo ${vyos_op_scripts_dir}/show_openvpn.py --mode=client + + + + Show tunnel status for OpenVPN server interfaces + + sudo ${vyos_op_scripts_dir}/show_openvpn.py --mode=server + + + + Show tunnel status for OpenVPN site-to-site interfaces + + sudo ${vyos_op_scripts_dir}/show_openvpn.py --mode=site-to-site + + + diff --git a/src/op_mode/show_openvpn.py b/src/op_mode/show_openvpn.py new file mode 100755 index 000000000..ee3729fc3 --- /dev/null +++ b/src/op_mode/show_openvpn.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +import jinja2 +import argparse + +from vyos.config import Config + +outp_tmpl = """ +{% if clients %} +OpenVPN status on {{ intf }} + +Client CN Remote Host Local Host TX bytes RX bytes Connected Since +--------- ----------- ---------- -------- -------- --------------- +{%- for c in clients %} +{{ "%-15s"|format(c.name) }} {{ "%-21s"|format(c.remote) }} {{ "%-15s"|format(local) }} {{ "%-9s"|format(c.tx_bytes) }} {{ "%-9s"|format(c.tx_bytes) }} {{ c.online_since }} +{%- endfor %} +{% endif %} +""" + +def bytes2HR(size): + # we need to operate in integers + size = int(size) + + suff = ['B', 'KB', 'MB', 'GB', 'TB'] + suffIdx = 0 + + while size > 1024: + # incr. suffix index + suffIdx += 1 + # divide + size = size/1024.0 + + output="{0:.1f} {1}".format(size, suff[suffIdx]) + return output + +def get_status(mode, interface): + status_file = '/opt/vyatta/etc/openvpn/status/{}.status'.format(interface) + # this is an empirical value - I assume we have no more then 999999 + # current OpenVPN connections + routing_table_line = 999999 + + data = { + 'mode': mode, + 'intf': interface, + 'local': '', + 'date': '', + 'clients': [], + } + + with open(status_file, 'r') as f: + lines = f.readlines() + for line_no, line in enumerate(lines): + # remove trailing newline character first + line = line.rstrip('\n') + + # check first line header + if line_no == 0: + if mode == 'server': + if not line == 'OpenVPN CLIENT LIST': + raise NameError('Expected "OpenVPN CLIENT LIST"') + else: + if not line == 'OpenVPN STATISTICS': + raise NameError('Expected "OpenVPN STATISTICS"') + + continue + + # second line informs us when the status file has been last updated + if line_no == 1: + data['date'] = line.lstrip('Updated,').rstrip('\n') + continue + + if mode == 'server': + # followed by line3 giving output information and the actual output data + # + # Common Name,Real Address,Bytes Received,Bytes Sent,Connected Since + # client1,172.18.202.10:55904,2880587,2882653,Fri Aug 23 16:25:48 2019 + # client3,172.18.204.10:41328,2850832,2869729,Fri Aug 23 16:25:43 2019 + # client2,172.18.203.10:48987,2856153,2871022,Fri Aug 23 16:25:45 2019 + if (line_no >= 3) and (line_no < routing_table_line): + # indicator that there are no more clients and we will continue with the + # routing table + if line == 'ROUTING TABLE': + routing_table_line = line_no + continue + + client = { + 'name': line.split(',')[0], + 'remote': line.split(',')[1], + 'rx_bytes': bytes2HR(line.split(',')[2]), + 'tx_bytes': bytes2HR(line.split(',')[3]), + 'online_since': line.split(',')[4] + } + + data['clients'].append(client) + continue + else: + if line_no == 2: + client = { + 'name': 'N/A', + 'rx_bytes': bytes2HR(line.split(',')[1]), + 'tx_bytes': '', + 'online_since': 'N/A' + } + continue + + if line_no == 3: + client['tx_bytes'] = bytes2HR(line.split(',')[1]) + data['clients'].append(client) + break + + return data + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-m', '--mode', help='OpenVPN operation mode (server, client, site-2-site)', required=True) + + args = parser.parse_args() + + # Do nothing if service is not configured + config = Config() + if len(config.list_effective_nodes('interfaces openvpn')) == 0: + print("No OpenVPN interfaces configured") + sys.exit(0) + + # search all OpenVPN interfaces and add those with a matching mode to our + # interfaces list + interfaces = [] + for intf in config.list_effective_nodes('interfaces openvpn'): + # get interface type (server, client, site-to-site) + mode = config.return_effective_value('interfaces openvpn {} mode'.format(intf)) + if args.mode == mode: + interfaces.append(intf) + + for intf in interfaces: + data = get_status(args.mode, intf) + local_host = config.return_effective_value('interfaces openvpn {} local-host'.format(intf)) + local_port = config.return_effective_value('interfaces openvpn {} local-port'.format(intf)) + data['local'] = local_host + ':' + local_port + + if args.mode in ['client', 'site-to-site']: + for client in data['clients']: + if config.exists_effective('interfaces openvpn {} shared-secret-key-file'.format(intf)): + client['name'] = "None (PSK)" + + remote_host = config.return_effective_values('interfaces openvpn {} remote-host'.format(intf)) + remote_port = config.return_effective_value('interfaces openvpn {} remote-port'.format(intf)) + if len(remote_host) >= 1: + client['remote'] = str(remote_host[0]) + ':' + remote_port + else: + client['remote'] = 'N/A' + + + tmpl = jinja2.Template(outp_tmpl) + print(tmpl.render(data)) + -- cgit v1.2.3 From 37e4d24f3679c2f24513cf5dcfbeb761d5bf701d Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 26 Aug 2019 13:35:36 +0200 Subject: openvpn: T1548: add missing if statement in 'show openvpn' command --- src/op_mode/show_openvpn.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/op_mode/show_openvpn.py b/src/op_mode/show_openvpn.py index ee3729fc3..23a8156ec 100755 --- a/src/op_mode/show_openvpn.py +++ b/src/op_mode/show_openvpn.py @@ -24,10 +24,10 @@ outp_tmpl = """ {% if clients %} OpenVPN status on {{ intf }} -Client CN Remote Host Local Host TX bytes RX bytes Connected Since ---------- ----------- ---------- -------- -------- --------------- +Client CN Remote Host Local Host TX bytes RX bytes Connected Since +--------- ----------- ---------- -------- -------- --------------- {%- for c in clients %} -{{ "%-15s"|format(c.name) }} {{ "%-21s"|format(c.remote) }} {{ "%-15s"|format(local) }} {{ "%-9s"|format(c.tx_bytes) }} {{ "%-9s"|format(c.tx_bytes) }} {{ c.online_since }} +{{ "%-15s"|format(c.name) }} {{ "%-21s"|format(c.remote) }} {{ "%-21s"|format(local) }} {{ "%-9s"|format(c.tx_bytes) }} {{ "%-9s"|format(c.tx_bytes) }} {{ c.online_since }} {%- endfor %} {% endif %} """ @@ -57,7 +57,7 @@ def get_status(mode, interface): data = { 'mode': mode, 'intf': interface, - 'local': '', + 'local': 'N/A', 'date': '', 'clients': [], } @@ -112,6 +112,7 @@ def get_status(mode, interface): if line_no == 2: client = { 'name': 'N/A', + 'remote': 'N/A', 'rx_bytes': bytes2HR(line.split(',')[1]), 'tx_bytes': '', 'online_since': 'N/A' @@ -150,7 +151,8 @@ if __name__ == '__main__': data = get_status(args.mode, intf) local_host = config.return_effective_value('interfaces openvpn {} local-host'.format(intf)) local_port = config.return_effective_value('interfaces openvpn {} local-port'.format(intf)) - data['local'] = local_host + ':' + local_port + if local_host and local_port: + data['local'] = local_host + ':' + local_port if args.mode in ['client', 'site-to-site']: for client in data['clients']: @@ -161,9 +163,6 @@ if __name__ == '__main__': remote_port = config.return_effective_value('interfaces openvpn {} remote-port'.format(intf)) if len(remote_host) >= 1: client['remote'] = str(remote_host[0]) + ':' + remote_port - else: - client['remote'] = 'N/A' - tmpl = jinja2.Template(outp_tmpl) print(tmpl.render(data)) -- cgit v1.2.3 From d5e9512b8461f55d276182b4a75267378aa11f50 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 26 Aug 2019 16:19:31 +0200 Subject: bridge: T1556: reword exception error when beeing member of multiple bridges --- src/conf_mode/interface-bridge.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index 65b5c4066..fc1243867 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -187,7 +187,8 @@ def verify(bridge): for intf in bridge['member']: tmp = conf.list_nodes('interfaces bridge {} member interface'.format(br)) if intf['name'] in tmp: - raise ConfigError('{} can be assigned to any one bridge only'.format(intf['name'])) + raise ConfigError('Interface "{}" belongs to bridge "{}" and can not be enslaved.'.format(intf['name'], bridge['intf'])) + return None -- cgit v1.2.3 From 4a8ab14dc3cbe4245b95250c51ee427eb6241372 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 26 Aug 2019 16:20:03 +0200 Subject: bridge: T1608: deny adding non existing interfaces to bridge config --- src/conf_mode/interface-bridge.py | 5 ++++ src/helpers/vyos-bridge-sync.py | 53 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100755 src/helpers/vyos-bridge-sync.py (limited to 'src') diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index fc1243867..c5c5bd4ac 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -23,6 +23,7 @@ import subprocess import vyos.configinterface as VyIfconfig +from netifaces import interfaces from vyos.config import Config from vyos import ConfigError @@ -189,6 +190,10 @@ def verify(bridge): if intf['name'] in tmp: raise ConfigError('Interface "{}" belongs to bridge "{}" and can not be enslaved.'.format(intf['name'], bridge['intf'])) + # the interface must exist prior adding it to a bridge + for intf in bridge['member']: + if intf['name'] not in interfaces(): + raise ConfigError('Can not add non existing interface "{}" to bridge "{}"'.format(intf['name'], bridge['intf'])) return None diff --git a/src/helpers/vyos-bridge-sync.py b/src/helpers/vyos-bridge-sync.py new file mode 100755 index 000000000..495eb5d40 --- /dev/null +++ b/src/helpers/vyos-bridge-sync.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# Script is used to synchronize configured bridge interfaces. +# one can add a non existing interface to a bridge group (e.g. VLAN) +# but the vlan interface itself does yet not exist. It should be added +# to the bridge automatically once it's available + +import argparse +import subprocess + +from sys import exit +from time import sleep +from vyos.config import Config + +def subprocess_cmd(command): + process = subprocess.Popen(command,stdout=subprocess.PIPE, shell=True) + proc_stdout = process.communicate()[0].strip() + pass + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-i', '--interface', action='store', help='Interface name which should be added to bridge it is configured for', required=True) + args, unknownargs = parser.parse_known_args() + + conf = Config() + if not conf.list_nodes('interfaces bridge'): + # no bridge interfaces exist .. bail out early + exit(0) + else: + for bridge in conf.list_nodes('interfaces bridge'): + for member_if in conf.list_nodes('interfaces bridge {} member interface'.format(bridge)): + if args.interface == member_if: + cmd = 'brctl addif "{}" "{}"'.format(bridge, args.interface) + # let interfaces etc. settle - especially required for OpenVPN bridged interfaces + sleep(4) + subprocess_cmd(cmd) + + exit(0) -- cgit v1.2.3 From b812bae9e317f7bcc91371316af28d07345682ba Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Mon, 26 Aug 2019 19:58:04 +0200 Subject: T1598: add a vyos-hostsd operation for retrieving name servers by tag. --- python/vyos/hostsd_client.py | 7 +++++++ src/services/vyos-hostsd | 18 +++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/python/vyos/hostsd_client.py b/python/vyos/hostsd_client.py index e2f05071b..f009aba98 100644 --- a/python/vyos/hostsd_client.py +++ b/python/vyos/hostsd_client.py @@ -30,6 +30,8 @@ class Client(object): reply = json.loads(reply_msg) if 'error' in reply: raise VyOSHostsdError(reply['error']) + else: + return reply["data"] except zmq.error.Again: raise VyOSHostsdError("Could not connect to vyos-hostsd") @@ -60,3 +62,8 @@ class Client(object): def delete_name_servers(self, tag): msg = {'type': 'name_servers', 'op': 'delete', 'tag': tag} self._communicate(msg) + + def get_name_servers(self, tag): + msg = {'type': 'name_servers', 'op': 'get', 'tag': tag} + return self._communicate(msg) + diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd index 1bcb2e1f5..8f70eb4e9 100755 --- a/src/services/vyos-hostsd +++ b/src/services/vyos-hostsd @@ -171,6 +171,14 @@ def set_host_name(state, data): if data['search_domains']: state['search_domains'] = data['search_domains'] +def get_name_servers(state, tag): + ns = [] + data = state['name_servers'] + for n in data: + if data[n]['tag'] == tag: + ns.append(n) + return ns + def get_option(msg, key): if key in msg: return msg[key] @@ -209,6 +217,13 @@ def handle_message(msg_json): set_host_name(STATE, data) else: raise ValueError("Unknown message type {0}".format(_type)) + elif op == 'get': + tag = get_option(msg, 'tag') + if _type == 'name_servers': + result = get_name_servers(STATE, tag) + else: + raise ValueError("Unimplemented") + return result else: raise ValueError("Unknown operation {0}".format(op)) @@ -254,7 +269,8 @@ if __name__ == '__main__': resp = {} try: - handle_message(message) + result = handle_message(message) + resp['data'] = result except ValueError as e: resp['error'] = str(e) except: -- cgit v1.2.3 From fdae741be5ffaa3719ce889d0342c3091ad3c92c Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Mon, 26 Aug 2019 19:58:39 +0200 Subject: T1598: make dns_forwarding.py retrieve name servers from vyos-hostsd. --- src/conf_mode/dns_forwarding.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) (limited to 'src') diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index 9e81c7294..38f3cb4de 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -24,6 +24,7 @@ import jinja2 import netifaces import vyos.util +import vyos.hostsd_client from vyos.config import Config from vyos import ConfigError @@ -94,19 +95,6 @@ default_config_data = { } -# borrowed from: https://github.com/donjajo/py-world/blob/master/resolvconfReader.py, THX! -def get_resolvers(file): - try: - with open(file, 'r') as resolvconf: - lines = [line.split('#', 1)[0].rstrip() - for line in resolvconf.readlines()] - resolvers = [line.split()[1] - for line in lines if 'nameserver' in line] - return resolvers - except IOError: - return [] - - def get_config(arguments): dns = default_config_data conf = Config() @@ -171,11 +159,16 @@ def get_config(arguments): if conf.exists('dhcp'): interfaces = [] interfaces = conf.return_values('dhcp') + hc = vyos.hostsd_client.Client() + for interface in interfaces: - dhcp_resolvers = get_resolvers( - "/etc/resolv.conf.dhclient-new-{0}".format(interface)) + dhcp_resolvers = hc.get_name_servers("dhcp-{0}".format(interface)) + dhcpv6_resolvers = hc.get_name_servers("dhcpv6-{0}".format(interface)) + if dhcp_resolvers: dns['name_servers'] = dns['name_servers'] + dhcp_resolvers + if dhcpv6_resolvers: + dns['name_servers'] = dns['name_servers'] + dhcpv6_resolvers return dns -- cgit v1.2.3 From 6e36bafad6d8300b0bd90261f2a57cf65716ac7f Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 26 Aug 2019 22:29:16 +0200 Subject: bridge: T1556: migrate interface configuration to pyroute2 Tested with: set interfaces bridge br0 address '192.0.2.1/24' set interfaces bridge br0 aging '500' set interfaces bridge br0 disable-link-detect set interfaces bridge br0 forwarding-delay '11' set interfaces bridge br0 hello-time '5' set interfaces bridge br0 igmp querier set interfaces bridge br0 max-age '11' set interfaces bridge br0 member interface eth1 cost '1000' set interfaces bridge br0 member interface eth1 priority '4' set interfaces bridge br0 member interface eth2 cost '1001' set interfaces bridge br0 member interface eth2 priority '56' --- debian/control | 1 + src/conf_mode/interface-bridge.py | 238 +++++++++++++++++--------------------- 2 files changed, 109 insertions(+), 130 deletions(-) (limited to 'src') diff --git a/debian/control b/debian/control index 12eb7c309..7b75ca111 100644 --- a/debian/control +++ b/debian/control @@ -29,6 +29,7 @@ Depends: python3, python3-vici (>= 5.7.2), python3-bottle, python3-zmq, + python3-pyroute2, ipaddrcheck, tcpdump, tshark, diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index c5c5bd4ac..d5ef85940 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -16,13 +16,10 @@ # # -import os -import sys -import copy -import subprocess - -import vyos.configinterface as VyIfconfig - +from os import environ +from copy import deepcopy +from sys import exit +from pyroute2 import IPDB from netifaces import interfaces from vyos.config import Config from vyos import ConfigError @@ -30,44 +27,42 @@ from vyos import ConfigError default_config_data = { 'address': [], 'address_remove': [], - 'aging': '300', - 'arp_cache_timeout_ms': '30000', + 'aging': 300, + 'arp_cache_timeout_ms': 30000, 'description': '', 'deleted': False, - 'dhcp_client_id': '', - 'dhcp_hostname': '', - 'dhcpv6_parameters_only': False, - 'dhcpv6_temporary': False, 'disable': False, - 'disable_link_detect': False, - 'forwarding_delay': '15', - 'hello_time': '2', + 'disable_link_detect': 1, + 'forwarding_delay': 14, + 'hello_time': 2, 'igmp_querier': 0, 'intf': '', 'mac' : '', - 'max_age': '20', + 'max_age': 20, 'member': [], 'member_remove': [], - 'priority': '32768', - 'stp': 'off' + 'priority': 32768, + 'stp': 0 } -def subprocess_cmd(command): - process = subprocess.Popen(command,stdout=subprocess.PIPE, shell=True) - proc_stdout = process.communicate()[0].strip() - pass +def freeze(d): + if isinstance(d, dict): + return frozenset((key, freeze(value)) for key, value in d.items()) + elif isinstance(d, list): + return tuple(freeze(value) for value in d) + return d def diff(first, second): second = set(second) return [item for item in first if item not in second] def get_config(): - bridge = copy.deepcopy(default_config_data) + bridge = deepcopy(default_config_data) conf = Config() # determine tagNode instance try: - bridge['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + bridge['intf'] = environ['VYOS_TAGNODE_VALUE'] except KeyError as E: print("Interface not specified") @@ -85,27 +80,13 @@ def get_config(): # retrieve aging - how long addresses are retained if conf.exists('aging'): - bridge['aging'] = conf.return_value('aging') + bridge['aging'] = int(conf.return_value('aging')) # retrieve interface description if conf.exists('description'): bridge['description'] = conf.return_value('description') - - # DHCP client identifier - if conf.exists('dhcp-options client-id'): - bridge['dhcp_client_id'] = conf.return_value('dhcp-options client-id') - - # DHCP client hostname - if conf.exists('dhcp-options host-name'): - bridge['dhcp_hostname'] = conf.return_value('dhcp-options host-name') - - # DHCPv6 acquire only config parameters, no address - if conf.exists('dhcpv6-options parameters-only'): - bridge['dhcpv6_parameters_only'] = True - - # DHCPv6 IPv6 "temporary" address - if conf.exists('dhcpv6-options temporary'): - bridge['dhcpv6_temporary'] = True + else: + bridge['description'] = bridge['intf'] # Disable this bridge interface if conf.exists('disable'): @@ -113,15 +94,15 @@ def get_config(): # Ignore link state changes if conf.exists('disable-link-detect'): - bridge['disable_link_detect'] = True + bridge['disable_link_detect'] = 2 # Forwarding delay if conf.exists('forwarding-delay'): - bridge['forwarding_delay'] = conf.return_value('forwarding-delay') + bridge['forwarding_delay'] = int(conf.return_value('forwarding-delay')) # Hello packet advertisment interval if conf.exists('hello-time'): - bridge['hello_time'] = conf.return_value('hello-time') + bridge['hello_time'] = int(conf.return_value('hello-time')) # Enable Internet Group Management Protocol (IGMP) querier if conf.exists('igmp querier'): @@ -129,8 +110,7 @@ def get_config(): # ARP cache entry timeout in seconds if conf.exists('ip arp-cache-timeout'): - tmp = 1000 * int(conf.return_value('ip arp-cache-timeout')) - bridge['arp_cache_timeout_ms'] = str(tmp) + bridge['arp_cache_timeout_ms'] = int(conf.return_value('ip arp-cache-timeout')) * 1000 # Media Access Control (MAC) address if conf.exists('mac'): @@ -138,21 +118,24 @@ def get_config(): # Interval at which neighbor bridges are removed if conf.exists('max-age'): - bridge['max_age'] = conf.return_value('max-age') + bridge['max_age'] = int(conf.return_value('max-age')) # Determine bridge member interface (currently configured) for intf in conf.list_nodes('member interface'): + # cost and priority initialized with linux defaults + # by reading /sys/devices/virtual/net/br0/brif/eth2/{path_cost,priority} + # after adding interface to bridge after reboot iface = { 'name': intf, - 'cost': '', - 'priority': '' + 'cost': 100, + 'priority': 32 } if conf.exists('member interface {} cost'.format(intf)): - iface['cost'] = conf.return_value('member interface {} cost'.format(intf)) + iface['cost'] = int(conf.return_value('member interface {} cost'.format(intf))) if conf.exists('member interface {} priority'.format(intf)): - iface['priority'] = conf.return_value('member interface {} priority'.format(intf)) + iface['priority'] = int(conf.return_value('member interface {} priority'.format(intf))) bridge['member'].append(iface) @@ -170,11 +153,11 @@ def get_config(): # Priority for this bridge if conf.exists('priority'): - bridge['priority'] = conf.return_value('priority') + bridge['priority'] = int(conf.return_value('priority')) # Enable spanning tree protocol if conf.exists('stp'): - bridge['stp'] = 'on' + bridge['stp'] = 1 return bridge @@ -201,94 +184,89 @@ def generate(bridge): return None def apply(bridge): - cmd = '' - if bridge['deleted']: - # bridges need to be shutdown first - cmd += 'ip link set dev "{}" down'.format(bridge['intf']) - cmd += ' && ' - # delete bridge - cmd += 'brctl delbr "{}"'.format(bridge['intf']) - subprocess_cmd(cmd) + ipdb = IPDB(mode='explicit') + brif = bridge['intf'] + if bridge['deleted']: + try: + # delete bridge interface + with ipdb.interfaces[ brif ] as br: + br.remove() + except: + pass else: - # create bridge if it does not exist - if not os.path.exists("/sys/class/net/" + bridge['intf']): - # create bridge interface - cmd += 'brctl addbr "{}"'.format(bridge['intf']) - cmd += ' && ' - # activate "UP" the interface - cmd += 'ip link set dev "{}" up'.format(bridge['intf']) - cmd += ' && ' - - # set ageing time - cmd += 'brctl setageing "{}" "{}"'.format(bridge['intf'], bridge['aging']) - cmd += ' && ' - - # set bridge forward delay - cmd += 'brctl setfd "{}" "{}"'.format(bridge['intf'], bridge['forwarding_delay']) - cmd += ' && ' - - # set hello time - cmd += 'brctl sethello "{}" "{}"'.format(bridge['intf'], bridge['hello_time']) - cmd += ' && ' - - # set max message age - cmd += 'brctl setmaxage "{}" "{}"'.format(bridge['intf'], bridge['max_age']) - cmd += ' && ' - + try: + # create bridge interface if it not already exists + ipdb.create(kind='bridge', ifname=brif).commit() + except: + pass + + # get handle in bridge interface + br = ipdb.interfaces[brif] + # begin() a transaction prior to make any change + br.begin() + # enable interface + br.up() + # set ageing time - - value is in centiseconds YES! centiseconds! + br.br_ageing_time = bridge['aging'] * 100 + # set bridge forward delay - value is in centiseconds YES! centiseconds! + br.br_forward_delay = bridge['forwarding_delay'] * 100 + # set hello time - value is in centiseconds YES! centiseconds! + br.br_hello_time = bridge['hello_time'] * 100 + # set max message age - value is in centiseconds YES! centiseconds! + br.br_max_age = bridge['max_age'] * 100 # set bridge priority - cmd += 'brctl setbridgeprio "{}" "{}"'.format(bridge['intf'], bridge['priority']) - cmd += ' && ' - + br.br_priority = bridge['priority'] # turn stp on/off - cmd += 'brctl stp "{}" "{}"'.format(bridge['intf'], bridge['stp']) - - for intf in bridge['member_remove']: - # remove interface from bridge - cmd += ' && ' - cmd += 'brctl delif "{}" "{}"'.format(bridge['intf'], intf) - - for intf in bridge['member']: - # add interface to bridge - # but only if it is not yet member of this bridge - if not os.path.exists('/sys/devices/virtual/net/' + bridge['intf'] + '/brif/' + intf['name']): - cmd += ' && ' - cmd += 'brctl addif "{}" "{}"'.format(bridge['intf'], intf['name']) - - # set bridge port cost - if intf['cost']: - cmd += ' && ' - cmd += 'brctl setpathcost "{}" "{}" "{}"'.format(bridge['intf'], intf['name'], intf['cost']) - - # set bridge port priority - if intf['priority']: - cmd += ' && ' - cmd += 'brctl setportprio "{}" "{}" "{}"'.format(bridge['intf'], intf['name'], intf['priority']) - - subprocess_cmd(cmd) + br.br_stp_state = bridge['stp'] + # enable or disable IGMP querier + br.br_mcast_querier = bridge['igmp_querier'] + # update interface description used e.g. within SNMP + br.ifalias = bridge['description'] # Change interface MAC address if bridge['mac']: - VyIfconfig.set_mac_address(bridge['intf'], bridge['mac']) + br.set_address = bridge['mac'] - # update interface description used e.g. within SNMP - VyIfconfig.set_description(bridge['intf'], bridge['description']) - - # Ignore link state changes? - VyIfconfig.set_link_detect(bridge['intf'], bridge['disable_link_detect']) - - # enable or disable IGMP querier - VyIfconfig.set_multicast_querier(bridge['intf'], bridge['igmp_querier']) + # remove interface from bridge + for intf in bridge['member_remove']: + br.del_port( intf['name'] ) - # ARP cache entry timeout in seconds - VyIfconfig.set_arp_cache_timeout(bridge['intf'], bridge['arp_cache_timeout_ms']) + # configure bridge member interfaces + for member in bridge['member']: + # add interface + br.add_port(member['name']) # Configure interface address(es) for addr in bridge['address_remove']: - VyIfconfig.remove_interface_address(bridge['intf'], addr) - + br.del_ip(addr) for addr in bridge['address']: - VyIfconfig.add_interface_address(bridge['intf'], addr) + br.add_ip(addr) + + # up/down interface + if bridge['disable']: + br.down() + + # commit change son bridge interface + br.commit() + + # configure additional bridge member options + for member in bridge['member']: + # configure ARP cache timeout in milliseconds + with open('/proc/sys/net/ipv4/neigh/' + member['name'] + '/base_reachable_time_ms', 'w') as f: + f.write(str(bridge['arp_cache_timeout_ms'])) + # ignore link state changes + with open('/proc/sys/net/ipv4/conf/' + member['name'] + '/link_filter', 'w') as f: + f.write(str(bridge['disable_link_detect'])) + + # adjust member port stp attributes + member_if = ipdb.interfaces[ member['name'] ] + member_if.begin() + # set bridge port cost + member_if.brport_cost = member['cost'] + # set bridge port priority + member_if.brport_priority = member['priority'] + member_if.commit() return None @@ -300,4 +278,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) -- cgit v1.2.3 From 3dbb258f5f02076aa96bb9cd840464bcf08725ab Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 26 Aug 2019 22:39:37 +0200 Subject: bridge: T1556: fix comment --- src/conf_mode/interface-bridge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index d5ef85940..85ea68e26 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -247,7 +247,7 @@ def apply(bridge): if bridge['disable']: br.down() - # commit change son bridge interface + # commit changes on bridge interface br.commit() # configure additional bridge member options -- cgit v1.2.3 From 0e1f53c5fb810d54a3c64e09d86b955ddbcbce87 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 26 Aug 2019 22:41:39 +0200 Subject: loopback: T1601: migrate to pyroute2 --- src/conf_mode/interface-loopback.py | 42 ++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interface-loopback.py b/src/conf_mode/interface-loopback.py index 445a9af64..5c1419b11 100755 --- a/src/conf_mode/interface-loopback.py +++ b/src/conf_mode/interface-loopback.py @@ -14,14 +14,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -# - -import os -import sys -import copy - -import vyos.configinterface as VyIfconfig +from os import environ +from sys import exit +from copy import deepcopy +from pyroute2 import IPDB from vyos.config import Config from vyos import ConfigError @@ -37,12 +34,12 @@ def diff(first, second): return [item for item in first if item not in second] def get_config(): - loopback = copy.deepcopy(default_config_data) + loopback = deepcopy(default_config_data) conf = Config() # determine tagNode instance try: - loopback['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + loopback['intf'] = environ['VYOS_TAGNODE_VALUE'] except KeyError as E: print("Interface not specified") @@ -60,6 +57,8 @@ def get_config(): # retrieve interface description if conf.exists('description'): loopback['description'] = conf.return_value('description') + else: + loopback['description'] = loopback['intf'] # Determine interface addresses (currently effective) - to determine which # address is no longer valid and needs to be removed from the interface @@ -76,19 +75,28 @@ def generate(loopback): return None def apply(loopback): - # Remove loopback interface + ipdb = IPDB(mode='explicit') + lo_if = loopback['intf'] + + # the loopback device always exists + lo = ipdb.interfaces[lo_if] + # begin() a transaction prior to make any change + lo.begin() + if not loopback['deleted']: # update interface description used e.g. within SNMP - VyIfconfig.set_description(loopback['intf'], loopback['description']) - - # Configure interface address(es) + # update interface description used e.g. within SNMP + lo.ifalias = loopback['description'] + # configure interface address(es) for addr in loopback['address']: - VyIfconfig.add_interface_address(loopback['intf'], addr) + lo.add_ip(addr) - # Remove interface address(es) + # remove interface address(es) for addr in loopback['address_remove']: - VyIfconfig.remove_interface_address(loopback['intf'], addr) + lo.del_ip(addr) + # commit changes on loopback interface + lo.commit() return None if __name__ == '__main__': @@ -99,4 +107,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) -- cgit v1.2.3 From 93184326fc3768216b734a5fcc60e193b5e27fad Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 27 Aug 2019 11:10:32 +0200 Subject: dummy: T1580: migrate implementation to pyroute2 --- src/conf_mode/interface-dummy.py | 64 +++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 24 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interface-dummy.py b/src/conf_mode/interface-dummy.py index c7c5ac99c..d8a36a5b2 100755 --- a/src/conf_mode/interface-dummy.py +++ b/src/conf_mode/interface-dummy.py @@ -16,11 +16,10 @@ # # -import os -import sys -import copy - -from vyos.interfaceconfig import Interface +from os import environ +from copy import deepcopy +from sys import exit +from pyroute2 import IPDB from vyos.config import Config from vyos import ConfigError @@ -38,12 +37,12 @@ def diff(first, second): return [item for item in first if item not in second] def get_config(): - dummy = copy.deepcopy(default_config_data) + dummy = deepcopy(default_config_data) conf = Config() # determine tagNode instance try: - dummy['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + dummy['intf'] = environ['VYOS_TAGNODE_VALUE'] except KeyError as E: print("Interface not specified") @@ -62,6 +61,8 @@ def get_config(): # retrieve interface description if conf.exists('description'): dummy['description'] = conf.return_value('description') + else: + dummy['description'] = dummy['intf'] # Disable this interface if conf.exists('disable'): @@ -82,30 +83,45 @@ def generate(dummy): return None def apply(dummy): + ipdb = IPDB(mode='explicit') + dummyif = dummy['intf'] + # Remove dummy interface if dummy['deleted']: - Interface(dummy['intf']).remove_interface() + try: + # delete dummy interface + with ipdb.interface[ dummyif ] as du: + du.remove() + except: + pass else: - # Interface will only be added if it yet does not exist - Interface(dummy['intf'], 'dummy') - + try: + # create dummy interface if it's non existing + ipdb.create(kind='dummy', ifname=dummyif).commit() + except: + pass + + # retrieve handle to dummy interface + du = ipdb.interfaces[dummyif] + # begin a transaction prior to make any change + du.begin() + # enable interface + du.up() # update interface description used e.g. within SNMP - if dummy['description']: - Interface(dummy['intf']).ifalias = dummy['description'] + du.ifalias = dummy['description'] # Configure interface address(es) - if len(dummy['address_remove']) > 0: - Interface(dummy['intf']).del_addr(dummy['address_remove']) - - if len(dummy['address']) > 0: - # delete already existing addreses from list - addresess = diff(dummy['address'], Interface(dummy['intf']).get_addr(1)) - Interface(dummy['intf']).add_addr(addresess) + for addr in dummy['address_remove']: + du.del_ip(addr) + for addr in dummy['address']: + du.add_ip(addr) + # disable interface on demand if dummy['disable']: - Interface(dummy['intf']).linkstate = 'down' - else: - Interface(dummy['intf']).linkstate = 'up' + du.down() + + # commit changes on bridge interface + du.commit() return None @@ -117,4 +133,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) -- cgit v1.2.3 From cd9bb2963e9e7c30d3bbebce9d4ef170107533a9 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 27 Aug 2019 11:55:16 +0200 Subject: list-interfaces: T1614: support listing interfaces which can be bonded --- src/completion/list_interfaces.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/completion/list_interfaces.py b/src/completion/list_interfaces.py index 66432af19..5e444ef78 100755 --- a/src/completion/list_interfaces.py +++ b/src/completion/list_interfaces.py @@ -2,15 +2,14 @@ import sys import argparse - import vyos.interfaces - parser = argparse.ArgumentParser() group = parser.add_mutually_exclusive_group() group.add_argument("-t", "--type", type=str, help="List interfaces of specific type") group.add_argument("-b", "--broadcast", action="store_true", help="List all broadcast interfaces") group.add_argument("-br", "--bridgeable", action="store_true", help="List all bridgeable interfaces") +group.add_argument("-bo", "--bondable", action="store_true", help="List all bondable interfaces") args = parser.parse_args() @@ -21,11 +20,13 @@ if args.type: except ValueError as e: print(e, file=sys.stderr) print("") + elif args.broadcast: eth = vyos.interfaces.list_interfaces_of_type("ethernet") bridge = vyos.interfaces.list_interfaces_of_type("bridge") bond = vyos.interfaces.list_interfaces_of_type("bonding") interfaces = eth + bridge + bond + elif args.bridgeable: eth = vyos.interfaces.list_interfaces_of_type("ethernet") bond = vyos.interfaces.list_interfaces_of_type("bonding") @@ -34,6 +35,15 @@ elif args.bridgeable: vxlan = vyos.interfaces.list_interfaces_of_type("vxlan") wireless = vyos.interfaces.list_interfaces_of_type("wireless") interfaces = eth + bond + l2tpv3 + openvpn + vxlan + wireless + +elif args.bondable: + eth = vyos.interfaces.list_interfaces_of_type("ethernet") + # we need to filter out VLAN interfaces identified by a dot (.) in their name + for intf in eth: + if '.' in intf: + eth.remove(intf) + interfaces = eth + else: interfaces = vyos.interfaces.list_interfaces() -- cgit v1.2.3 From 343a48be36b1573286db8f944aa933911b09530b Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 27 Aug 2019 12:24:09 +0200 Subject: bridge: T1556: remove unused function freeze() --- src/conf_mode/interface-bridge.py | 7 ------- 1 file changed, 7 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index 85ea68e26..c1527e4a7 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -45,13 +45,6 @@ default_config_data = { 'stp': 0 } -def freeze(d): - if isinstance(d, dict): - return frozenset((key, freeze(value)) for key, value in d.items()) - elif isinstance(d, list): - return tuple(freeze(value) for value in d) - return d - def diff(first, second): second = set(second) return [item for item in first if item not in second] -- cgit v1.2.3 From 1ace4a35237889bceff7309df0c687bf32ab89a9 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Tue, 27 Aug 2019 08:13:02 -0500 Subject: [service https] T1443: Correct the use of listen/server_name directives --- interface-definitions/https.xml | 18 ++++++++++-- python/vyos/defaults.py | 2 +- src/conf_mode/https.py | 61 +++++++++++++++++++++++++++++++++++++---- 3 files changed, 71 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/interface-definitions/https.xml b/interface-definitions/https.xml index 13d5c43ea..7a87133f3 100644 --- a/interface-definitions/https.xml +++ b/interface-definitions/https.xml @@ -9,7 +9,7 @@ 1001 - + Addresses to listen for HTTPS requests @@ -20,13 +20,25 @@ ipv6 HTTPS IPv6 address - + + '*' + any + + ^\\*$ - + + + + Server names: exact, wildcard, regex, or '_' (any) + + + + + TLS certificates diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index 3e4c02562..85d27d60d 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -29,7 +29,7 @@ cfg_vintage = 'vyatta' commit_lock = '/opt/vyatta/config/.lock' https_data = { - 'listen_address' : [ '127.0.0.1' ] + 'listen_addresses' : { '*': ['_'] } } api_data = { diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index 289eacf69..d5aa1f5b3 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -40,12 +40,21 @@ server { return 302 https://$server_name$request_uri; } +{% for addr, names in listen_addresses.items() %} server { # SSL configuration # +{% if addr == '*' %} listen 443 ssl default_server; listen [::]:443 ssl default_server; +{% else %} + listen {{ addr }}:443 ssl; +{% endif %} + +{% for name in names %} + server_name {{ name }}; +{% endfor %} {% if vyos_cert %} include {{ vyos_cert.conf }}; @@ -57,9 +66,42 @@ server { include snippets/snakeoil.conf; {% endif %} -{% for l_addr in listen_address %} - server_name {{ l_addr }}; -{% endfor %} + # proxy settings for HTTP API, if enabled; 503, if not + location ~ /(retrieve|configure) { +{% if api %} + proxy_pass http://localhost:{{ api.port }}; + proxy_buffering off; +{% else %} + return 503; +{% endif %} + } + + error_page 501 502 503 =200 @50*_json; + + location @50*_json { + default_type application/json; + return 200 '{"error": "Start service in configuration mode: set service https api"}'; + } + +} +{% else %} +server { + # SSL configuration + # + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + + server_name _; + +{% if vyos_cert %} + include {{ vyos_cert.conf }}; +{% else %} + # + # Self signed certs generated by the ssl-cert package + # Don't use them in a production server! + # + include snippets/snakeoil.conf; +{% endif %} # proxy settings for HTTP API, if enabled; 503, if not location ~ /(retrieve|configure) { @@ -79,6 +121,8 @@ server { } } + +{% endfor %} """ def get_config(): @@ -89,9 +133,14 @@ def get_config(): else: conf.set_level('service https') - if conf.exists('listen-address'): - addrs = conf.return_values('listen-address') - https['listen_address'] = addrs[:] + if conf.exists('listen-addresses'): + addrs = {} + for addr in conf.list_nodes('listen-addresses'): + addrs[addr] = ['_'] + if conf.exists('listen-addresses {0} server-names'.format(addr)): + names = conf.return_values('listen-addresses {0} server-names'.format(addr)) + addrs[addr] = names[:] + https['listen_addresses'] = addrs if conf.exists('certificates'): if conf.exists('certificates system-generated-certificate'): -- cgit v1.2.3 From e4f1bbb270f0afea295646764516675bbcfe0be5 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 27 Aug 2019 22:18:41 +0200 Subject: openvpn: T1617: bugfix for server push-route --- src/conf_mode/interface-openvpn.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interface-openvpn.py b/src/conf_mode/interface-openvpn.py index a8313378b..4e5915d4e 100755 --- a/src/conf_mode/interface-openvpn.py +++ b/src/conf_mode/interface-openvpn.py @@ -522,9 +522,9 @@ def get_config(): # Route to be pushed to all clients if conf.exists('server push-route'): - network = conf.return_value('server push-route') - tmp = IPv4Interface(network).with_netmask - openvpn['server_push_route'] = tmp.replace(r'/', ' ') + for network in conf.return_values('server push-route'): + tmp = IPv4Interface(network).with_netmask + openvpn['server_push_route'].append(tmp.replace(r'/', ' ')) # Reject connections from clients that are not explicitly configured if conf.exists('server reject-unconfigured-clients'): -- cgit v1.2.3 From 71f7a947539963112c61fef2a5f278d524d71198 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 28 Aug 2019 10:59:09 +0200 Subject: bridge: T1615: add missing support for DHCP/DHCPv6 interface address This feature is not well supported by pyroute2 and thus uses the proof-of-concept vyos.interfaceconfig library. Maybe it's a better idea to write our own library from scratch. --- src/conf_mode/interface-bridge.py | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index c1527e4a7..9918cbec7 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -22,6 +22,8 @@ from sys import exit from pyroute2 import IPDB from netifaces import interfaces from vyos.config import Config +from vyos.validate import is_ip +from vyos.interfaceconfig import Interface as IF from vyos import ConfigError default_config_data = { @@ -62,7 +64,9 @@ def get_config(): # Check if bridge has been removed if not conf.exists('interfaces bridge ' + bridge['intf']): bridge['deleted'] = True - return bridge + # we should not bail out early here b/c we should + # find possible DHCP interfaces later on. + # DHCP interfaces invoke dhclient which should be stopped, too # set new configuration level conf.set_level('interfaces bridge ' + bridge['intf']) @@ -185,6 +189,13 @@ def apply(bridge): # delete bridge interface with ipdb.interfaces[ brif ] as br: br.remove() + + # stop DHCP(v6) clients if configured + for addr in bridge['address_remove']: + if addr == 'dhcp': + IF(brif).del_dhcpv4() + elif addr == 'dhcpv6': + IF(brif).del_dhcpv6() except: pass else: @@ -225,16 +236,31 @@ def apply(bridge): for intf in bridge['member_remove']: br.del_port( intf['name'] ) - # configure bridge member interfaces + # add interfaces to bridge for member in bridge['member']: - # add interface br.add_port(member['name']) - # Configure interface address(es) + # remove configured network interface addresses/DHCP(v6) configuration for addr in bridge['address_remove']: - br.del_ip(addr) + try: + is_ip(addr) + br.del_ip(addr) + except ValueError: + if addr == 'dhcp': + IF(brif).del_dhcpv4() + elif addr == 'dhcpv6': + IF(brif).del_dhcpv6() + + # add configured network interface addresses/DHCP(v6) configuration for addr in bridge['address']: - br.add_ip(addr) + try: + is_ip(addr) + br.add_ip(addr) + except: + if addr == 'dhcp': + IF(brif).set_dhcpv4() + elif addr == 'dhcpv6': + IF(brif).set_dhcpv6() # up/down interface if bridge['disable']: -- cgit v1.2.3 From 24495a18b2ba39cd0c5b024dbe63f3e7df92e69c Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 30 Aug 2019 13:25:23 +0200 Subject: Python/ifconfig: rename interfaceconfig.py -> ifconfig.py --- python/vyos/ifconfig.py | 491 ++++++++++++++++++++++++++++++++++++++ python/vyos/interfaceconfig.py | 471 ------------------------------------ src/conf_mode/interface-bridge.py | 2 +- 3 files changed, 492 insertions(+), 472 deletions(-) create mode 100644 python/vyos/ifconfig.py delete mode 100644 python/vyos/interfaceconfig.py (limited to 'src') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py new file mode 100644 index 000000000..5f28125af --- /dev/null +++ b/python/vyos/ifconfig.py @@ -0,0 +1,491 @@ +#!/usr/bin/python3 + +# Copyright 2019 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see . + +import sys +import os +import re +import json +import socket +import subprocess +import ipaddress + +from vyos.validate import * +from ipaddress import IPv4Network, IPv6Address +from netifaces import ifaddresses, AF_INET, AF_INET6 + +dhclient_conf_dir = r'/var/lib/dhcp/dhclient_' + +class Interface: + def __init__(self, ifname=None, type=None): + """ + Create instance of an IP interface + + Example: + + >>> from vyos.ifconfig import Interface + >>> i = Interface('br111', type='bridge') + """ + + if not ifname: + raise Exception("interface name required") + + if not os.path.exists('/sys/class/net/{0}'.format(ifname)) and not type: + raise Exception("interface {0} not found".format(str(ifname))) + + if not os.path.exists('/sys/class/net/{0}'.format(ifname)): + try: + cmd = 'ip link add dev "{}" type "{}"'.format(ifname, type) + self._cmd(cmd) + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + if "Operation not supported" in str(e.output.decode()): + print(str(e.output.decode())) + sys.exit(0) + + self._ifname = str(ifname) + + @property + def remove(self): + """ + Remove system interface + + Example: + + >>> from vyos.ifconfig import Interface + >>> i = Interface('br111', type='bridge') + >>> i.remove + """ + + # NOTE (Improvement): + # after interface removal no other commands should be allowed + # to be called and instead should raise an Exception: + + cmd = 'ip link del dev "{}"'.format(self._ifname) + self._cmd(cmd) + + + def _cmd(self, command): + process = subprocess.Popen(command,stdout=subprocess.PIPE, shell=True) + proc_stdout = process.communicate()[0].strip() + pass + + + @property + def mtu(self): + """ + Get/set interface mtu in bytes. + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth1').mtu + '1500' + """ + + mtu = 0 + with open('/sys/class/net/{0}/mtu'.format(self._ifname), 'r') as f: + mtu = f.read().rstrip('\n') + return mtu + + + @mtu.setter + def mtu(self, mtu=None): + """ + Get/set interface mtu in bytes. + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('br100', type='bridge').mtu = 1400 + >>> Interface('br100').mtu + '1400' + """ + + if mtu < 68 or mtu > 9000: + raise ValueError('Invalid MTU size: "{}"'.format(mru)) + + with open('/sys/class/net/{0}/mtu'.format(self._ifname), 'w') as f: + f.write(str(mtu)) + + + @property + def mac(self): + """ + Get/set interface mac address + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth1').mac + '00:0c:29:11:aa:cc' + """ + address = '' + with open('/sys/class/net/{0}/address'.format(self._ifname), 'r') as f: + address = f.read().rstrip('\n') + return address + + + @mac.setter + def mac(self, mac=None): + """ + Get/set interface mac address + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth1').mac = '00:90:43:fe:fe:1b' + >>> Interface('eth1').mac + '00:90:43:fe:fe:1b' + """ + # a mac address consits out of 6 octets + octets = len(mac.split(':')) + if octets != 6: + raise ValueError('wrong number of MAC octets: {} '.format(octets)) + + # validate against the first mac address byte if it's a multicast address + if int(mac.split(':')[0]) & 1: + raise ValueError('{} is a multicast MAC address'.format(mac)) + + # overall mac address is not allowed to be 00:00:00:00:00:00 + if sum(int(i, 16) for i in mac.split(':')) == 0: + raise ValueError('00:00:00:00:00:00 is not a valid MAC address') + + # check for VRRP mac address + if mac.split(':')[0] == '0' and addr.split(':')[1] == '0' and mac.split(':')[2] == '94' and mac.split(':')[3] == '0' and mac.split(':')[4] == '1': + raise ValueError('{} is a VRRP MAC address'.format(mac)) + + # Assemble command executed on system. Unfortunately there is no way + # of altering the MAC address via sysfs + cmd = 'ip link set dev "{}" address "{}"'.format(self._ifname, mac) + self._cmd(cmd) + + + @property + def ifalias(self): + """ + Get/set interface alias name + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth1').ifalias + '' + """ + + alias = '' + with open('/sys/class/net/{0}/ifalias'.format(self._ifname), 'r') as f: + alias = f.read().rstrip('\n') + return alias + + + @ifalias.setter + def ifalias(self, ifalias=None): + """ + Get/set interface alias name + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth1').ifalias = 'VyOS upstream interface' + >>> Interface('eth1').ifalias + 'VyOS upstream interface' + + to clear interface alias e.g. delete it use: + + >>> Interface('eth1').ifalias = '' + >>> Interface('eth1').ifalias + '' + """ + + # clear interface alias + if not ifalias: + ifalias = '\0' + + with open('/sys/class/net/{0}/ifalias'.format(self._ifname), 'w') as f: + f.write(str(ifalias)) + + @property + def state(self): + """ + Enable (up) / Disable (down) an interface + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth1').state + 'up' + """ + + state = '' + with open('/sys/class/net/{0}/operstate'.format(self._ifname), 'r') as f: + state = f.read().rstrip('\n') + return state + + + @state.setter + def state(self, state=None): + """ + Enable (up) / Disable (down) an interface + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth1').state = 'down' + >>> Interface('eth1').state + 'down' + """ + + if state not in ['up', 'down']: + raise ValueError('state must be "up" or "down"') + + # Assemble command executed on system. Unfortunately there is no way + # to up/down an interface via sysfs + cmd = 'ip link set dev "{}" "{}"'.format(self._ifname, state) + self._cmd(cmd) + + + def _debug(self, e=None): + """ + export DEBUG=1 to see debug messages + """ + if os.getenv('DEBUG') == '1': + if e: + print ("Exception raised:\ncommand: {0}\nerror code: {1}\nsubprocess output: {2}".format( + e.cmd, e.returncode, e.output.decode())) + return True + return False + + + def get_addr(self): + """ + Retrieve assigned IPv4 and IPv6 addresses from given interface. + This is done using the netifaces and ipaddress python modules. + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth1').get_addrs() + ['172.16.33.30/24', 'fe80::20c:29ff:fe11:a174/64'] + """ + + ipv4 = [] + ipv6 = [] + + if AF_INET in ifaddresses(self._ifname).keys(): + for v4_addr in ifaddresses(self._ifname)[AF_INET]: + # we need to manually assemble a list of IPv4 address/prefix + prefix = '/' + str(IPv4Network('0.0.0.0/' + v4_addr['netmask']).prefixlen) + ipv4.append( v4_addr['addr'] + prefix ) + + if AF_INET6 in ifaddresses(self._ifname).keys(): + for v6_addr in ifaddresses(self._ifname)[AF_INET6]: + # Note that currently expanded netmasks are not supported. That means + # 2001:db00::0/24 is a valid argument while 2001:db00::0/ffff:ff00:: not. + # see https://docs.python.org/3/library/ipaddress.html + bits = bin( int(v6_addr['netmask'].replace(':',''), 16) ).count('1') + prefix = '/' + str(bits) + + # we alsoneed to remove the interface suffix on link local addresses + v6_addr['addr'] = v6_addr['addr'].split('%')[0] + ipv6.append( v6_addr['addr'] + prefix ) + + return ipv4 + ipv6 + + + def add_addr(self, addr=None): + """ + Add IP address to interface. Address is only added if it yet not added + to that interface. + + Example: + + >>> from vyos.interfaceconfig import Interface + >>> j = Interface('br100', type='bridge') + >>> j.add_addr('192.0.2.1/24') + >>> j.add_addr('2001:db8::ffff/64') + >>> j.get_addr() + ['192.0.2.1/24', '2001:db8::ffff/64'] + """ + + if not addr: + raise ValueError('No IP address specified') + + if not is_intf_addr_assigned(self._ifname, addr): + cmd = '' + if is_ipv4(addr): + cmd = 'sudo ip -4 addr add "{}" broadcast + dev "{}"'.format(addr, self._ifname) + elif is_ipv6(addr): + cmd = 'sudo ip -6 addr add "{}" dev "{}"'.format(addr, self._ifname) + + self._cmd(cmd) + + + def del_addr(self, addr=None): + """ + Remove IP address from interface. + + Example: + >>> from vyos.interfaceconfig import Interface + >>> j = Interface('br100', type='bridge') + >>> j.add_addr('2001:db8::ffff/64') + >>> j.add_addr('192.0.2.1/24') + >>> j.get_addr() + ['192.0.2.1/24', '2001:db8::ffff/64'] + >>> j.del_addr('192.0.2.1/24') + >>> j.get_addr() + ['2001:db8::ffff/64'] + """ + + if not addr: + raise ValueError('No IP address specified') + + if is_intf_addr_assigned(self._ifname, addr): + cmd = '' + if is_ipv4(addr): + cmd = 'ip -4 addr del "{}" dev "{}"'.format(addr, self._ifname) + elif is_ipv6(addr): + cmd = 'ip -6 addr del "{}" dev "{}"'.format(addr, self._ifname) + + self._cmd(cmd) + + + # replace dhcpv4/v6 with systemd.networkd? + def set_dhcpv4(self): + conf_file = dhclient_conf_dir + self._ifname + '.conf' + pidfile = dhclient_conf_dir + self._ifname + '.pid' + leasefile = dhclient_conf_dir + self._ifname + '.leases' + + a = [ + '# generated by interface_config.py', + 'option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;', + 'interface \"' + self._ifname + '\" {', + '\tsend host-name \"' + socket.gethostname() + '\";', + '\trequest subnet-mask, broadcast-address, routers, domain-name-servers, rfc3442-classless-static-routes, domain-name, interface-mtu;', + '}' + ] + + cnf = "" + for ln in a: + cnf += str(ln + "\n") + open(conf_file, 'w').write(cnf) + if os.path.exists(dhclient_conf_dir + self._ifname + '.pid'): + try: + ret = subprocess.check_output( + ['/sbin/dhclient -4 -r -pf ' + pidfile], shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + try: + ret = subprocess.check_output( + ['/sbin/dhclient -4 -q -nw -cf ' + conf_file + ' -pf ' + pidfile + ' -lf ' + leasefile + ' ' + self._ifname], shell=True).decode() + return True + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + def del_dhcpv4(self): + conf_file = dhclient_conf_dir + self._ifname + '.conf' + pidfile = dhclient_conf_dir + self._ifname + '.pid' + leasefile = dhclient_conf_dir + self._ifname + '.leases' + if not os.path.exists(pidfile): + return 1 + try: + ret = subprocess.check_output( + ['/sbin/dhclient -4 -r -pf ' + pidfile], shell=True).decode() + return True + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + def get_dhcpv4(self): + pidfile = dhclient_conf_dir + self._ifname + '.pid' + if not os.path.exists(pidfile): + print ( + "no dhcp client running on interface {0}".format(self._ifname)) + return False + else: + pid = open(pidfile, 'r').read() + print( + "dhclient running on {0} with pid {1}".format(self._ifname, pid)) + return True + + def set_dhcpv6(self): + conf_file = dhclient_conf_dir + self._ifname + '.v6conf' + pidfile = dhclient_conf_dir + self._ifname + '.v6pid' + leasefile = dhclient_conf_dir + self._ifname + '.v6leases' + a = [ + '# generated by interface_config.py', + 'interface \"' + self._ifname + '\" {', + '\trequest routers, domain-name-servers, domain-name;', + '}' + ] + cnf = "" + for ln in a: + cnf += str(ln + "\n") + open(conf_file, 'w').write(cnf) + subprocess.call( + ['sysctl', '-q', '-w', 'net.ipv6.conf.' + self._ifname + '.accept_ra=0']) + if os.path.exists(pidfile): + try: + ret = subprocess.check_output( + ['/sbin/dhclient -6 -q -x -pf ' + pidfile], shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + try: + ret = subprocess.check_output( + ['/sbin/dhclient -6 -q -nw -cf ' + conf_file + ' -pf ' + pidfile + ' -lf ' + leasefile + ' ' + self._ifname], shell=True).decode() + return True + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + def del_dhcpv6(self): + conf_file = dhclient_conf_dir + self._ifname + '.v6conf' + pidfile = dhclient_conf_dir + self._ifname + '.v6pid' + leasefile = dhclient_conf_dir + self._ifname + '.v6leases' + if not os.path.exists(pidfile): + return 1 + try: + ret = subprocess.check_output( + ['/sbin/dhclient -6 -q -x -pf ' + pidfile], shell=True).decode() + subprocess.call( + ['sysctl', '-q', '-w', 'net.ipv6.conf.' + self._ifname + '.accept_ra=1']) + return True + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + def get_dhcpv6(self): + pidfile = dhclient_conf_dir + self._ifname + '.v6pid' + if not os.path.exists(pidfile): + print ( + "no dhcpv6 client running on interface {0}".format(self._ifname)) + return False + else: + pid = open(pidfile, 'r').read() + print( + "dhclientv6 running on {0} with pid {1}".format(self._ifname, pid)) + return True + + +# TODO: dhcpv6-pd via dhclient diff --git a/python/vyos/interfaceconfig.py b/python/vyos/interfaceconfig.py deleted file mode 100644 index 9790fae49..000000000 --- a/python/vyos/interfaceconfig.py +++ /dev/null @@ -1,471 +0,0 @@ -#!/usr/bin/python3 - -# Copyright 2019 VyOS maintainers and contributors -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library. If not, see . - -import sys -import os -import re -import json -import socket -import subprocess -import ipaddress - -from vyos.validate import * -from ipaddress import IPv4Network, IPv6Address -from netifaces import ifaddresses, AF_INET, AF_INET6 - -dhclient_conf_dir = r'/var/lib/dhcp/dhclient_' - -class Interface: - def __init__(self, ifname=None, type=None): - """ - Create instance of an IP interface - - Example: - - from vyos.interfaceconfig import Interface - i = Interface('br111', type='bridge') - """ - - if not ifname: - raise Exception("interface name required") - - if not os.path.exists('/sys/class/net/{0}'.format(ifname)) and not type: - raise Exception("interface {0} not found".format(str(ifname))) - - if not os.path.exists('/sys/class/net/{0}'.format(ifname)): - try: - cmd = 'ip link add dev "{}" type "{}"'.format(ifname, type) - self._cmd(cmd) - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - if "Operation not supported" in str(e.output.decode()): - print(str(e.output.decode())) - sys.exit(0) - - self._ifname = str(ifname) - - @property - def remove(self): - """ - Remove system interface - - Example: - - from vyos.interfaceconfig import Interface - i = Interface('br111', type='bridge') - i.remove - """ - - # NOTE (Improvement): - # after interface removal no other commands should be allowed - # to be called and instead should raise an Exception: - - cmd = 'ip link del dev "{}"'.format(self._ifname) - self._cmd(cmd) - - - def _cmd(self, command): - process = subprocess.Popen(command,stdout=subprocess.PIPE, shell=True) - proc_stdout = process.communicate()[0].strip() - pass - - - @property - def mtu(self): - """ - Get/set interface mtu in bytes. - - Example: - - from vyos.interfaceconfig import Interface - mtu = Interface('ens192').mtu - print(mtu) - """ - - mtu = 0 - with open('/sys/class/net/{0}/mtu'.format(self._ifname), 'r') as f: - mtu = f.read().rstrip('\n') - return mtu - - - @mtu.setter - def mtu(self, mtu=None): - """ - Get/set interface mtu in bytes. - - Example: - - from vyos.interfaceconfig import Interface - Interface('ens192').mtu = 1400 - """ - - if mtu < 68 or mtu > 9000: - raise ValueError('Invalid MTU size: "{}"'.format(mru)) - - with open('/sys/class/net/{0}/mtu'.format(self._ifname), 'w') as f: - f.write(str(mtu)) - - - @property - def mac(self): - """ - Get/set interface mac address - - Example: - - from vyos.interfaceconfig import Interface - mac = Interface('ens192').mac - """ - address = '' - with open('/sys/class/net/{0}/address'.format(self._ifname), 'r') as f: - address = f.read().rstrip('\n') - return address - - - @mac.setter - def mac(self, mac=None): - """ - Get/set interface mac address - - Example: - - from vyos.interfaceconfig import Interface - Interface('ens192').mac = '00:90:43:fe:fe:1b' - """ - # a mac address consits out of 6 octets - octets = len(mac.split(':')) - if octets != 6: - raise ValueError('wrong number of MAC octets: {} '.format(octets)) - - # validate against the first mac address byte if it's a multicast address - if int(mac.split(':')[0]) & 1: - raise ValueError('{} is a multicast MAC address'.format(mac)) - - # overall mac address is not allowed to be 00:00:00:00:00:00 - if sum(int(i, 16) for i in mac.split(':')) == 0: - raise ValueError('00:00:00:00:00:00 is not a valid MAC address') - - # check for VRRP mac address - if mac.split(':')[0] == '0' and addr.split(':')[1] == '0' and mac.split(':')[2] == '94' and mac.split(':')[3] == '0' and mac.split(':')[4] == '1': - raise ValueError('{} is a VRRP MAC address'.format(mac)) - - # Assemble command executed on system. Unfortunately there is no way - # of altering the MAC address via sysfs - cmd = 'ip link set dev "{}" address "{}"'.format(self._ifname, mac) - self._cmd(cmd) - - - @property - def ifalias(self): - """ - Get/set interface alias name - - Example: - - from vyos.interfaceconfig import Interface - alias = Interface('ens192').ifalias - """ - - alias = '' - with open('/sys/class/net/{0}/ifalias'.format(self._ifname), 'r') as f: - alias = f.read().rstrip('\n') - return alias - - - @ifalias.setter - def ifalias(self, ifalias=None): - """ - Get/set interface alias name - - Example: - - from vyos.interfaceconfig import Interface - Interface('ens192').ifalias = 'VyOS upstream interface' - - to clear interface alias e.g. delete it use: - - Interface('ens192').ifalias = '' - """ - - # clear interface alias - if not ifalias: - ifalias = '\0' - - with open('/sys/class/net/{0}/ifalias'.format(self._ifname), 'w') as f: - f.write(str(ifalias)) - - @property - def state(self): - """ - Enable (up) / Disable (down) an interface - - Example: - - from vyos.interfaceconfig import Interface - i = Interface('ens192').link - """ - - state = '' - with open('/sys/class/net/{0}/operstate'.format(self._ifname), 'r') as f: - state = f.read().rstrip('\n') - return state - - - @state.setter - def state(self, state=None): - - if state not in ['up', 'down']: - raise ValueError('state must be "up" or "down"') - - # Assemble command executed on system. Unfortunately there is no way - # to up/down an interface via sysfs - cmd = 'ip link set dev "{}" "{}"'.format(self._ifname, state) - self._cmd(cmd) - - - def _debug(self, e=None): - """ - export DEBUG=1 to see debug messages - """ - if os.getenv('DEBUG') == '1': - if e: - print ("Exception raised:\ncommand: {0}\nerror code: {1}\nsubprocess output: {2}".format( - e.cmd, e.returncode, e.output.decode())) - return True - return False - - - def get_addr(self): - """ - Retrieve assigned IPv4 and IPv6 addresses from given interface. - This is done using the netifaces and ipaddress python modules. - - Example: - - from vyos.interfaceconfig import Interface - i = Interface('ens192') - i.get_addrs() - ['172.16.33.30/24', 'fe80::20c:29ff:fe11:a174/64'] - """ - - ipv4 = [] - ipv6 = [] - - if AF_INET in ifaddresses(self._ifname).keys(): - for v4_addr in ifaddresses(self._ifname)[AF_INET]: - # we need to manually assemble a list of IPv4 address/prefix - prefix = '/' + str(IPv4Network('0.0.0.0/' + v4_addr['netmask']).prefixlen) - ipv4.append( v4_addr['addr'] + prefix ) - - if AF_INET6 in ifaddresses(self._ifname).keys(): - for v6_addr in ifaddresses(self._ifname)[AF_INET6]: - # Note that currently expanded netmasks are not supported. That means - # 2001:db00::0/24 is a valid argument while 2001:db00::0/ffff:ff00:: not. - # see https://docs.python.org/3/library/ipaddress.html - bits = bin( int(v6_addr['netmask'].replace(':',''), 16) ).count('1') - prefix = '/' + str(bits) - - # we alsoneed to remove the interface suffix on link local addresses - v6_addr['addr'] = v6_addr['addr'].split('%')[0] - ipv6.append( v6_addr['addr'] + prefix ) - - return ipv4 + ipv6 - - - def add_addr(self, addr=None): - """ - Add IP address to interface. Address is only added if it yet not added - to that interface. - - Example: - - >>> from vyos.interfaceconfig import Interface - >>> j = Interface('br100', type='bridge') - >>> j.add_addr('192.0.2.1/24') - >>> j.add_addr('2001:db8::ffff/64') - >>> j.get_addr() - ['192.0.2.1/24', '2001:db8::ffff/64'] - """ - - if not addr: - raise ValueError('No IP address specified') - - if not is_intf_addr_assigned(self._ifname, addr): - cmd = '' - if is_ipv4(addr): - cmd = 'sudo ip -4 addr add "{}" broadcast + dev "{}"'.format(addr, self._ifname) - elif is_ipv6(addr): - cmd = 'sudo ip -6 addr add "{}" dev "{}"'.format(addr, self._ifname) - - self._cmd(cmd) - - - def del_addr(self, addr=None): - """ - Remove IP address from interface. - - Example: - >>> from vyos.interfaceconfig import Interface - >>> j = Interface('br100', type='bridge') - >>> j.add_addr('2001:db8::ffff/64') - >>> j.add_addr('192.0.2.1/24') - >>> j.get_addr() - ['192.0.2.1/24', '2001:db8::ffff/64'] - >>> j.del_addr('192.0.2.1/24') - >>> j.get_addr() - ['2001:db8::ffff/64'] - """ - - if not addr: - raise ValueError('No IP address specified') - - if is_intf_addr_assigned(self._ifname, addr): - cmd = '' - if is_ipv4(addr): - cmd = 'ip -4 addr del "{}" dev "{}"'.format(addr, self._ifname) - elif is_ipv6(addr): - cmd = 'ip -6 addr del "{}" dev "{}"'.format(addr, self._ifname) - - self._cmd(cmd) - - - # replace dhcpv4/v6 with systemd.networkd? - def set_dhcpv4(self): - conf_file = dhclient_conf_dir + self._ifname + '.conf' - pidfile = dhclient_conf_dir + self._ifname + '.pid' - leasefile = dhclient_conf_dir + self._ifname + '.leases' - - a = [ - '# generated by interface_config.py', - 'option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;', - 'interface \"' + self._ifname + '\" {', - '\tsend host-name \"' + socket.gethostname() + '\";', - '\trequest subnet-mask, broadcast-address, routers, domain-name-servers, rfc3442-classless-static-routes, domain-name, interface-mtu;', - '}' - ] - - cnf = "" - for ln in a: - cnf += str(ln + "\n") - open(conf_file, 'w').write(cnf) - if os.path.exists(dhclient_conf_dir + self._ifname + '.pid'): - try: - ret = subprocess.check_output( - ['/sbin/dhclient -4 -r -pf ' + pidfile], shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - try: - ret = subprocess.check_output( - ['/sbin/dhclient -4 -q -nw -cf ' + conf_file + ' -pf ' + pidfile + ' -lf ' + leasefile + ' ' + self._ifname], shell=True).decode() - return True - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def del_dhcpv4(self): - conf_file = dhclient_conf_dir + self._ifname + '.conf' - pidfile = dhclient_conf_dir + self._ifname + '.pid' - leasefile = dhclient_conf_dir + self._ifname + '.leases' - if not os.path.exists(pidfile): - return 1 - try: - ret = subprocess.check_output( - ['/sbin/dhclient -4 -r -pf ' + pidfile], shell=True).decode() - return True - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def get_dhcpv4(self): - pidfile = dhclient_conf_dir + self._ifname + '.pid' - if not os.path.exists(pidfile): - print ( - "no dhcp client running on interface {0}".format(self._ifname)) - return False - else: - pid = open(pidfile, 'r').read() - print( - "dhclient running on {0} with pid {1}".format(self._ifname, pid)) - return True - - def set_dhcpv6(self): - conf_file = dhclient_conf_dir + self._ifname + '.v6conf' - pidfile = dhclient_conf_dir + self._ifname + '.v6pid' - leasefile = dhclient_conf_dir + self._ifname + '.v6leases' - a = [ - '# generated by interface_config.py', - 'interface \"' + self._ifname + '\" {', - '\trequest routers, domain-name-servers, domain-name;', - '}' - ] - cnf = "" - for ln in a: - cnf += str(ln + "\n") - open(conf_file, 'w').write(cnf) - subprocess.call( - ['sysctl', '-q', '-w', 'net.ipv6.conf.' + self._ifname + '.accept_ra=0']) - if os.path.exists(pidfile): - try: - ret = subprocess.check_output( - ['/sbin/dhclient -6 -q -x -pf ' + pidfile], shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - try: - ret = subprocess.check_output( - ['/sbin/dhclient -6 -q -nw -cf ' + conf_file + ' -pf ' + pidfile + ' -lf ' + leasefile + ' ' + self._ifname], shell=True).decode() - return True - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def del_dhcpv6(self): - conf_file = dhclient_conf_dir + self._ifname + '.v6conf' - pidfile = dhclient_conf_dir + self._ifname + '.v6pid' - leasefile = dhclient_conf_dir + self._ifname + '.v6leases' - if not os.path.exists(pidfile): - return 1 - try: - ret = subprocess.check_output( - ['/sbin/dhclient -6 -q -x -pf ' + pidfile], shell=True).decode() - subprocess.call( - ['sysctl', '-q', '-w', 'net.ipv6.conf.' + self._ifname + '.accept_ra=1']) - return True - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def get_dhcpv6(self): - pidfile = dhclient_conf_dir + self._ifname + '.v6pid' - if not os.path.exists(pidfile): - print ( - "no dhcpv6 client running on interface {0}".format(self._ifname)) - return False - else: - pid = open(pidfile, 'r').read() - print( - "dhclientv6 running on {0} with pid {1}".format(self._ifname, pid)) - return True - - -# TODO: dhcpv6-pd via dhclient diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index 9918cbec7..d5661be93 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -23,7 +23,7 @@ from pyroute2 import IPDB from netifaces import interfaces from vyos.config import Config from vyos.validate import is_ip -from vyos.interfaceconfig import Interface as IF +from vyos.ifconfig import Interface as IF from vyos import ConfigError default_config_data = { -- cgit v1.2.3 From 76f498220c796a39a92ce3bed47c1cc5bac1a99d Mon Sep 17 00:00:00 2001 From: Dmytro Aleksandrov Date: Fri, 30 Aug 2019 18:43:44 +0300 Subject: [op-mode] T1621 rewrite misc commands to python/xml syntax --- op-mode-definitions/bandwidth-test.xml | 29 +++++ op-mode-definitions/disks.xml | 50 ++++++++ op-mode-definitions/generate-ssh-server-key.xml | 16 +++ op-mode-definitions/show-disk.xml | 23 ---- op-mode-definitions/show-history.xml | 31 +++++ op-mode-definitions/show-host.xml | 12 ++ op-mode-definitions/telnet.xml | 8 +- op-mode-definitions/terminal.xml | 93 +++++++++++++++ python/vyos/util.py | 8 ++ src/completion/list_disks.py | 36 ++++++ src/completion/list_disks.sh | 5 - src/helpers/vyos-sudo.py | 9 +- src/op_mode/format_disk.py | 148 ++++++++++++++++++++++++ src/op_mode/generate_ssh_server_key.py | 27 +++++ src/op_mode/toggle_help_binding.sh | 25 ++++ 15 files changed, 480 insertions(+), 40 deletions(-) create mode 100644 op-mode-definitions/bandwidth-test.xml create mode 100644 op-mode-definitions/disks.xml create mode 100644 op-mode-definitions/generate-ssh-server-key.xml delete mode 100644 op-mode-definitions/show-disk.xml create mode 100644 op-mode-definitions/show-history.xml create mode 100755 src/completion/list_disks.py delete mode 100755 src/completion/list_disks.sh create mode 100755 src/op_mode/format_disk.py create mode 100755 src/op_mode/generate_ssh_server_key.py create mode 100755 src/op_mode/toggle_help_binding.sh (limited to 'src') diff --git a/op-mode-definitions/bandwidth-test.xml b/op-mode-definitions/bandwidth-test.xml new file mode 100644 index 000000000..d1e459b17 --- /dev/null +++ b/op-mode-definitions/bandwidth-test.xml @@ -0,0 +1,29 @@ + + + + + + + Initiate or wait for bandwidth test + + + + + Wait for bandwidth test connections (port TCP/5001) + + iperf -s + + + + Initiate a bandwidth test to specified host (port TCP/5001) + + <hostname> <x.x.x.x> <h:h:h:h:h:h:h:h> + + + iperf -c $4 + + + + + + diff --git a/op-mode-definitions/disks.xml b/op-mode-definitions/disks.xml new file mode 100644 index 000000000..fb39c4f3c --- /dev/null +++ b/op-mode-definitions/disks.xml @@ -0,0 +1,50 @@ + + + + + Format a device + + + + + Format a disk drive + + + + + + + + Format this disk the same as another disk + + + + + ${vyos_op_scripts_dir}/format_disk.py --target $3 --proto $5 + + + + + + + + + + + Show status of disk device + + + + + + + + Show disk drive formatting + + ${vyos_op_scripts_dir}/show_disk_format.sh $3 + + + + + + diff --git a/op-mode-definitions/generate-ssh-server-key.xml b/op-mode-definitions/generate-ssh-server-key.xml new file mode 100644 index 000000000..a6ebf1b78 --- /dev/null +++ b/op-mode-definitions/generate-ssh-server-key.xml @@ -0,0 +1,16 @@ + + + + + Generate an object + + + + + Regenerate the host SSH keys and restart the SSH server + + ${vyos_op_scripts_dir}/generate_ssh_server_key.py + + + + diff --git a/op-mode-definitions/show-disk.xml b/op-mode-definitions/show-disk.xml deleted file mode 100644 index 37da07fbe..000000000 --- a/op-mode-definitions/show-disk.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - Show status of disk device - - - - - - - - Show disk drive formatting - - ${vyos_op_scripts_dir}/show_disk_format.sh $3 - - - - - - diff --git a/op-mode-definitions/show-history.xml b/op-mode-definitions/show-history.xml new file mode 100644 index 000000000..7fb286264 --- /dev/null +++ b/op-mode-definitions/show-history.xml @@ -0,0 +1,31 @@ + + + + + + + Show command history + + HISTTIMEFORMAT='%FT%T%z ' HISTFILE="$HOME/.bash_history" \set -o history; history + + + + Show recent command history + + HISTTIMEFORMAT='%FT%T%z ' HISTFILE="$HOME/.bash_history" \set -o history; history 20 + + + + + + + Show last N commands in history + + <NUMBER> + + + HISTTIMEFORMAT='%FT%T%z ' HISTFILE="$HOME/.bash_history" \set -o history; history $3 + + + + diff --git a/op-mode-definitions/show-host.xml b/op-mode-definitions/show-host.xml index d7f8104aa..eee1288a1 100644 --- a/op-mode-definitions/show-host.xml +++ b/op-mode-definitions/show-host.xml @@ -7,6 +7,12 @@ Show host information + + + Show host current date + + /bin/date + Show domain name @@ -25,6 +31,12 @@ /usr/bin/host $4 + + + Show host operating system details + + /bin/uname -a + diff --git a/op-mode-definitions/telnet.xml b/op-mode-definitions/telnet.xml index 7ec75be88..c5bb6d283 100644 --- a/op-mode-definitions/telnet.xml +++ b/op-mode-definitions/telnet.xml @@ -5,23 +5,23 @@ Telnet to a node - + Telnet to a host <hostname> <x.x.x.x> <h:h:h:h:h:h:h:h> - /usr/bin/telnet $2 + /usr/bin/telnet $3 - + Telnet to a host:port <0-65535> - /usr/bin/telnet $2 $3 + /usr/bin/telnet $3 $5 diff --git a/op-mode-definitions/terminal.xml b/op-mode-definitions/terminal.xml index db74f867e..9c4e629cb 100644 --- a/op-mode-definitions/terminal.xml +++ b/op-mode-definitions/terminal.xml @@ -26,4 +26,97 @@ + + + Set operational options + + + + + Bash builtin set command + + <OPTION> + + + builtin $3 + + + + + Control console behaviors + + + + + Reconfigure console keyboard layout + + sudo dpkg-reconfigure -f dialog keyboard-configuration && sudo systemctl restart keyboard-setup + + + + + + + Control terminal behaviors + + + + + + Set key behaviors + + + + + Enable/disable getting help using question mark (default enabled) + + enable disable + + + ${vyos_op_scripts_dir}/toggle_help_binding.sh $5 + + + + + + + Set terminal pager to default (less) + + VYATTA_PAGER=${_vyatta_default_pager} + + + + Set terminal pager + + <PROGRAM> + + + VYATTA_PAGER=$4 + + + + + Set terminal to given number of rows (0 disables paging) + + <NUMBER> + + + if [ "$4" -eq 0 ]; then VYATTA_PAGER=cat; else VYATTA_PAGER=${_vyatta_default_pager}; stty rows $4; fi + + + + + Set terminal to given number of columns + + <NUMBER> + + + stty columns $4 + + + + + + + diff --git a/python/vyos/util.py b/python/vyos/util.py index 88bb0c8f4..67a602f7a 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -15,6 +15,7 @@ import os import re +import getpass import grp import time import subprocess @@ -191,3 +192,10 @@ def ask_yes_no(question, default=False) -> bool: return False else: sys.stdout.write("Please respond with yes/y or no/n\n") + + +def is_admin() -> bool: + """Look if current user is in sudo group""" + current_user = getpass.getuser() + (_, _, _, admin_group_members) = grp.getgrnam('sudo') + return current_user in admin_group_members diff --git a/src/completion/list_disks.py b/src/completion/list_disks.py new file mode 100755 index 000000000..ff1135e23 --- /dev/null +++ b/src/completion/list_disks.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Completion script used by show disks to collect physical disk + +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument("-e", "--exclude", type=str, help="Exclude specified device from the result list") +args = parser.parse_args() + +disks = set() +with open('/proc/partitions') as partitions_file: + for line in partitions_file: + fields = line.strip().split() + if len(fields) == 4 and fields[3].isalpha() and fields[3] != 'name': + disks.add(fields[3]) + +if args.exclude: + disks.remove(args.exclude) + +for disk in disks: + print(disk) diff --git a/src/completion/list_disks.sh b/src/completion/list_disks.sh deleted file mode 100755 index f32e558fd..000000000 --- a/src/completion/list_disks.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -# Completion script used by show disks to collect physical disk - -awk 'NR > 2 && $4 !~ /[0-9]$/ { print $4 }' . -import getpass -import grp import os import sys - -def is_admin() -> bool: - """Look if current user is in sudo group""" - current_user = getpass.getuser() - (_, _, _, admin_group_members) = grp.getgrnam('sudo') - return current_user in admin_group_members +from vyos.util import is_admin if __name__ == '__main__': diff --git a/src/op_mode/format_disk.py b/src/op_mode/format_disk.py new file mode 100755 index 000000000..5a3b250ee --- /dev/null +++ b/src/op_mode/format_disk.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import argparse +import os +import re +import subprocess +import sys +from datetime import datetime +from time import sleep + +from vyos.util import is_admin, ask_yes_no + + +def list_disks(): + disks = set() + with open('/proc/partitions') as partitions_file: + for line in partitions_file: + fields = line.strip().split() + if len(fields) == 4 and fields[3].isalpha() and fields[3] != 'name': + disks.add(fields[3]) + return disks + + +def is_busy(disk: str): + """Check if given disk device is busy by re-reading it's partition table""" + + cmd = 'sudo blockdev --rereadpt /dev/{}'.format(disk) + status = subprocess.call([cmd], shell=True, stderr=subprocess.DEVNULL) + return status != 0 + + +def backup_partitions(disk: str): + """Save sfdisk partitions output to a backup file""" + + device_path = '/dev/' + disk + backup_ts = datetime.now().strftime('%Y-%m-%d-%H:%M') + backup_file = '/var/tmp/backup_{}.{}'.format(disk, backup_ts) + cmd = 'sudo /sbin/sfdisk -d {} > {}'.format(device_path, backup_file) + subprocess.check_call([cmd], shell=True) + + +def list_partitions(disk: str): + """List partition numbers of a given disk""" + + parts = set() + part_num_expr = re.compile(disk + '([0-9]+)') + with open('/proc/partitions') as partitions_file: + for line in partitions_file: + fields = line.strip().split() + if len(fields) == 4 and fields[3] != 'name' and part_num_expr.match(fields[3]): + part_idx = part_num_expr.match(fields[3]).group(1) + parts.add(int(part_idx)) + return parts + + +def delete_partition(disk: str, partition_idx: int): + cmd = 'sudo /sbin/parted /dev/{} rm {}'.format(disk, partition_idx) + subprocess.check_call([cmd], shell=True) + + +def format_disk_like(target: str, proto: str): + cmd = 'sudo /sbin/sfdisk -d /dev/{} | sudo /sbin/sfdisk --force /dev/{}'.format(proto, target) + subprocess.check_call([cmd], shell=True) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + group = parser.add_argument_group() + group.add_argument('-t', '--target', type=str, required=True, help='Target device to format') + group.add_argument('-p', '--proto', type=str, required=True, help='Prototype device to use as reference') + args = parser.parse_args() + + if not is_admin(): + print('Must be admin or root to format disk') + sys.exit(1) + + target_disk = args.target + eligible_target_disks = list_disks() + + proto_disk = args.proto + eligible_proto_disks = eligible_target_disks.copy() + eligible_proto_disks.remove(target_disk) + + fmt = { + 'target_disk': target_disk, + 'proto_disk': proto_disk, + } + + if proto_disk == target_disk: + print('The two disk drives must be different.') + sys.exit(1) + + if not os.path.exists('/dev/' + proto_disk): + print('Device /dev/{proto_disk} does not exist'.format_map(fmt)) + sys.exit(1) + + if not os.path.exists('/dev/' + target_disk): + print('Device /dev/{target_disk} does not exist'.format_map(fmt)) + sys.exit(1) + + if target_disk not in eligible_target_disks: + print('Device {target_disk} can not be formatted'.format_map(fmt)) + sys.exit(1) + + if proto_disk not in eligible_proto_disks: + print('Device {proto_disk} can not be used as a prototype for {target_disk}'.format_map(fmt)) + sys.exit(1) + + if is_busy(target_disk): + print("Disk device {target_disk} is busy. Can't format it now".format_map(fmt)) + sys.exit(1) + + print('This will re-format disk {target_disk} so that it has the same disk\n' + 'partion sizes and offsets as {proto_disk}. This will not copy\n' + 'data from {proto_disk} to {target_disk}. But this will erase all\n' + 'data on {target_disk}.\n'.format_map(fmt)) + + if not ask_yes_no("Do you wish to proceed?"): + print('OK. Disk drive {target_disk} will not be re-formated'.format_map(fmt)) + sys.exit(0) + + print('OK. Re-formating disk drive {target_disk}...'.format_map(fmt)) + + print('Making backup copy of partitions...') + backup_partitions(target_disk) + sleep(1) + + print('Deleting old partitions...') + for p in list_partitions(target_disk): + delete_partition(disk=target_disk, partition_idx=p) + + print('Creating new partitions on {target_disk} based on {proto_disk}...'.format_map(fmt)) + format_disk_like(target=target_disk, proto=proto_disk) + print('Done.') diff --git a/src/op_mode/generate_ssh_server_key.py b/src/op_mode/generate_ssh_server_key.py new file mode 100755 index 000000000..f205919b8 --- /dev/null +++ b/src/op_mode/generate_ssh_server_key.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import subprocess +import sys + +from vyos.util import ask_yes_no + +if not ask_yes_no('Do you really want to remove the existing SSH host keys?'): + sys.exit(0) +else: + subprocess.check_call(['sudo rm -v /etc/ssh/ssh_host_*'], shell=True) + subprocess.check_call(['sudo dpkg-reconfigure openssh-server'], shell=True) + subprocess.check_call(['sudo systemctl restart ssh'], shell=True) diff --git a/src/op_mode/toggle_help_binding.sh b/src/op_mode/toggle_help_binding.sh new file mode 100755 index 000000000..a8708f3da --- /dev/null +++ b/src/op_mode/toggle_help_binding.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Script for [un-]binding the question mark key for getting help +if [ "$1" == 'disable' ]; then + sed -i "/^bind '\"?\": .* # vyatta key binding$/d" $HOME/.bashrc + echo "bind '\"?\": self-insert' # vyatta key binding" >> $HOME/.bashrc + bind '"?": self-insert' +else + sed -i "/^bind '\"?\": .* # vyatta key binding$/d" $HOME/.bashrc + bind '"?": possible-completions' +fi -- cgit v1.2.3 From ff05e2a90edf8af5d7b8ad5c69cae2dd40af2c8d Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Fri, 30 Aug 2019 18:40:44 +0200 Subject: T1598: fix vyos-hostsd unit dependencies. --- src/systemd/vyos-hostsd.service | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/systemd/vyos-hostsd.service b/src/systemd/vyos-hostsd.service index 3b0fadb5c..2444f5352 100644 --- a/src/systemd/vyos-hostsd.service +++ b/src/systemd/vyos-hostsd.service @@ -1,8 +1,7 @@ [Unit] Description=VyOS DNS configuration keeper -After=auditd.service time-sync.target -Before=network-pre.target vyos-router.service -Wants=network-pre.target +DefaultDependencies=no +After=systemd-remount-fs.service [Service] ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-hostsd @@ -19,6 +18,4 @@ User=root Group=vyattacfg [Install] -# -WantedBy=network.target - +RequiredBy=cloud-init-local.service vyos-router.service -- cgit v1.2.3 From 0af7ae0e91a2214f70a415b84273e7b2bba3e757 Mon Sep 17 00:00:00 2001 From: DmitriyEshenko Date: Sat, 31 Aug 2019 00:26:06 +0000 Subject: [l2tp ipsec] T1605: Changed ipsec marking only for inbound policy, all functionality must saved --- src/conf_mode/ipsec-settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py index 8d25e7abd..156bb2edd 100755 --- a/src/conf_mode/ipsec-settings.py +++ b/src/conf_mode/ipsec-settings.py @@ -62,7 +62,7 @@ conn {{ra_conn_name}} left={{outside_addr}} leftsubnet=%dynamic[/1701] rightsubnet=%dynamic - mark=%unique + mark_in=%unique auto=add ike=aes256-sha1-modp1024,3des-sha1-modp1024,3des-sha1-modp1024! dpddelay=15 -- cgit v1.2.3 From 04b192c7d1949a6c8223051f144287ac8dbbd1ec Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 31 Aug 2019 13:02:04 +0200 Subject: bridge: T1615: replace pyroute2 by vyos.ifconfig --- src/conf_mode/interface-bridge.py | 127 ++++++++++++++------------------------ 1 file changed, 47 insertions(+), 80 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index d5661be93..187be677e 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -16,21 +16,21 @@ # # -from os import environ +import os + from copy import deepcopy from sys import exit -from pyroute2 import IPDB from netifaces import interfaces from vyos.config import Config from vyos.validate import is_ip -from vyos.ifconfig import Interface as IF +from vyos.ifconfig import BridgeIf, Interface from vyos import ConfigError default_config_data = { 'address': [], 'address_remove': [], 'aging': 300, - 'arp_cache_timeout_ms': 30000, + 'arp_cache_tmo': 30, 'description': '', 'deleted': False, 'disable': False, @@ -57,7 +57,7 @@ def get_config(): # determine tagNode instance try: - bridge['intf'] = environ['VYOS_TAGNODE_VALUE'] + bridge['intf'] = os.environ['VYOS_TAGNODE_VALUE'] except KeyError as E: print("Interface not specified") @@ -107,7 +107,7 @@ def get_config(): # ARP cache entry timeout in seconds if conf.exists('ip arp-cache-timeout'): - bridge['arp_cache_timeout_ms'] = int(conf.return_value('ip arp-cache-timeout')) * 1000 + bridge['arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) # Media Access Control (MAC) address if conf.exists('mac'): @@ -181,56 +181,35 @@ def generate(bridge): return None def apply(bridge): - ipdb = IPDB(mode='explicit') - brif = bridge['intf'] + br = BridgeIf(bridge['intf']) if bridge['deleted']: - try: - # delete bridge interface - with ipdb.interfaces[ brif ] as br: - br.remove() - - # stop DHCP(v6) clients if configured - for addr in bridge['address_remove']: - if addr == 'dhcp': - IF(brif).del_dhcpv4() - elif addr == 'dhcpv6': - IF(brif).del_dhcpv6() - except: - pass + # delete bridge interface + # DHCP is stopped inside remove() + br.remove() else: - try: - # create bridge interface if it not already exists - ipdb.create(kind='bridge', ifname=brif).commit() - except: - pass - - # get handle in bridge interface - br = ipdb.interfaces[brif] - # begin() a transaction prior to make any change - br.begin() # enable interface - br.up() - # set ageing time - - value is in centiseconds YES! centiseconds! - br.br_ageing_time = bridge['aging'] * 100 - # set bridge forward delay - value is in centiseconds YES! centiseconds! - br.br_forward_delay = bridge['forwarding_delay'] * 100 - # set hello time - value is in centiseconds YES! centiseconds! - br.br_hello_time = bridge['hello_time'] * 100 - # set max message age - value is in centiseconds YES! centiseconds! - br.br_max_age = bridge['max_age'] * 100 + br.state = 'up' + # set ageing time + br.ageing_time = bridge['aging'] + # set bridge forward delay + br.forward_delay = bridge['forwarding_delay'] + # set hello time + br.hello_time = bridge['hello_time'] + # set max message age + br.max_age = bridge['max_age'] # set bridge priority - br.br_priority = bridge['priority'] + br.priority = bridge['priority'] # turn stp on/off - br.br_stp_state = bridge['stp'] + br.stp_state = bridge['stp'] # enable or disable IGMP querier - br.br_mcast_querier = bridge['igmp_querier'] + br.multicast_querier = bridge['igmp_querier'] # update interface description used e.g. within SNMP br.ifalias = bridge['description'] # Change interface MAC address if bridge['mac']: - br.set_address = bridge['mac'] + br.mac = bridge['mac'] # remove interface from bridge for intf in bridge['member_remove']: @@ -240,52 +219,40 @@ def apply(bridge): for member in bridge['member']: br.add_port(member['name']) + # up/down interface + if bridge['disable']: + br.state = 'down' + # remove configured network interface addresses/DHCP(v6) configuration for addr in bridge['address_remove']: - try: - is_ip(addr) - br.del_ip(addr) - except ValueError: - if addr == 'dhcp': - IF(brif).del_dhcpv4() - elif addr == 'dhcpv6': - IF(brif).del_dhcpv6() + if addr == 'dhcp': + br.del_dhcp() + elif addr == 'dhcpv6': + br.del_dhcpv6() + else: + br.del_addr(addr) # add configured network interface addresses/DHCP(v6) configuration for addr in bridge['address']: - try: - is_ip(addr) - br.add_ip(addr) - except: - if addr == 'dhcp': - IF(brif).set_dhcpv4() - elif addr == 'dhcpv6': - IF(brif).set_dhcpv6() - - # up/down interface - if bridge['disable']: - br.down() - - # commit changes on bridge interface - br.commit() + if addr == 'dhcp': + br.set_dhcp() + elif addr == 'dhcpv6': + br.set_dhcpv6() + else: + br.add_addr(addr) # configure additional bridge member options for member in bridge['member']: - # configure ARP cache timeout in milliseconds - with open('/proc/sys/net/ipv4/neigh/' + member['name'] + '/base_reachable_time_ms', 'w') as f: - f.write(str(bridge['arp_cache_timeout_ms'])) - # ignore link state changes - with open('/proc/sys/net/ipv4/conf/' + member['name'] + '/link_filter', 'w') as f: - f.write(str(bridge['disable_link_detect'])) - - # adjust member port stp attributes - member_if = ipdb.interfaces[ member['name'] ] - member_if.begin() # set bridge port cost - member_if.brport_cost = member['cost'] + br.set_cost(member['name'], member['cost']) # set bridge port priority - member_if.brport_priority = member['priority'] - member_if.commit() + br.set_priority(member['name'], member['priority']) + + i = Interface(member['name']) + # configure ARP cache timeout + i.arp_cache_tmo = bridge['arp_cache_tmo'] + # ignore link state changes + i.link_detect = bridge['disable_link_detect'] return None -- cgit v1.2.3 From 3dc08ceaf2c9574133f3b4ff94822b65789a2139 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 31 Aug 2019 13:08:14 +0200 Subject: dummy: T1580: migrate from pyroute2 -> vyos.ifconfig --- src/conf_mode/interface-dummy.py | 33 +++++++-------------------------- 1 file changed, 7 insertions(+), 26 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interface-dummy.py b/src/conf_mode/interface-dummy.py index d8a36a5b2..1f76d0638 100755 --- a/src/conf_mode/interface-dummy.py +++ b/src/conf_mode/interface-dummy.py @@ -19,8 +19,8 @@ from os import environ from copy import deepcopy from sys import exit -from pyroute2 import IPDB from vyos.config import Config +from vyos.ifconfig import DummyIf from vyos import ConfigError default_config_data = { @@ -83,45 +83,26 @@ def generate(dummy): return None def apply(dummy): - ipdb = IPDB(mode='explicit') - dummyif = dummy['intf'] + du = DummyIf(dummy['intf']) # Remove dummy interface if dummy['deleted']: - try: - # delete dummy interface - with ipdb.interface[ dummyif ] as du: - du.remove() - except: - pass + du.remove() else: - try: - # create dummy interface if it's non existing - ipdb.create(kind='dummy', ifname=dummyif).commit() - except: - pass - - # retrieve handle to dummy interface - du = ipdb.interfaces[dummyif] - # begin a transaction prior to make any change - du.begin() # enable interface - du.up() + du.state = 'up' # update interface description used e.g. within SNMP du.ifalias = dummy['description'] # Configure interface address(es) for addr in dummy['address_remove']: - du.del_ip(addr) + du.del_addr(addr) for addr in dummy['address']: - du.add_ip(addr) + du.add_addr(addr) # disable interface on demand if dummy['disable']: - du.down() - - # commit changes on bridge interface - du.commit() + du.state = 'down' return None -- cgit v1.2.3 From e461fcf77c8b071a26e5c57021890bb96ccb7504 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 31 Aug 2019 13:08:45 +0200 Subject: loopback: T1601: migrate from pyroute2 -> vyos.ifconfig --- python/vyos/ifconfig.py | 5 +++++ src/conf_mode/interface-loopback.py | 17 ++++------------- 2 files changed, 9 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 0cd27592a..944c1ef82 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -624,6 +624,11 @@ class Interface: os.remove(self._dhcpv6_lease_file) +class LoopbackIf(Interface): + def __init__(self, ifname=None): + super().__init__(ifname, type='loopback') + + class DummyIf(Interface): def __init__(self, ifname=None): super().__init__(ifname, type='dummy') diff --git a/src/conf_mode/interface-loopback.py b/src/conf_mode/interface-loopback.py index 5c1419b11..1dd68c039 100755 --- a/src/conf_mode/interface-loopback.py +++ b/src/conf_mode/interface-loopback.py @@ -18,7 +18,7 @@ from os import environ from sys import exit from copy import deepcopy -from pyroute2 import IPDB +from vyos.ifconfig import LoopbackIf from vyos.config import Config from vyos import ConfigError @@ -75,28 +75,19 @@ def generate(loopback): return None def apply(loopback): - ipdb = IPDB(mode='explicit') - lo_if = loopback['intf'] - - # the loopback device always exists - lo = ipdb.interfaces[lo_if] - # begin() a transaction prior to make any change - lo.begin() - + lo = LoopbackIf(loopback['intf']) if not loopback['deleted']: # update interface description used e.g. within SNMP # update interface description used e.g. within SNMP lo.ifalias = loopback['description'] # configure interface address(es) for addr in loopback['address']: - lo.add_ip(addr) + lo.add_addr(addr) # remove interface address(es) for addr in loopback['address_remove']: - lo.del_ip(addr) + lo.del_addr(addr) - # commit changes on loopback interface - lo.commit() return None if __name__ == '__main__': -- cgit v1.2.3 From aa5054e524a4996ba9cc11deec25fdf4279b29cf Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 31 Aug 2019 13:18:11 +0200 Subject: dummy: T1580: support deleting interface description --- src/conf_mode/interface-dummy.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interface-dummy.py b/src/conf_mode/interface-dummy.py index 1f76d0638..03afdc668 100755 --- a/src/conf_mode/interface-dummy.py +++ b/src/conf_mode/interface-dummy.py @@ -61,8 +61,6 @@ def get_config(): # retrieve interface description if conf.exists('description'): dummy['description'] = conf.return_value('description') - else: - dummy['description'] = dummy['intf'] # Disable this interface if conf.exists('disable'): -- cgit v1.2.3 From c306c248ff2ebdbd2f669e8c052a6ecd85a50fad Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 31 Aug 2019 13:18:15 +0200 Subject: loopback: T1601: support deleting interface description --- src/conf_mode/interface-loopback.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interface-loopback.py b/src/conf_mode/interface-loopback.py index 1dd68c039..be47324c1 100755 --- a/src/conf_mode/interface-loopback.py +++ b/src/conf_mode/interface-loopback.py @@ -57,8 +57,6 @@ def get_config(): # retrieve interface description if conf.exists('description'): loopback['description'] = conf.return_value('description') - else: - loopback['description'] = loopback['intf'] # Determine interface addresses (currently effective) - to determine which # address is no longer valid and needs to be removed from the interface -- cgit v1.2.3 From 86d1291ec5323e921a0f995211e223bf1f7dcdff Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Sat, 31 Aug 2019 17:25:07 -0500 Subject: [boot-config-loader] T1622: Add failsafe and back trace --- python/vyos/configsession.py | 3 + src/helpers/vyos-boot-config-loader.py | 101 +++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100755 src/helpers/vyos-boot-config-loader.py (limited to 'src') diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index 8626839f2..acbdd3d5f 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -24,6 +24,7 @@ COMMENT = '/opt/vyatta/sbin/my_comment' COMMIT = '/opt/vyatta/sbin/my_commit' DISCARD = '/opt/vyatta/sbin/my_discard' SHOW_CONFIG = ['/bin/cli-shell-api', 'showConfig'] +LOAD_CONFIG = ['/bin/cli-shell-api', 'loadFile'] # Default "commit via" string APP = "vyos-http-api" @@ -155,3 +156,5 @@ class ConfigSession(object): if format == 'raw': return config_data + def load_config(self, file_path): + self.__run_command(LOAD_CONFIG + [file_path]) diff --git a/src/helpers/vyos-boot-config-loader.py b/src/helpers/vyos-boot-config-loader.py new file mode 100755 index 000000000..06c95765f --- /dev/null +++ b/src/helpers/vyos-boot-config-loader.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# + +import os +import sys +import subprocess +import traceback + +from vyos.configsession import ConfigSession, ConfigSessionError +from vyos.configtree import ConfigTree + +STATUS_FILE = '/tmp/vyos-config-status' +TRACE_FILE = '/tmp/boot-config-trace' + +session = ConfigSession(os.getpid(), 'vyos-boot-config-loader') +env = session.get_session_env() + +default_file_name = env['vyatta_sysconfdir'] + '/config.boot.default' + +if len(sys.argv) < 1: + print("Must be called with argument.") + sys.exit(1) +else: + file_name = sys.argv[1] + +def write_config_status(status): + with open(STATUS_FILE, 'w') as f: + f.write('{0}\n'.format(status)) + +def trace_to_file(trace_file_name): + with open(trace_file_name, 'w') as trace_file: + traceback.print_exc(file=trace_file) + +def failsafe(): + try: + with open(default_file_name, 'r') as f: + config_file = f.read() + except Exception as e: + print("Catastrophic: no default config file " + "'{0}'".format(default_file_name)) + sys.exit(1) + + config = ConfigTree(config_file) + if not config.exists(['system', 'login', 'user', 'vyos', + 'authentication', 'encrypted-password']): + print("No password entry in default config file;") + print("unable to recover password for user 'vyos'.") + sys.exit(1) + else: + passwd = config.return_value(['system', 'login', 'user', 'vyos', + 'authentication', + 'encrypted-password']) + + cmd = ("useradd -s /bin/bash -G 'users,sudo' -m -N -p '{0}' " + "vyos".format(passwd)) + try: + subprocess.check_call(cmd, shell=True) + except subprocess.CalledProcessError as e: + sys.exit("{0}".format(e)) + + with open('/etc/motd', 'a+') as f: + f.write('\n\n') + f.write('!!!!!\n') + f.write('There were errors loading the initial configuration;\n') + f.write('please examine the errors in {0} and correct.' + '\n'.format(TRACE_FILE)) + f.write('!!!!!\n\n') + +try: + with open(file_name, 'r') as f: + config_file = f.read() +except Exception as e: + write_config_status(1) + failsafe() + trace_to_file(TRACE_FILE) + sys.exit("{0}".format(e)) + +try: + session.load_config(file_name) + session.commit() + write_config_status(0) +except ConfigSessionError as e: + write_config_status(1) + failsafe() + trace_to_file(TRACE_FILE) + sys.exit(1) -- cgit v1.2.3 From 650293e0e02442803801896128f52624495c319b Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 1 Sep 2019 12:01:52 +0200 Subject: bridge: T1615: support deleting interface description --- src/conf_mode/interface-bridge.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index 187be677e..7bc457680 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -82,8 +82,6 @@ def get_config(): # retrieve interface description if conf.exists('description'): bridge['description'] = conf.return_value('description') - else: - bridge['description'] = bridge['intf'] # Disable this bridge interface if conf.exists('disable'): -- cgit v1.2.3 From 81add62632bcdd02a96f6ec2a4bbb533865d68ee Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 1 Sep 2019 12:18:30 +0200 Subject: bridge: T1615: remove is_ip import from vyos.validate --- src/conf_mode/interface-bridge.py | 1 - 1 file changed, 1 deletion(-) (limited to 'src') diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index 7bc457680..6e48a1382 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -22,7 +22,6 @@ from copy import deepcopy from sys import exit from netifaces import interfaces from vyos.config import Config -from vyos.validate import is_ip from vyos.ifconfig import BridgeIf, Interface from vyos import ConfigError -- cgit v1.2.3 From 6205c4d6701bda5f8a859291a5e152e009252301 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 3 Sep 2019 13:52:33 +0200 Subject: bonding: T1614: Initial version in new style XML/Python interface The node 'interfaces ethernet eth0 bond-group' has been changed and de-nested. Bond members are now configured in the bond interface itself. set interfaces bonding bond0 member interface eth0 --- Makefile | 1 + interface-definitions/interfaces-bonding.xml | 673 +++++++++++++++++++++++++++ src/conf_mode/interface-bonding.py | 409 ++++++++++++++++ src/migration-scripts/interfaces/1-to-2 | 44 ++ 4 files changed, 1127 insertions(+) create mode 100644 interface-definitions/interfaces-bonding.xml create mode 100755 src/conf_mode/interface-bonding.py create mode 100755 src/migration-scripts/interfaces/1-to-2 (limited to 'src') diff --git a/Makefile b/Makefile index 186e63678..03b4712fc 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,7 @@ interface_definitions: rm -f $(TMPL_DIR)/firewall/node.def rm -f $(TMPL_DIR)/interfaces/node.def rm -f $(TMPL_DIR)/interfaces/bridge/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/bonding/node.tag/ip/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/interface-definitions/interfaces-bonding.xml b/interface-definitions/interfaces-bonding.xml new file mode 100644 index 000000000..d4c3bb704 --- /dev/null +++ b/interface-definitions/interfaces-bonding.xml @@ -0,0 +1,673 @@ + + + + + + + Bonding interface name + 315 + + bond[0-9]+$ + + Bonding interface must be named bondN + + bondN + Bonding interface name + + + + + + IP address + + dhcp dhcpv6 + + + ipv4net + IPv4 address and prefix length + + + ipv6net + IPv6 address and prefix length + + + dhcp + Dynamic Host Configuration Protocol + + + dhcpv6 + Dynamic Host Configuration Protocol for IPv6 + + + + (dhcp|dhcpv6) + + + + + + + ARP link monitoring parameters + + + + + ARP link monitoring interval + + 0-4294967295 + Specifies the ARP link monitoring frequency in milliseconds + + + + + + + + + IP address used for ARP monitoring + + ipv4 + Network Time Protocol (NTP) IPv4 address + + + + + + + + + + + + Interface description + + ^.{1,256}$ + + Interface description too long (limit 256 characters) + + + + + DHCP options + + + + + DHCP client identifier + + + + + DHCP client host name (overrides the system host name) + + + + + + + DHCPv6 options + 319 + + + + + Acquire only config parameters, no address + + + + + + IPv6 "temporary" address + + + + + + + + Ignore link state changes + + + + + + Disable this bridge interface + + + + + + Bonding transmit hash policy + + layer2 layer2+3 layer3+4 + + + layer2 + use MAC addresses to generate the hash (802.3ad, default) + + + layer2+3 + combine MAC address and IP address to make hash + + + layer3+4 + combine IP address and port to make hash + + + (layer2\\+3|layer3\\+4|layer2) + + hash-policy must be layer2 layer2+3 or layer3+4 + + + + + + + ARP cache entry timeout in seconds + + 1-86400 + ARP cache entry timout in seconds (default 30) + + + + + Bridge max aging value must be between 6 and 86400 seconds + + + + + Enable proxy-arp on this interface + + + + + + Enable private VLAN proxy ARP on this interface + + + + + + + + Media Access Control (MAC) address + + h:h:h:h:h:h + Hardware (MAC) address + + + + + + + + + Bonding mode + + 802.3ad active-backup broadcast round-robin transmit-load-balance adaptive-load-balance xor-hash + + + 802.3ad + IEEE 802.3ad Dynamic link aggregation (Default) + + + active-backup + Fault tolerant: only one slave in the bond is active + + + broadcast + Fault tolerant: transmits everything on all slave interfaces + + + round-robin + Load balance: transmit packets in sequential order + + + transmit-load-balance + Load balance: adapts based on transmit load and speed + + + adaptive-load-balance + Load balance: adapts based on transmit and receive plus ARP + + + xor-hash + Distribute based on MAC address + + + (802.3ad|active-backup|broadcast|round-robin|transmit-load-balance|adaptive-load-balance|xor-hash) + + mode must be 802.3ad, active-backup, broadcast, round-robin, transmit-load-balance, adaptive-load-balance, or xor + + + + + Bridge member interfaces + + + + + Member interface name + + + + + + + + + + + Maximum Transmission Unit (MTU) + + 68-9000 + Maximum Transmission Unit + + + + + MTU must be between 68 and 9000 + + + + + Primary device interface + + + + + + + + QinQ TAG-S Virtual Local Area Network (VLAN) ID + + + + VLAN ID must be between 0 and 4094 + + + + + IP address + + dhcp dhcpv6 + + + ipv4net + IPv4 address and prefix length + + + ipv6net + IPv6 address and prefix length + + + dhcp + Dynamic Host Configuration Protocol + + + dhcpv6 + Dynamic Host Configuration Protocol for IPv6 + + + + (dhcp|dhcpv6) + + + + + + + Interface description + + ^.{1,256}$ + + Interface description too long (limit 256 characters) + + + + + DHCP options + + + + + DHCP client identifier + + + + + DHCP client host name (overrides the system host name) + + + + + + + DHCPv6 options + 319 + + + + + Acquire only config parameters, no address + + + + + + IPv6 "temporary" address + + + + + + + + Ignore link state changes + + + + + + Disable this bridge interface + + + + + + Set Ethertype + + 0x88A8 0x8100 + + + 0x88A8 + 802.1ad + + + 0x8100 + 802.1q + + + (0x88A8|0x8100) + + Ethertype must be 0x88A8 or 0x8100 + + + + + Media Access Control (MAC) address + + h:h:h:h:h:h + Hardware (MAC) address + + + + + + + + + Maximum Transmission Unit (MTU) + + 68-9000 + Maximum Transmission Unit + + + + + MTU must be between 68 and 9000 + + + + + QinQ TAG-C Virtual Local Area Network (VLAN) ID + + + + VLAN ID must be between 0 and 4094 + + + + + IP address + + dhcp dhcpv6 + + + ipv4net + IPv4 address and prefix length + + + ipv6net + IPv6 address and prefix length + + + dhcp + Dynamic Host Configuration Protocol + + + dhcpv6 + Dynamic Host Configuration Protocol for IPv6 + + + + (dhcp|dhcpv6) + + + + + + + Interface description + + ^.{1,256}$ + + Interface description too long (limit 256 characters) + + + + + DHCP options + + + + + DHCP client identifier + + + + + DHCP client host name (overrides the system host name) + + + + + + + DHCPv6 options + 319 + + + + + Acquire only config parameters, no address + + + + + + IPv6 "temporary" address + + + + + + + + Ignore link state changes + + + + + + Disable this bridge interface + + + + + + Media Access Control (MAC) address + + h:h:h:h:h:h + Hardware (MAC) address + + + + + + + + + Maximum Transmission Unit (MTU) + + 68-9000 + Maximum Transmission Unit + + + + + MTU must be between 68 and 9000 + + + + + + + + + Virtual Local Area Network (VLAN) ID + + + + VLAN ID must be between 0 and 4094 + + + + + IP address + + dhcp dhcpv6 + + + ipv4net + IPv4 address and prefix length + + + ipv6net + IPv6 address and prefix length + + + dhcp + Dynamic Host Configuration Protocol + + + dhcpv6 + Dynamic Host Configuration Protocol for IPv6 + + + + (dhcp|dhcpv6) + + + + + + + Interface description + + ^.{1,256}$ + + Interface description too long (limit 256 characters) + + + + + DHCP options + + + + + DHCP client identifier + + + + + DHCP client host name (overrides the system host name) + + + + + + + DHCPv6 options + 319 + + + + + Acquire only config parameters, no address + + + + + + IPv6 "temporary" address + + + + + + + + Ignore link state changes + + + + + + Disable this bridge interface + + + + + + Media Access Control (MAC) address + + h:h:h:h:h:h + Hardware (MAC) address + + + + + + + + + Maximum Transmission Unit (MTU) + + 68-9000 + Maximum Transmission Unit + + + + + MTU must be between 68 and 9000 + + + + + + + + + diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py new file mode 100755 index 000000000..6cd8fdc56 --- /dev/null +++ b/src/conf_mode/interface-bonding.py @@ -0,0 +1,409 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# + +import os + +from copy import deepcopy +from sys import exit +from netifaces import interfaces +from vyos.ifconfig import BondIf +from vyos.config import Config +from vyos import ConfigError + +default_config_data = { + 'address': [], + 'address_remove': [], + 'arp_mon_intvl': 0, + 'arp_mon_tgt': [], + 'description': '', + 'deleted': False, + 'dhcp_client_id': '', + 'dhcp_hostname': '', + 'dhcpv6_prm_only': False, + 'dhcpv6_temporary': False, + 'disable': False, + 'disable_link_detect': 1, + 'hash_policy': 'layer2', + 'ip_arp_cache_tmo': 30, + 'ip_proxy_arp': 0, + 'ip_proxy_arp_pvlan': 0, + 'intf': '', + 'mac': '', + 'mode': '802.3ad', + 'member': [], + 'member_remove': [], + 'mtu': 1500, + 'primary': '', + 'vif_s': [], + 'vif': [] +} + +def diff(first, second): + second = set(second) + return [item for item in first if item not in second] + +def get_bond_mode(mode): + if mode == 'round-robin': + return 'balance-rr' + elif mode == 'active-backup': + return 'active-backup' + elif mode == 'xor-hash': + return 'balance-xor' + elif mode == 'broadcast': + return 'broadcast' + elif mode == '802.3ad': + return '802.3ad' + elif mode == 'transmit-load-balance': + return 'balance-tlb' + elif mode == 'adaptive-load-balance': + return 'balance-alb' + else: + raise ConfigError('invalid bond mode "{}"'.format(mode)) + +def vlan_to_dict(conf): + """ + Common used function which will extract VLAN related information from config + and represent the result as Python dictionary. + + Function call's itself recursively if a vif-s/vif-c pair is detected. + """ + vlan = { + 'id': conf.get_level().split(' ')[-1], # get the '100' in 'interfaces bonding bond0 vif-s 100' + 'address': [], + 'address_remove': [], + 'description': '', + 'dhcp_client_id': '', + 'dhcp_hostname': '', + 'dhcpv6_prm_only': False, + 'dhcpv6_temporary': False, + 'disable': False, + 'disable_link_detect': 1, + 'mac': '', + 'mtu': 1500 + } + # retrieve configured interface addresses + if conf.exists('address'): + vlan['address'] = conf.return_values('address') + + # Determine interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed from the bond + eff_addr = conf.return_effective_values('address') + act_addr = conf.return_values('address') + vlan['address_remove'] = diff(eff_addr, act_addr) + + # retrieve interface description + if conf.exists('description'): + vlan['description'] = conf.return_value('description') + + # get DHCP client identifier + if conf.exists('dhcp-options client-id'): + vlan['dhcp_client_id'] = conf.return_value('dhcp-options client-id') + + # DHCP client host name (overrides the system host name) + if conf.exists('dhcp-options host-name'): + vlan['dhcp_hostname'] = conf.return_value('dhcp-options host-name') + + # DHCPv6 only acquire config parameters, no address + if conf.exists('dhcpv6-options parameters-only'): + vlan['dhcpv6_prm_only'] = conf.return_value('dhcpv6-options parameters-only') + + # DHCPv6 temporary IPv6 address + if conf.exists('dhcpv6-options temporary'): + vlan['dhcpv6_temporary'] = conf.return_value('dhcpv6-options temporary') + + # ignore link state changes + if conf.exists('disable-link-detect'): + vlan['disable_link_detect'] = 2 + + # disable bond interface + if conf.exists('disable'): + vlan['disable'] = True + + # ethertype (only on vif-s nodes) + if conf.exists('ethertype'): + vlan['ethertype'] = conf.return_value('ethertype') + + # Media Access Control (MAC) address + if conf.exists('mac'): + vlan['mac'] = conf.return_value('mac') + + # Maximum Transmission Unit (MTU) + if conf.exists('mtu'): + vlan['mtu'] = int(conf.return_value('mtu')) + + # check if there is a Q-in-Q vlan customer interface + # and call this function recursively + if conf.exists('vif-c'): + cfg_level = conf.get_level() + # add new key (vif-c) to dictionary + vlan['vif-c'] = [] + for vif in conf.list_nodes('vif-c'): + # set config level to vif interface + conf.set_level(cfg_level + ' vif-c ' + vif) + vlan['vif-c'].append(vlan_to_dict(conf)) + + return vlan + +def get_config(): + # initialize kernel module if not loaded + if not os.path.isfile('/sys/class/net/bonding_masters'): + import syslog + syslog.syslog(syslog.LOG_NOTICE, "loading bonding kernel module") + if os.system('modprobe bonding max_bonds=0 miimon=250') != 0: + syslog.syslog(syslog.LOG_NOTICE, "failed loading bonding kernel module") + raise ConfigError("failed loading bonding kernel module") + + bond = deepcopy(default_config_data) + conf = Config() + + # determine tagNode instance + try: + bond['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + except KeyError as E: + print("Interface not specified") + + # check if bond has been removed + cfg_base = 'interfaces bonding ' + bond['intf'] + if not conf.exists(cfg_base): + bond['deleted'] = True + return bond + + # set new configuration level + conf.set_level(cfg_base) + + # retrieve configured interface addresses + if conf.exists('address'): + bond['address'] = conf.return_values('address') + + # ARP link monitoring frequency in milliseconds + if conf.exists('arp-monitor interval'): + bond['arp_mon_intvl'] = conf.return_value('arp-monitor interval') + + # IP address to use for ARP monitoring + if conf.exists('arp-monitor target'): + bond['arp_mon_tgt'] = conf.return_values('arp-monitor target') + + # retrieve interface description + if conf.exists('description'): + bond['description'] = conf.return_value('description') + else: + bond['description'] = bond['intf'] + + # get DHCP client identifier + if conf.exists('dhcp-options client-id'): + bond['dhcp_client_id'] = conf.return_value('dhcp-options client-id') + + # DHCP client host name (overrides the system host name) + if conf.exists('dhcp-options host-name'): + bond['dhcp_hostname'] = conf.return_value('dhcp-options host-name') + + # DHCPv6 only acquire config parameters, no address + if conf.exists('dhcpv6-options parameters-only'): + bond['dhcpv6_prm_only'] = conf.return_value('dhcpv6-options parameters-only') + + # DHCPv6 temporary IPv6 address + if conf.exists('dhcpv6-options temporary'): + bond['dhcpv6_temporary'] = conf.return_value('dhcpv6-options temporary') + + # ignore link state changes + if conf.exists('disable-link-detect'): + bond['disable_link_detect'] = 2 + + # disable bond interface + if conf.exists('disable'): + bond['disable'] = True + + # Bonding transmit hash policy + if conf.exists('hash-policy'): + bond['hash_policy'] = conf.return_value('hash-policy') + + # ARP cache entry timeout in seconds + if conf.exists('ip arp-cache-timeout'): + bond['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) + + # Enable proxy-arp on this interface + if conf.exists('ip enable-proxy-arp'): + bond['ip_proxy_arp'] = 1 + + # Enable private VLAN proxy ARP on this interface + if conf.exists('ip proxy-arp-pvlan'): + bond['ip_proxy_arp_pvlan'] = 1 + + # Media Access Control (MAC) address + if conf.exists('mac'): + bond['mac'] = conf.return_value('mac') + + # Bonding mode + if conf.exists('mode'): + bond['mode'] = get_bond_mode(conf.return_value('mode')) + + # Maximum Transmission Unit (MTU) + if conf.exists('mtu'): + bond['mtu'] = int(conf.return_value('mtu')) + + # determine bond member interfaces (currently configured) + if conf.exists('member interface'): + bond['member'] = conf.return_values('member interface') + + # Determine bond member interface (currently effective) - to determine which + # interfaces is no longer assigend to the bond and thus can be removed + eff_intf = conf.return_effective_values('member interface') + act_intf = conf.return_values('member interface') + bond['member_remove'] = diff(eff_intf, act_intf) + + # Determine interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed from the bond + eff_addr = conf.return_effective_values('address') + act_addr = conf.return_values('address') + bond['address_remove'] = diff(eff_addr, act_addr) + + # Primary device interface + if conf.exists('primary'): + bond['primary'] = conf.return_value('primary') + + # re-set configuration level and retrieve vif-s interfaces + conf.set_level(cfg_base) + if conf.exists('vif-s'): + for vif_s in conf.list_nodes('vif-s'): + # set config level to vif-s interface + conf.set_level(cfg_base + ' vif-s ' + vif_s) + bond['vif_s'].append(vlan_to_dict(conf)) + + # re-set configuration level and retrieve vif-s interfaces + conf.set_level(cfg_base) + if conf.exists('vif'): + for vif in conf.list_nodes('vif'): + # set config level to vif interface + conf.set_level(cfg_base + ' vif ' + vif) + bond['vif'].append(vlan_to_dict(conf)) + + return bond + +def verify(bond): + if len (bond['arp_mon_tgt']) > 16: + raise ConfigError('The maximum number of targets that can be specified is 16') + + if bond['primary']: + if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: + raise ConfigError('Mode dependency failed, primary not supported in this mode.'.format()) + + return None + +def generate(bond): + return None + +def apply(bond): + b = BondIf(bond['intf']) + + if bond['deleted']: + # delete bonding interface + b.remove() + else: + # Some parameters can not be changed when the bond is up. + # Always disable the bond prior changing anything + b.state = 'down' + + # Configure interface address(es) + for addr in bond['address_remove']: + b.del_addr(addr) + for addr in bond['address']: + b.add_addr(addr) + + # ARP link monitoring frequency + b.arp_interval = bond['arp_mon_intvl'] + # reset miimon on arp-montior deletion + if bond['arp_mon_intvl'] == 0: + # reset miimon to default + b.bond_miimon = 250 + + # ARP monitor targets need to be synchronized between sysfs and CLI. + # Unfortunately an address can't be send twice to sysfs as this will + # result in the following exception: OSError: [Errno 22] Invalid argument. + # + # We remove ALL adresses prior adding new ones, this will remove addresses + # added manually by the user too - but as we are limited to 16 adresses + # from the kernel side this looks valid to me. We won't run into an error + # when a user added manual adresses which would result in having more + # then 16 adresses in total. + cur_addr = list(map(str, b.arp_ip_target.split())) + for addr in cur_addr: + b.arp_ip_target = '-' + addr + + # Add configured ARP target addresses + for addr in bond['arp_mon_tgt']: + b.arp_ip_target = '+' + addr + + # update interface description used e.g. within SNMP + b.ifalias = bond['description'] + + # + # missing DHCP/DHCPv6 options go here + # + + # ignore link state changes + b.link_detect = bond['disable_link_detect'] + # Bonding transmit hash policy + b.xmit_hash_policy = bond['hash_policy'] + # configure ARP cache timeout in milliseconds + b.arp_cache_tmp = bond['ip_arp_cache_tmo'] + # Enable proxy-arp on this interface + b.proxy_arp = bond['ip_proxy_arp'] + # Enable private VLAN proxy ARP on this interface + b.proxy_arp_pvlan = bond['ip_proxy_arp_pvlan'] + + # Change interface MAC address + if bond['mac']: + b.mac = bond['mac'] + + # The bonding mode can not be changed when there are interfaces enslaved + # to this bond, thus we will free all interfaces from the bond first! + for intf in b.get_slaves(): + b.del_port(intf) + + # Bonding policy + b.mode = bond['mode'] + # Maximum Transmission Unit (MTU) + b.mtu = bond['mtu'] + # Primary device interface + b.primary = bond['primary'] + + # + # VLAN config goes here + # + + # Add (enslave) interfaces to bond + for intf in bond['member']: + b.add_port(intf) + + # As the bond interface is always disabled first when changing + # parameters we will only re-enable the interface if it is not + # administratively disabled + if not bond['disable']: + b.state = 'up' + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/migration-scripts/interfaces/1-to-2 b/src/migration-scripts/interfaces/1-to-2 new file mode 100755 index 000000000..10d542d1d --- /dev/null +++ b/src/migration-scripts/interfaces/1-to-2 @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 + +# Change syntax of bond interface +# - move interface based bond-group to actual bond (de-nest) +# https://phabricator.vyos.net/T1614 + +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() + +config = ConfigTree(config_file) +base = ['interfaces', 'bonding'] + +if not config.exists(base): + # Nothing to do + sys.exit(0) +else: + # + # move interface based bond-group to actual bond (de-nest) + # + for intf in config.list_nodes(['interfaces', 'ethernet']): + # check if bond-group exists + if config.exists(['interfaces', 'ethernet', intf, 'bond-group']): + # get configured bond interface + bond = config.return_value(['interfaces', 'ethernet', intf, 'bond-group']) + # delete old interface asigned (nested) bond group + config.delete(['interfaces', 'ethernet', intf, 'bond-group']) + # create new bond member interface + config.set(base + [bond, 'member', 'interface'], value=intf, replace=False) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) -- cgit v1.2.3 From 3fad8f2f74224502018c5e53b5931251595ad3b7 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 3 Sep 2019 14:56:21 +0200 Subject: bonding: T1614: can not set primary interface when it's not part of the bond --- src/conf_mode/interface-bonding.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src') diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index 6cd8fdc56..b150578e3 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -302,6 +302,9 @@ def verify(bond): if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: raise ConfigError('Mode dependency failed, primary not supported in this mode.'.format()) + if bond['primary'] not in bond['member']: + raise ConfigError('Interface "{}" is not part of the bond'.format(bond['primary'])) + return None def generate(bond): -- cgit v1.2.3 From 029085250c4e2dea8a98f3031cf6b8037ffdd534 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 3 Sep 2019 17:14:11 +0200 Subject: bonding: T1614: remove obsolete 'member_remove' dict-key --- src/conf_mode/interface-bonding.py | 7 ------- 1 file changed, 7 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index b150578e3..a623ca524 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -46,7 +46,6 @@ default_config_data = { 'mac': '', 'mode': '802.3ad', 'member': [], - 'member_remove': [], 'mtu': 1500, 'primary': '', 'vif_s': [], @@ -260,12 +259,6 @@ def get_config(): if conf.exists('member interface'): bond['member'] = conf.return_values('member interface') - # Determine bond member interface (currently effective) - to determine which - # interfaces is no longer assigend to the bond and thus can be removed - eff_intf = conf.return_effective_values('member interface') - act_intf = conf.return_values('member interface') - bond['member_remove'] = diff(eff_intf, act_intf) - # Determine interface addresses (currently effective) - to determine which # address is no longer valid and needs to be removed from the bond eff_addr = conf.return_effective_values('address') -- cgit v1.2.3 From 1fc4c201d7771ac4164e9480d55a654b7674d098 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 3 Sep 2019 18:58:27 +0200 Subject: bonding: T1614: T1557: add vif/vif-s VLAN interface support Support for vif-c interfaces is still missing --- python/vyos/ifconfig.py | 47 ++++++++++++++++- src/conf_mode/interface-bonding.py | 101 +++++++++++++++++++++++++++++++++---- 2 files changed, 136 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 1d03e4e74..bb2d23d0d 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -87,7 +87,9 @@ class Interface: def remove(self): """ - Remove system interface + Remove interface from operating system. Removing the interface + deconfigures all assigned IP addresses and clear possible DHCP(v6) + client processes. Example: >>> from vyos.ifconfig import Interface @@ -964,10 +966,53 @@ class BridgeIf(Interface): .format(self._ifname, interface), priority) + class EthernetIf(Interface): def __init__(self, ifname, type=None): super().__init__(ifname, type) + def add_vlan(self, vlan_id, ethertype=''): + """ + A virtual LAN (VLAN) is any broadcast domain that is partitioned and + isolated in a computer network at the data link layer (OSI layer 2). + Use this function to create a new VLAN interface on a given physical + interface. + + This function creates both 802.1q and 802.1ad (Q-in-Q) interfaces. Proto + parameter is used to indicate VLAN type. + + A new object of type EthernetIf is returned once the interface has been + created. + """ + vlan_ifname = self._ifname + '.' + str(vlan_id) + if not os.path.exists('/sys/class/net/{}'.format(vlan_ifname)): + self._vlan_id = int(vlan_id) + + if ethertype: + self._ethertype = ethertype + ethertype='proto {}'.format(ethertype) + + # create interface in the system + cmd = 'ip link add link {intf} name {intf}.{vlan} type vlan {proto} id {vlan}'.format(intf=self._ifname, vlan=self._vlan_id, proto=ethertype) + self._cmd(cmd) + + # return new object mapping to the newly created interface + # we can now work on this object for e.g. IP address setting + # or interface description and so on + return EthernetIf(vlan_ifname) + + + def del_vlan(self, vlan_id): + """ + Remove VLAN interface from operating system. Removing the interface + deconfigures all assigned IP addresses and clear possible DHCP(v6) + client processes. + """ + vlan_ifname = self._ifname + '.' + str(vlan_id) + tmp = EthernetIf(vlan_ifname) + tmp.remove() + + class BondIf(EthernetIf): """ The Linux bonding driver provides a method for aggregating multiple network diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index a623ca524..9e7be19f5 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -21,7 +21,8 @@ import os from copy import deepcopy from sys import exit from netifaces import interfaces -from vyos.ifconfig import BondIf + +from vyos.ifconfig import BondIf, EthernetIf from vyos.config import Config from vyos import ConfigError @@ -49,7 +50,9 @@ default_config_data = { 'mtu': 1500, 'primary': '', 'vif_s': [], - 'vif': [] + 'vif_s_remove': [], + 'vif': [], + 'vif_remove': [] } def diff(first, second): @@ -74,6 +77,15 @@ def get_bond_mode(mode): else: raise ConfigError('invalid bond mode "{}"'.format(mode)) +def get_ethertype(ethertype_val): + if ethertype_val == '0x88A8': + return '802.1ad' + elif ethertype_val == '0x8100': + return '802.1q' + else: + raise ConfigError('invalid ethertype "{}"'.format(ethertype_val)) + + def vlan_to_dict(conf): """ Common used function which will extract VLAN related information from config @@ -82,7 +94,7 @@ def vlan_to_dict(conf): Function call's itself recursively if a vif-s/vif-c pair is detected. """ vlan = { - 'id': conf.get_level().split(' ')[-1], # get the '100' in 'interfaces bonding bond0 vif-s 100' + 'id': conf.get_level().split()[-1], # get the '100' in 'interfaces bonding bond0 vif-s 100' 'address': [], 'address_remove': [], 'description': '', @@ -133,9 +145,14 @@ def vlan_to_dict(conf): if conf.exists('disable'): vlan['disable'] = True - # ethertype (only on vif-s nodes) - if conf.exists('ethertype'): - vlan['ethertype'] = conf.return_value('ethertype') + # ethertype is mandatory on vif-s nodes and only exists here! + # check if this is a vif-s node at all: + if conf.get_level().split()[-2] == 'vif-s': + # ethertype uses a default of 0x88A8 + tmp = '0x88A8' + if conf.exists('ethertype'): + tmp = conf.return_value('ethertype') + vlan['ethertype'] = get_ethertype(tmp) # Media Access Control (MAC) address if conf.exists('mac'): @@ -158,6 +175,39 @@ def vlan_to_dict(conf): return vlan + +def apply_vlan_config(vlan, config): + """ + Generic function to apply a VLAN configuration from a dictionary + to a VLAN interface + """ + + if type(vlan) != type(EthernetIf("lo")): + raise TypeError() + + # Configure interface address(es) + for addr in config['address_remove']: + vlan.del_addr(addr) + for addr in config['address']: + vlan.add_addr(addr) + + # update interface description used e.g. within SNMP + vlan.ifalias = config['description'] + # ignore link state changes + vlan.link_detect = config['disable_link_detect'] + # Maximum Transmission Unit (MTU) + vlan.mtu = config['mtu'] + # Change VLAN interface MAC address + if config['mac']: + vlan.mac = config['mac'] + + # enable/disable VLAN interface + if config['disable']: + vlan.state = 'down' + else: + vlan.state = 'up' + + def get_config(): # initialize kernel module if not loaded if not os.path.isfile('/sys/class/net/bonding_masters'): @@ -271,6 +321,12 @@ def get_config(): # re-set configuration level and retrieve vif-s interfaces conf.set_level(cfg_base) + # Determine vif-s interfaces (currently effective) - to determine which + # vif-s interface is no longer present and needs to be removed + eff_intf = conf.list_effective_nodes('vif-s') + act_intf = conf.list_nodes('vif-s') + bond['vif_s_remove'] = diff(eff_intf, act_intf) + if conf.exists('vif-s'): for vif_s in conf.list_nodes('vif-s'): # set config level to vif-s interface @@ -279,6 +335,12 @@ def get_config(): # re-set configuration level and retrieve vif-s interfaces conf.set_level(cfg_base) + # Determine vif interfaces (currently effective) - to determine which + # vif interface is no longer present and needs to be removed + eff_intf = conf.list_effective_nodes('vif') + act_intf = conf.list_nodes('vif') + bond['vif_remove'] = diff(eff_intf, act_intf) + if conf.exists('vif'): for vif in conf.list_nodes('vif'): # set config level to vif interface @@ -287,6 +349,7 @@ def get_config(): return bond + def verify(bond): if len (bond['arp_mon_tgt']) > 16: raise ConfigError('The maximum number of targets that can be specified is 16') @@ -300,9 +363,11 @@ def verify(bond): return None + def generate(bond): return None + def apply(bond): b = BondIf(bond['intf']) @@ -378,13 +443,27 @@ def apply(bond): # Primary device interface b.primary = bond['primary'] - # - # VLAN config goes here - # - # Add (enslave) interfaces to bond for intf in bond['member']: - b.add_port(intf) + b.add_port(intf) + + # remove no longer required service VLAN interfaces (vif-s) + for vif_s in bond['vif_s_remove']: + b.del_vlan(vif_s) + + # create service VLAN interfaces (vif-s) + for vif_s in bond['vif_s']: + vlan = b.add_vlan(vif_s['id'], ethertype=vif_s['ethertype']) + apply_vlan_config(vlan, vif_s) + + # remove no longer required VLAN interfaces (vif) + for vif in bond['vif_remove']: + b.del_vlan(vif) + + # create VLAN interfaces (vif) + for vif in bond['vif']: + vlan = b.add_vlan(vif['id']) + apply_vlan_config(vlan, vif) # As the bond interface is always disabled first when changing # parameters we will only re-enable the interface if it is not -- cgit v1.2.3 From da2bb10b8e4c15aaf993fc82eb0f9b8c564a2113 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 3 Sep 2019 21:55:49 +0200 Subject: bonding: T1614: identical ID on vif and vif-s is not allowed --- src/conf_mode/interface-bonding.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src') diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index 9e7be19f5..c2f0086aa 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -361,6 +361,11 @@ def verify(bond): if bond['primary'] not in bond['member']: raise ConfigError('Interface "{}" is not part of the bond'.format(bond['primary'])) + for vif_s in bond['vif_s']: + for vif in bond['vif']: + if vif['id'] == vif_s['id']: + raise ConfigError('Can not use identical ID on vif and vif-s interface') + return None -- cgit v1.2.3 From 0d156a3947981e71774359b4566ac2af5892abe9 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 3 Sep 2019 22:23:38 +0200 Subject: bonding: T1614: add vif-c VLAN interface support Tested using: ============= set interfaces bonding bond0 address 192.0.2.1/24 set interfaces bonding bond0 description "VyOS bonding" set interfaces bonding bond0 disable-link-detect set interfaces bonding bond0 hash-policy layer2+3 set interfaces bonding bond0 ip arp-cache-timeout 86400 set interfaces bonding bond0 mac 00:91:00:00:00:01 set interfaces bonding bond0 mode active-backup set interfaces bonding bond0 mtu 9000 set interfaces bonding bond0 member interface eth1 set interfaces bonding bond0 member interface eth2 set interfaces bonding bond0 vif-s 100 address 192.168.10.1/24 set interfaces bonding bond0 vif-s 100 description "802.1ad service VLAN 100" set interfaces bonding bond0 vif-s 100 mtu 1500 set interfaces bonding bond0 vif-s 100 mac 00:91:00:00:00:02 set interfaces bonding bond0 vif-s 100 vif-c 110 address "192.168.110.1/24" set interfaces bonding bond0 vif-s 100 vif-c 110 description "client VLAN 110" set interfaces bonding bond0 vif-s 100 vif-c 120 address "192.168.120.1/24" set interfaces bonding bond0 vif-s 100 vif-c 120 description "client VLAN 120" set interfaces bonding bond0 vif-s 100 vif-c 130 address "192.168.130.1/24" set interfaces bonding bond0 vif-s 100 vif-c 130 description "client VLAN 130" set interfaces bonding bond0 vif 400 address 192.168.40.1/24 set interfaces bonding bond0 vif 400 description "802.1q VLAN 400" set interfaces bonding bond0 vif 400 mtu 1500 set interfaces bonding bond0 vif 400 mac 00:91:00:00:00:03 --- src/conf_mode/interface-bonding.py | 65 ++++++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 23 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index c2f0086aa..b01018e5b 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -145,33 +145,41 @@ def vlan_to_dict(conf): if conf.exists('disable'): vlan['disable'] = True + # Media Access Control (MAC) address + if conf.exists('mac'): + vlan['mac'] = conf.return_value('mac') + + # Maximum Transmission Unit (MTU) + if conf.exists('mtu'): + vlan['mtu'] = int(conf.return_value('mtu')) + # ethertype is mandatory on vif-s nodes and only exists here! # check if this is a vif-s node at all: if conf.get_level().split()[-2] == 'vif-s': + vlan['vif_c'] = [] + vlan['vif_c_remove'] = [] + # ethertype uses a default of 0x88A8 tmp = '0x88A8' if conf.exists('ethertype'): tmp = conf.return_value('ethertype') vlan['ethertype'] = get_ethertype(tmp) - # Media Access Control (MAC) address - if conf.exists('mac'): - vlan['mac'] = conf.return_value('mac') - - # Maximum Transmission Unit (MTU) - if conf.exists('mtu'): - vlan['mtu'] = int(conf.return_value('mtu')) - - # check if there is a Q-in-Q vlan customer interface - # and call this function recursively - if conf.exists('vif-c'): - cfg_level = conf.get_level() - # add new key (vif-c) to dictionary - vlan['vif-c'] = [] - for vif in conf.list_nodes('vif-c'): - # set config level to vif interface - conf.set_level(cfg_level + ' vif-c ' + vif) - vlan['vif-c'].append(vlan_to_dict(conf)) + # get vif-c interfaces (currently effective) - to determine which vif-c + # interface is no longer present and needs to be removed + eff_intf = conf.list_effective_nodes('vif-c') + act_intf = conf.list_nodes('vif-c') + vlan['vif_c_remove'] = diff(eff_intf, act_intf) + + # check if there is a Q-in-Q vlan customer interface + # and call this function recursively + if conf.exists('vif-c'): + cfg_level = conf.get_level() + # add new key (vif-c) to dictionary + for vif in conf.list_nodes('vif-c'): + # set config level to vif interface + conf.set_level(cfg_level + ' vif-c ' + vif) + vlan['vif_c'].append(vlan_to_dict(conf)) return vlan @@ -309,7 +317,7 @@ def get_config(): if conf.exists('member interface'): bond['member'] = conf.return_values('member interface') - # Determine interface addresses (currently effective) - to determine which + # get interface addresses (currently effective) - to determine which # address is no longer valid and needs to be removed from the bond eff_addr = conf.return_effective_values('address') act_addr = conf.return_values('address') @@ -321,8 +329,8 @@ def get_config(): # re-set configuration level and retrieve vif-s interfaces conf.set_level(cfg_base) - # Determine vif-s interfaces (currently effective) - to determine which - # vif-s interface is no longer present and needs to be removed + # get vif-s interfaces (currently effective) - to determine which vif-s + # interface is no longer present and needs to be removed eff_intf = conf.list_effective_nodes('vif-s') act_intf = conf.list_nodes('vif-s') bond['vif_s_remove'] = diff(eff_intf, act_intf) @@ -458,8 +466,19 @@ def apply(bond): # create service VLAN interfaces (vif-s) for vif_s in bond['vif_s']: - vlan = b.add_vlan(vif_s['id'], ethertype=vif_s['ethertype']) - apply_vlan_config(vlan, vif_s) + s_vlan = b.add_vlan(vif_s['id'], ethertype=vif_s['ethertype']) + apply_vlan_config(s_vlan, vif_s) + + # remove no longer required client VLAN interfaces (vif-c) + # on lower service VLAN interface + for vif_c in vif_s['vif_c_remove']: + s_vlan.del_vlan(vif_c) + + # create client VLAN interfaces (vif-c) + # on lower service VLAN interface + for vif_c in vif_s['vif_c']: + c_vlan = s_vlan.add_vlan(vif_c['id']) + apply_vlan_config(c_vlan, vif_c) # remove no longer required VLAN interfaces (vif) for vif in bond['vif_remove']: -- cgit v1.2.3 From 9e1cbaa5f8943221caf4ce4fd3bebc3fec8dc49a Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 4 Sep 2019 16:03:15 +0200 Subject: [service https] T1443: create /etc/vyos if it doesn't exist. --- src/conf_mode/http-api.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src') diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py index 1f91ac582..9c062f0aa 100755 --- a/src/conf_mode/http-api.py +++ b/src/conf_mode/http-api.py @@ -69,6 +69,9 @@ def generate(http_api): if http_api is None: return None + if not os.path.exists('/etc/vyos'): + os.mkdir('/etc/vyos') + with open(config_file, 'w') as f: json.dump(http_api, f, indent=2) -- cgit v1.2.3 From 07ebd7589a961aaa8d4f3099836dd94e4bce2379 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Sep 2019 15:52:39 +0200 Subject: bonding: T1614: T532: new commit validators As in the past during the priority race of the bash script invalid configuration could appear in the CLI and are de-synced from the kernle state, e.g. some bonding modes do not support arp_interval. This is no longer allowed and added to the migration script so that the config again represents the truth. --- src/conf_mode/interface-bonding.py | 53 ++++++++++++++++++++++++++++----- src/migration-scripts/interfaces/1-to-2 | 19 ++++++++++++ 2 files changed, 65 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index b01018e5b..03d28954d 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -249,7 +249,7 @@ def get_config(): # ARP link monitoring frequency in milliseconds if conf.exists('arp-monitor interval'): - bond['arp_mon_intvl'] = conf.return_value('arp-monitor interval') + bond['arp_mon_intvl'] = int(conf.return_value('arp-monitor interval')) # IP address to use for ARP monitoring if conf.exists('arp-monitor target'): @@ -374,6 +374,43 @@ def verify(bond): if vif['id'] == vif_s['id']: raise ConfigError('Can not use identical ID on vif and vif-s interface') + + conf = Config() + for intf in bond['member']: + # we can not add disabled slave interfaces to our bond + if conf.exists('interfaces ethernet ' + intf + ' disable'): + raise ConfigError('can not add disabled interface {} to {}'.format(intf, bond['intf'])) + + # can not add interfaces with an assigned address to a bond + if conf.exists('interfaces ethernet ' + intf + ' address'): + raise ConfigError('can not add interface {} with an assigned address to {}'.format(intf, bond['intf'])) + + # bond members are not allowed to be bridge members, too + for bridge in conf.list_nodes('interfaces bridge'): + if conf.exists('interfaces bridge ' + bridge + ' member interface ' + intf): + raise ConfigError('can not add interface {} that is part of bridge {} to {}'.format(intf, bridge, bond['intf'])) + + # bond members are not allowed to be vrrp members, too + for vrrp in conf.list_nodes('high-availability vrrp group'): + if conf.exists('high-availability vrrp group ' + vrrp + ' interface ' + intf): + raise ConfigError('can not add interface {} which belongs to a VRRP group to {}'.format(intf, bond['intf'])) + + # bond members are not allowed to be underlaying psuedo-ethernet devices + for peth in conf.list_nodes('interfaces pseudo-ethernet'): + if conf.exists('interfaces pseudo-ethernet ' + peth + ' link ' + intf): + raise ConfigError('can not add interface {} used by pseudo-ethernet {} to {}'.format(intf, peth, bond['intf'])) + + if bond['primary']: + if bond['primary'] not in bond['member']: + raise ConfigError('primary interface must be a member interface of {}'.format(bond['intf'])) + + if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: + raise ConfigError('primary interface only works for mode active-backup, transmit-load-balance or adaptive-load-balance') + + if bond['arp_mon_intvl'] > 0: + if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']: + raise ConfigError('ARP link monitoring does not work for mode 802.3ad, transmit-load-balance or adaptive-load-balance') + return None @@ -392,6 +429,11 @@ def apply(bond): # Always disable the bond prior changing anything b.state = 'down' + # The bonding mode can not be changed when there are interfaces enslaved + # to this bond, thus we will free all interfaces from the bond first! + for intf in b.get_slaves(): + b.del_port(intf) + # Configure interface address(es) for addr in bond['address_remove']: b.del_addr(addr) @@ -444,17 +486,14 @@ def apply(bond): if bond['mac']: b.mac = bond['mac'] - # The bonding mode can not be changed when there are interfaces enslaved - # to this bond, thus we will free all interfaces from the bond first! - for intf in b.get_slaves(): - b.del_port(intf) - # Bonding policy b.mode = bond['mode'] # Maximum Transmission Unit (MTU) b.mtu = bond['mtu'] + # Primary device interface - b.primary = bond['primary'] + if bond['primary']: + b.primary = bond['primary'] # Add (enslave) interfaces to bond for intf in bond['member']: diff --git a/src/migration-scripts/interfaces/1-to-2 b/src/migration-scripts/interfaces/1-to-2 index 10d542d1d..050137318 100755 --- a/src/migration-scripts/interfaces/1-to-2 +++ b/src/migration-scripts/interfaces/1-to-2 @@ -36,6 +36,25 @@ else: # create new bond member interface config.set(base + [bond, 'member', 'interface'], value=intf, replace=False) + # + # some combinations were allowed in the past from a CLI perspective + # but the kernel overwrote them - remove from CLI to not confuse the users. + # In addition new consitency checks are in place so users can't repeat the + # mistake. One of those nice issues is https://phabricator.vyos.net/T532 + for bond in config.list_nodes(base): + if config.exists(base + [bond, 'arp-monitor', 'interval']) and config.exists(base + [bond, 'mode']): + mode = config.return_value(base + [bond, 'mode']) + if mode in ['802.3ad', 'transmit-load-balance', 'adaptive-load-balance']: + intvl = int(config.return_value(base + [bond, 'arp-monitor', 'interval'])) + if intvl > 0: + # this is not allowed and the linux kernel replies with: + # option arp_interval: mode dependency failed, not supported in mode 802.3ad(4) + # option arp_interval: mode dependency failed, not supported in mode balance-alb(6) + # option arp_interval: mode dependency failed, not supported in mode balance-tlb(5) + # + # so we simply disable arp_interval by setting it to 0 and miimon will take care about the link + config.set(base + [bond, 'arp-monitor', 'interval'], value='0') + try: with open(file_name, 'w') as f: f.write(config.to_string()) -- cgit v1.2.3 From 053f208b2ed9477fc70b770ab7ac884109d9a89a Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Sep 2019 16:22:15 +0200 Subject: bridge: T1615: can not add member interface to bridge if it is also part of a bond --- src/conf_mode/interface-bridge.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'src') diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index 6e48a1382..1d3587114 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -172,6 +172,13 @@ def verify(bridge): if intf['name'] not in interfaces(): raise ConfigError('Can not add non existing interface "{}" to bridge "{}"'.format(intf['name'], bridge['intf'])) + # bridge members are not allowed to be bond members, too + for intf in bridge['member']: + for bond in conf.list_nodes('interfaces bonding'): + if conf.exists('interfaces bonding ' + bond + ' member interface'): + if intf['name'] in conf.return_values('interfaces bonding ' + bond + ' member interface'): + raise ConfigError('Interface {} belongs to bond {}, can not add it to {}'.format(intf['name'], bond, bridge['intf'])) + return None def generate(bridge): -- cgit v1.2.3 From 77e4de232feea979ce55abd60d7dc9352b5bfe86 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 4 Sep 2019 17:46:40 +0200 Subject: [service https] T1443: correct the listen-address option in the script. --- src/conf_mode/https.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index d5aa1f5b3..2e7aeb5a4 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -133,12 +133,12 @@ def get_config(): else: conf.set_level('service https') - if conf.exists('listen-addresses'): + if conf.exists('listen-address'): addrs = {} - for addr in conf.list_nodes('listen-addresses'): + for addr in conf.list_nodes('listen-address'): addrs[addr] = ['_'] - if conf.exists('listen-addresses {0} server-names'.format(addr)): - names = conf.return_values('listen-addresses {0} server-names'.format(addr)) + if conf.exists('listen-address {0} server-names'.format(addr)): + names = conf.return_values('listen-address {0} server-names'.format(addr)) addrs[addr] = names[:] https['listen_addresses'] = addrs -- cgit v1.2.3 From b142ab9f5093e10915cba08852f75a94be0b9ee6 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 4 Sep 2019 18:15:27 +0200 Subject: [service https] T1443: rename "server-names" option to "server-name". --- interface-definitions/https.xml | 2 +- src/conf_mode/https.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/interface-definitions/https.xml b/interface-definitions/https.xml index a3bcacc09..2fb3bf082 100644 --- a/interface-definitions/https.xml +++ b/interface-definitions/https.xml @@ -31,7 +31,7 @@ - + Server names: exact, wildcard, regex, or '_' (any) diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index 2e7aeb5a4..f948063e9 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -137,8 +137,8 @@ def get_config(): addrs = {} for addr in conf.list_nodes('listen-address'): addrs[addr] = ['_'] - if conf.exists('listen-address {0} server-names'.format(addr)): - names = conf.return_values('listen-address {0} server-names'.format(addr)) + if conf.exists('listen-address {0} server-name'.format(addr)): + names = conf.return_values('listen-address {0} server-name'.format(addr)) addrs[addr] = names[:] https['listen_addresses'] = addrs -- cgit v1.2.3 From 8257a5e67679fe6b7b74e8ba4b57547209a281d2 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Sep 2019 20:32:36 +0200 Subject: openvpn: T1617: T1632: support quotes in openvpn-option The following CLI command can be used to add a raw option to OpenVPN which requires quotes: > set interfaces openvpn vtun10 openvpn-option 'push "keepalive 1 10"' The resulting config file will then have the following set: > push "keepalive 1 10" --- src/conf_mode/interface-openvpn.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src') diff --git a/src/conf_mode/interface-openvpn.py b/src/conf_mode/interface-openvpn.py index 4e5915d4e..91679084e 100755 --- a/src/conf_mode/interface-openvpn.py +++ b/src/conf_mode/interface-openvpn.py @@ -806,6 +806,11 @@ def generate(openvpn): tmpl = jinja2.Template(config_tmpl) config_text = tmpl.render(openvpn) + + # we need to support quoting of raw parameters from OpenVPN CLI + # see https://phabricator.vyos.net/T1632 + config_text = config_text.replace(""",'"') + with open(get_config_name(interface), 'w') as f: f.write(config_text) os.chown(get_config_name(interface), uid, gid) -- cgit v1.2.3 From ff34756f534bfc0f09a5ab6db0d36e1bf43546a8 Mon Sep 17 00:00:00 2001 From: hagbard Date: Wed, 4 Sep 2019 12:50:43 -0700 Subject: [wireguard] - T1628: Adopt WireGuard configuration script to new vyos.ifconfig class --- python/vyos/ifconfig.py | 70 ++++++ src/conf_mode/interface-wireguard.py | 447 +++++++++++++---------------------- 2 files changed, 232 insertions(+), 285 deletions(-) (limited to 'src') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 449923f09..ad3a066a8 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -1273,3 +1273,73 @@ class BondIf(EthernetIf): return self._write_sysfs('/sys/class/net/{}/bonding/mode' .format(self._ifname), mode) + +class WireGuardIf(Interface): + """ + Wireguard interface class, contains a comnfig dictionary since + wireguard VPN is being comnfigured via the wg command rather than + writing the config into a file. Otherwise if a pre-shared key is used + (symetric enryption key), it would we exposed within multiple files. + Currently it's only within the config.boot if the config was saved. + + Example: + >>> from vyos.ifconfig import WireGuardIf as wg_if + >>> wg_intfc = wg_if("wg01") + >>> print (wg_intfc.wg_config) + {'private-key': None, 'keepalive': 0, 'endpoint': None, 'port': 0, 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} + >>> wg_intfc.wg_config['keepalive'] = 100 + >>> print (wg_intfc.wg_config) + {'private-key': None, 'keepalive': 100, 'endpoint': None, 'port': 0, 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} + """ + def __init__(self, ifname): + super().__init__(ifname, type='wireguard') + self.wg_config = { + 'port' : 0, + 'private-key' : None, + 'pubkey' : None, + 'psk' : '/dev/null', + 'allowed-ips' : [], + 'fwmark' : 0x00, + 'endpoint' : None, + 'keepalive' : 0 + } + + def wg_update(self): + if not self.wg_config['private-key']: + raise ValueError("private key required") + else: + ### fmask permission check? + pass + + cmd = "wg set {} ".format(self._ifname) + cmd += "listen-port {} ".format(self.wg_config['port']) + cmd += "fwmark {} ".format(str(self.wg_config['fwmark'])) + cmd += "private-key {} ".format(self.wg_config['private-key']) + cmd += "peer {} ".format(self.wg_config['pubkey']) + cmd += " preshared-key {} ".format(self.wg_config['psk']) + cmd += " allowed-ips " + for aip in self.wg_config['allowed-ips']: + if aip != self.wg_config['allowed-ips'][-1]: + cmd += aip + "," + else: + cmd += aip + if self.wg_config['endpoint']: + cmd += " endpoint {}".format(self.wg_config['endpoint']) + cmd += " persistent-keepalive {}".format(self.wg_config['keepalive']) + + self._cmd(cmd) + + ### remove psk since it isn't required anymore and is saved in the cli config only !! + if self.wg_config['psk'] != '/dev/null': + if os.path.exists(self.wg_config['psk']): + os.remove(self.wg_config['psk']) + + """ + Remove a peer of an interface, peers are identified by their public key. + Giving it a readable name is a vyos feature, to remove a peer the pubkey + and the interface is needed, to remove the entry. + """ + def wg_remove_peer(self, peerkey): + cmd = "sudo wg set {0} peer {1} remove".format(self._ifname, str(peerkey)) + self._cmd(cmd) + diff --git a/src/conf_mode/interface-wireguard.py b/src/conf_mode/interface-wireguard.py index 8234fad0b..40356da51 100755 --- a/src/conf_mode/interface-wireguard.py +++ b/src/conf_mode/interface-wireguard.py @@ -24,11 +24,15 @@ import subprocess from vyos.config import Config from vyos import ConfigError +from vyos.ifconfig import WireGuardIf as wg_if + +ifname = str(os.environ['VYOS_TAGNODE_VALUE']) +wg_intfc = wg_if(ifname) dir = r'/config/auth/wireguard' pk = dir + '/private.key' pub = dir + '/public.key' -psk_file = r'/tmp/psk' +psk_file = dir + '/psk' def check_kmod(): if not os.path.exists('/sys/module/wireguard'): @@ -42,92 +46,61 @@ def get_config(): if not c.exists('interfaces wireguard'): return None - c.set_level('interfaces') - intfcs = c.list_nodes('wireguard') - intfcs_eff = c.list_effective_nodes('wireguard') - new_lst = list(set(intfcs) - set(intfcs_eff)) - del_lst = list(set(intfcs_eff) - set(intfcs)) - config_data = { - 'interfaces' : {} + ifname : { + 'addr' : '', + 'descr' : ifname, + 'lport' : None, + 'status' : 'exists', + 'state' : 'enabled', + 'fwmark' : 0x00, + 'mtu' : 1420, + 'peer' : {} + } } - ### setting defaults and determine status of the config - for intfc in intfcs: - cnf = 'wireguard ' + intfc - # default data struct - config_data['interfaces'].update( - { - intfc : { - 'addr' : '', - 'descr' : intfc, ## snmp ifAlias - 'lport' : '', - 'status' : 'exists', - 'state' : 'enabled', - 'fwmark' : 0x00, - 'mtu' : '1420', - 'peer' : {} - } - } - ) - - ### determine status either delete or create - for i in new_lst: - config_data['interfaces'][i]['status'] = 'create' - - for i in del_lst: - config_data['interfaces'].update( - { - i : { - 'status': 'delete' - } - } - ) - - ### based on the status, setup conf values - for intfc in intfcs: - cnf = 'wireguard ' + intfc - if config_data['interfaces'][intfc]['status'] != 'delete': - ### addresses - if c.exists(cnf + ' address'): - config_data['interfaces'][intfc]['addr'] = c.return_values(cnf + ' address') - ### interface up/down - if c.exists(cnf + ' disable'): - config_data['interfaces'][intfc]['state'] = 'disable' - ### listen port - if c.exists(cnf + ' port'): - config_data['interfaces'][intfc]['lport'] = c.return_value(cnf + ' port') - ### fwmark - if c.exists(cnf + ' fwmark'): - config_data['interfaces'][intfc]['fwmark'] = c.return_value(cnf + ' fwmark') - ### description - if c.exists(cnf + ' description'): - config_data['interfaces'][intfc]['descr'] = c.return_value(cnf + ' description') - ### mtu - if c.exists(cnf + ' mtu'): - config_data['interfaces'][intfc]['mtu'] = c.return_value(cnf + ' mtu') - ### peers - if c.exists(cnf + ' peer'): - for p in c.list_nodes(cnf + ' peer'): - if not c.exists(cnf + ' peer ' + p + ' disable'): - config_data['interfaces'][intfc]['peer'].update( - { - p : { - 'allowed-ips' : [], - 'endpoint' : '', - 'pubkey' : '' - } - } - ) - if c.exists(cnf + ' peer ' + p + ' pubkey'): - config_data['interfaces'][intfc]['peer'][p]['pubkey'] = c.return_value(cnf + ' peer ' + p + ' pubkey') - if c.exists(cnf + ' peer ' + p + ' allowed-ips'): - config_data['interfaces'][intfc]['peer'][p]['allowed-ips'] = c.return_values(cnf + ' peer ' + p + ' allowed-ips') - if c.exists(cnf + ' peer ' + p + ' endpoint'): - config_data['interfaces'][intfc]['peer'][p]['endpoint'] = c.return_value(cnf + ' peer ' + p + ' endpoint') - if c.exists(cnf + ' peer ' + p + ' persistent-keepalive'): - config_data['interfaces'][intfc]['peer'][p]['persistent-keepalive'] = c.return_value(cnf + ' peer ' + p + ' persistent-keepalive') - if c.exists(cnf + ' peer ' + p + ' preshared-key'): - config_data['interfaces'][intfc]['peer'][p]['psk'] = c.return_value(cnf + ' peer ' + p + ' preshared-key') + + c.set_level('interfaces wireguard') + if not c.exists_effective(ifname): + config_data[ifname]['status'] = 'create' + + if not c.exists(ifname) and c.exists_effective(ifname): + config_data[ifname]['status'] = 'delete' + + if config_data[ifname]['status'] != 'delete': + if c.exists(ifname + ' address'): + config_data[ifname]['addr'] = c.return_values(ifname + ' address') + if c.exists(ifname + ' disable'): + config_data[ifname]['state'] = 'disable' + if c.exists(ifname + ' port'): + config_data[ifname]['lport'] = c.return_value(ifname + ' port') + if c.exists(ifname + ' fwmark'): + config_data[ifname]['fwmark'] = c.return_value(ifname + ' fwmark') + if c.exists(ifname + ' description'): + config_data[ifname]['descr'] = c.return_value(ifname + ' description') + if c.exists(ifname + ' mtu'): + config_data[ifname]['mtu'] = c.return_value(ifname + ' mtu') + if c.exists(ifname + ' peer'): + for p in c.list_nodes(ifname + ' peer'): + if not c.exists(ifname + ' peer ' + p + ' disable'): + config_data[ifname]['peer'].update( + { + p : { + 'allowed-ips' : [], + 'endpoint' : '', + 'pubkey' : '' + } + } + ) + if c.exists(ifname + ' peer ' + p + ' pubkey'): + config_data[ifname]['peer'][p]['pubkey'] = c.return_value(ifname + ' peer ' + p + ' pubkey') + if c.exists(ifname + ' peer ' + p + ' allowed-ips'): + config_data[ifname]['peer'][p]['allowed-ips'] = c.return_values(ifname + ' peer ' + p + ' allowed-ips') + if c.exists(ifname + ' peer ' + p + ' endpoint'): + config_data[ifname]['peer'][p]['endpoint'] = c.return_value(ifname + ' peer ' + p + ' endpoint') + if c.exists(ifname + ' peer ' + p + ' persistent-keepalive'): + config_data[ifname]['peer'][p]['persistent-keepalive'] = c.return_value(ifname + ' peer ' + p + ' persistent-keepalive') + if c.exists(ifname + ' peer ' + p + ' preshared-key'): + config_data[ifname]['peer'][p]['psk'] = c.return_value(ifname + ' peer ' + p + ' preshared-key') return config_data @@ -135,22 +108,22 @@ def verify(c): if not c: return None - for i in c['interfaces']: - if c['interfaces'][i]['status'] != 'delete': - if not c['interfaces'][i]['addr']: - raise ConfigError("address required for interface " + i) - if not c['interfaces'][i]['peer']: - raise ConfigError("peer required on interface " + i) - - for p in c['interfaces'][i]['peer']: - if not c['interfaces'][i]['peer'][p]['allowed-ips']: - raise ConfigError("allowed-ips required on interface " + i + " for peer " + p) - if not c['interfaces'][i]['peer'][p]['pubkey']: - raise ConfigError("pubkey from your peer is mandatory on " + i + " for peer " + p) + if not os.path.exists(pk): + raise ConfigError("No keys found, generate them by executing: \'run generate wireguard keypair\'") + if c[ifname]['status'] != 'delete': + if not c[ifname]['addr']: + raise ConfigError("ERROR: IP address required") + if not c[ifname]['peer']: + raise ConfigError("ERROR: peer required") + for p in c[ifname]['peer']: + if not c[ifname]['peer'][p]['allowed-ips']: + raise ConfigError("ERROR: allowed-ips required for peer " + p) + if not c[ifname]['peer'][p]['pubkey']: + raise ConfigError("peer pubkey required for peer " + p) def apply(c): - ### no wg config left, delete all wireguard devices on the os + ### no wg config left, delete all wireguard devices, if any if not c: net_devs = os.listdir('/sys/class/net/') for dev in net_devs: @@ -162,205 +135,109 @@ def apply(c): subprocess.call(['ip l d dev ' + wg_intf + ' >/dev/null'], shell=True) return None - ### - ## find the diffs between effective config an new config - ### + ### interface removal + if c[ifname]['status'] == 'delete': + sl.syslog(sl.LOG_NOTICE, "removing interface " + ifname) + wg_intfc.remove() + return None + c_eff = Config() c_eff.set_level('interfaces wireguard') - ### link status up/down aka interface disable - - for intf in c['interfaces']: - if not c['interfaces'][intf]['status'] == 'delete': - if c['interfaces'][intf]['state'] == 'disable': - sl.syslog(sl.LOG_NOTICE, "disable interface " + intf) - subprocess.call(['ip l s dev ' + intf + ' down ' + ' &>/dev/null'], shell=True) - else: - sl.syslog(sl.LOG_NOTICE, "enable interface " + intf) - subprocess.call(['ip l s dev ' + intf + ' up ' + ' &>/dev/null'], shell=True) - - ### deletion of a specific interface - for intf in c['interfaces']: - if c['interfaces'][intf]['status'] == 'delete': - sl.syslog(sl.LOG_NOTICE, "removing interface " + intf) - subprocess.call(['ip l d dev ' + intf + ' &>/dev/null'], shell=True) - - ### peer deletion - peer_eff = c_eff.list_effective_nodes( intf + ' peer') - peer_cnf = [] - try: - for p in c['interfaces'][intf]['peer']: - peer_cnf.append(p) - except KeyError: - pass - - peer_rem = list(set(peer_eff) - set(peer_cnf)) - for p in peer_rem: - pkey = c_eff.return_effective_value( intf + ' peer ' + p +' pubkey') - remove_peer(intf, pkey) - - ### peer pubkey update - ### wg identifies peers by its pubky, so we have to remove the peer first - ### it will recreated it then below with the new key from the cli config - for p in peer_eff: - if p in peer_cnf: - ekey = c_eff.return_effective_value( intf + ' peer ' + p +' pubkey') - nkey = c['interfaces'][intf]['peer'][p]['pubkey'] - if nkey != ekey: - sl.syslog(sl.LOG_NOTICE, "peer " + p + ' changed pubkey from ' + ekey + 'to key ' + nkey + ' on interface ' + intf) - remove_peer(intf, ekey) - - ### new config - if c['interfaces'][intf]['status'] == 'create': - if not os.path.exists(pk): - raise ConfigError("No keys found, generate them by executing: \'run generate wireguard keypair\'") - - subprocess.call(['ip l a dev ' + intf + ' type wireguard 2>/dev/null'], shell=True) - for addr in c['interfaces'][intf]['addr']: - add_addr(intf, addr) - - subprocess.call(['ip l set up dev ' + intf + ' mtu ' + c['interfaces'][intf]['mtu'] + ' &>/dev/null'], shell=True) - configure_interface(c, intf) - - ### config updates - if c['interfaces'][intf]['status'] == 'exists': - ### IP address change - addr_eff = c_eff.return_effective_values(intf + ' address') - addr_rem = list(set(addr_eff) - set(c['interfaces'][intf]['addr'])) - addr_add = list(set(c['interfaces'][intf]['addr']) - set(addr_eff)) - - if len(addr_rem) != 0: - for addr in addr_rem: - del_addr(intf, addr) - - if len(addr_add) != 0: - for addr in addr_add: - add_addr(intf, addr) - - ## mtu update - mtu = c['interfaces'][intf]['mtu'] - if mtu != 1420: - sl.syslog(sl.LOG_NOTICE, "setting mtu to " + mtu + " on " + intf) - subprocess.call(['ip l set mtu ' + mtu + ' dev ' + intf + ' &>/dev/null'], shell=True) - - - ### persistent-keepalive - for p in c['interfaces'][intf]['peer']: - val_eff = "" - val = "" - - try: - val = c['interfaces'][intf]['peer'][p]['persistent-keepalive'] - except KeyError: - pass - - if c_eff.exists_effective(intf + ' peer ' + p + ' persistent-keepalive'): - val_eff = c_eff.return_effective_value(intf + ' peer ' + p + ' persistent-keepalive') - - ### disable keepalive - if val_eff and not val: - c['interfaces'][intf]['peer'][p]['persistent-keepalive'] = 0 - - ### set new keepalive value - if not val_eff and val: - c['interfaces'][intf]['peer'][p]['persistent-keepalive'] = val - - ## wg command call - configure_interface(c, intf) - - ### ifalias for snmp from description - if c['interfaces'][intf]['status'] != 'delete': - descr_eff = c_eff.return_effective_value(intf + ' description') - cnf_descr = c['interfaces'][intf]['descr'] - if descr_eff != cnf_descr: - with open('/sys/class/net/' + str(intf) + '/ifalias', 'w') as fh: - fh.write(str(cnf_descr)) - -def configure_interface(c, intf): - for p in c['interfaces'][intf]['peer']: - ## config init for wg call - wg_config = { - 'interface' : intf, - 'port' : 0, - 'private-key' : pk, - 'pubkey' : '', - 'psk' : '/dev/null', - 'allowed-ips' : [], - 'fwmark' : 0x00, - 'endpoint' : None, - 'keepalive' : 0 - } - - ## mandatory settings - wg_config['pubkey'] = c['interfaces'][intf]['peer'][p]['pubkey'] - wg_config['allowed-ips'] = c['interfaces'][intf]['peer'][p]['allowed-ips'] + ## interface state + if c[ifname]['state'] == 'disable': + sl.syslog(sl.LOG_NOTICE, "disable interface " + ifname) + wg_intfc.state = 'down' + else: + if not wg_intfc.state == 'up': + sl.syslog(sl.LOG_NOTICE, "enable interface " + ifname) + wg_intfc.state = 'up' + + ## IP address + if not c_eff.exists_effective(ifname + ' address'): + for ip in c[ifname]['addr']: + wg_intfc.add_addr(ip) + else: + addr_eff = c_eff.return_effective_values(ifname + ' address') + addr_rem = list(set(addr_eff) - set(c[ifname]['addr'])) + addr_add = list(set(c[ifname]['addr']) - set(addr_eff)) + + if len(addr_rem) !=0: + for ip in addr_rem: + sl.syslog(sl.LOG_NOTICE, "remove IP address {0} from {1}".format(ip,ifname)) + wg_intfc.del_addr(ip) + + if len(addr_add) !=0: + for ip in addr_add: + sl.syslog(sl.LOG_NOTICE, "add IP address {0} to {1}".format(ip,ifname)) + wg_intfc.add_addr(ip) + + ## interface MTU + if c[ifname]['mtu'] != 1420: + wg_intfc.mtu = int(c[ifname]['mtu']) + else: + ## default is set to 1420 in config_data + wg_intfc.mtu = int(c[ifname]['mtu']) + + ## ifalias for snmp from description + descr_eff = c_eff.return_effective_value(ifname + ' description') + if descr_eff != c[ifname]['descr']: + wg_intfc.ifalias = str(c[ifname]['descr']) + + ## peer deletion + peer_eff = c_eff.list_effective_nodes(ifname + ' peer') + peer_cnf = [] - ## optional settings - # listen-port - if c['interfaces'][intf]['lport']: - wg_config['port'] = c['interfaces'][intf]['lport'] + try: + for p in c[ifname]['peer']: + peer_cnf.append(p) + except KeyError: + pass + + peer_rem = list(set(peer_eff) - set(peer_cnf)) + for p in peer_rem: + pkey = c_eff.return_effective_value( ifname + ' peer ' + p +' pubkey') + wg_intfc.wg_remove_peer(pkey) + + ## peer key update + for p in peer_eff: + if p in peer_cnf: + ekey = c_eff.return_effective_value( ifname + ' peer ' + p +' pubkey') + nkey = c[ifname]['peer'][p]['pubkey'] + if nkey != ekey: + sl.syslog(sl.LOG_NOTICE, "peer {0} pubkey changed from {1} to {2} on interface {3}".format(p, ekey, nkey, ifname)) + print ("peer {0} pubkey changed from {1} to {2} on interface {3}".format(p, ekey, nkey, ifname)) + wg_intfc.wg_remove_peer(ekey) + + wg_intfc.wg_config['private-key'] = pk + for p in c[ifname]['peer']: + wg_intfc.wg_config['pubkey'] = str(c[ifname]['peer'][p]['pubkey']) + wg_intfc.wg_config['allowed-ips'] = (c[ifname]['peer'][p]['allowed-ips']) + + ## listen-port + if c[ifname]['lport']: + wg_intfc.wg_config['port'] = c[ifname]['lport'] ## fwmark - if c['interfaces'][intf]['fwmark']: - wg_config['fwmark'] = c['interfaces'][intf]['fwmark'] - + if c[ifname]['fwmark']: + wg_intfc.wg_config['fwmark'] = c[ifname]['fwmark'] + ## endpoint - if c['interfaces'][intf]['peer'][p]['endpoint']: - wg_config['endpoint'] = c['interfaces'][intf]['peer'][p]['endpoint'] + if c[ifname]['peer'][p]['endpoint']: + wg_intfc.wg_config['endpoint'] = c[ifname]['peer'][p]['endpoint'] ## persistent-keepalive - if 'persistent-keepalive' in c['interfaces'][intf]['peer'][p]: - wg_config['keepalive'] = c['interfaces'][intf]['peer'][p]['persistent-keepalive'] + if 'persistent-keepalive' in c[ifname]['peer'][p]: + wg_intfc.wg_config['keepalive'] = c[ifname]['peer'][p]['persistent-keepalive'] - ## preshared-key - is only read from a file, it's called via sudo redirection doesn't work either - if 'psk' in c['interfaces'][intf]['peer'][p]: + ## preshared-key - needs to be read from a file + if 'psk' in c[ifname]['peer'][p]: old_umask = os.umask(0o077) - open(psk_file, 'w').write(str(c['interfaces'][intf]['peer'][p]['psk'])) + open(psk_file, 'w').write(str(c[ifname]['peer'][p]['psk'])) os.umask(old_umask) - wg_config['psk'] = psk_file - - ### assemble wg command - cmd = "sudo wg set " + intf - cmd += " listen-port " + str(wg_config['port']) - cmd += " fwmark " + str(wg_config['fwmark']) - cmd += " private-key " + wg_config['private-key'] - cmd += " peer " + wg_config['pubkey'] - cmd += " preshared-key " + wg_config['psk'] - cmd += " allowed-ips " - for ap in wg_config['allowed-ips']: - if ap != wg_config['allowed-ips'][-1]: - cmd += ap + "," - else: - cmd += ap - - if wg_config['endpoint']: - cmd += " endpoint " + wg_config['endpoint'] - - if wg_config['keepalive'] != 0: - cmd += " persistent-keepalive " + wg_config['keepalive'] - else: - cmd += " persistent-keepalive 0" - - sl.syslog(sl.LOG_NOTICE, cmd) - #print (cmd) - subprocess.call([cmd], shell=True) - """ remove psk_file """ - if os.path.exists(psk_file): - os.remove(psk_file) - -def add_addr(intf, addr): - # see https://phabricator.vyos.net/T949 - ret = subprocess.call(['ip a a dev ' + intf + ' ' + addr + ' &>/dev/null'], shell=True) - sl.syslog(sl.LOG_NOTICE, "ip a a dev " + intf + " " + addr) - -def del_addr(intf, addr): - ret = subprocess.call(['ip a d dev ' + intf + ' ' + addr + ' &>/dev/null'], shell=True) - sl.syslog(sl.LOG_NOTICE, "ip a d dev " + intf + " " + addr) - -def remove_peer(intf, peer_key): - cmd = 'sudo wg set ' + str(intf) + ' peer ' + peer_key + ' remove &>/dev/null' - ret = subprocess.call([cmd], shell=True) - sl.syslog(sl.LOG_NOTICE, "peer " + peer_key + " removed from " + intf) + wg_intfc.wg_config['psk'] = psk_file + + wg_intfc.wg_update() if __name__ == '__main__': try: -- cgit v1.2.3 From 64d58eda4c1ebaa9d346d65d606d2a75694467ee Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Sep 2019 21:50:04 +0200 Subject: Python/configdict: add list_diff function to compare two lists A list containing only unique elements not part of the other list is returned. This is usefull to check e.g. which IP addresses need to be removed from the OS. --- python/vyos/configdict.py | 8 ++++++++ src/conf_mode/interface-bonding.py | 11 +++++------ src/conf_mode/interface-bridge.py | 12 +++++------- src/conf_mode/interface-dummy.py | 10 ++++------ src/conf_mode/interface-loopback.py | 7 +++---- 5 files changed, 25 insertions(+), 23 deletions(-) (limited to 'src') diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 157011839..a723c5322 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -78,3 +78,11 @@ def retrieve_config(path_hash, base_path, config): config_hash[k][node] = retrieve_config(inner_hash, path + [node], config) return config_hash + + +def list_diff(first, second): + """ + Diff two dictionaries and return only unique items + """ + second = set(second) + return [item for item in first if item not in second] diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index 03d28954d..ba4577900 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -23,6 +23,7 @@ from sys import exit from netifaces import interfaces from vyos.ifconfig import BondIf, EthernetIf +from vyos.configdict import list_diff from vyos.config import Config from vyos import ConfigError @@ -55,9 +56,6 @@ default_config_data = { 'vif_remove': [] } -def diff(first, second): - second = set(second) - return [item for item in first if item not in second] def get_bond_mode(mode): if mode == 'round-robin': @@ -77,6 +75,7 @@ def get_bond_mode(mode): else: raise ConfigError('invalid bond mode "{}"'.format(mode)) + def get_ethertype(ethertype_val): if ethertype_val == '0x88A8': return '802.1ad' @@ -321,7 +320,7 @@ def get_config(): # address is no longer valid and needs to be removed from the bond eff_addr = conf.return_effective_values('address') act_addr = conf.return_values('address') - bond['address_remove'] = diff(eff_addr, act_addr) + bond['address_remove'] = list_diff(eff_addr, act_addr) # Primary device interface if conf.exists('primary'): @@ -333,7 +332,7 @@ def get_config(): # interface is no longer present and needs to be removed eff_intf = conf.list_effective_nodes('vif-s') act_intf = conf.list_nodes('vif-s') - bond['vif_s_remove'] = diff(eff_intf, act_intf) + bond['vif_s_remove'] = list_diff(eff_intf, act_intf) if conf.exists('vif-s'): for vif_s in conf.list_nodes('vif-s'): @@ -347,7 +346,7 @@ def get_config(): # vif interface is no longer present and needs to be removed eff_intf = conf.list_effective_nodes('vif') act_intf = conf.list_nodes('vif') - bond['vif_remove'] = diff(eff_intf, act_intf) + bond['vif_remove'] = list_diff(eff_intf, act_intf) if conf.exists('vif'): for vif in conf.list_nodes('vif'): diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index 1d3587114..b165428ee 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -21,8 +21,10 @@ import os from copy import deepcopy from sys import exit from netifaces import interfaces -from vyos.config import Config + from vyos.ifconfig import BridgeIf, Interface +from vyos.configdict import list_diff +from vyos.config import Config from vyos import ConfigError default_config_data = { @@ -46,10 +48,6 @@ default_config_data = { 'stp': 0 } -def diff(first, second): - second = set(second) - return [item for item in first if item not in second] - def get_config(): bridge = deepcopy(default_config_data) conf = Config() @@ -137,13 +135,13 @@ def get_config(): # interfaces is no longer assigend to the bridge and thus can be removed eff_intf = conf.list_effective_nodes('member interface') act_intf = conf.list_nodes('member interface') - bridge['member_remove'] = diff(eff_intf, act_intf) + bridge['member_remove'] = list_diff(eff_intf, act_intf) # Determine interface addresses (currently effective) - to determine which # address is no longer valid and needs to be removed from the bridge eff_addr = conf.return_effective_values('address') act_addr = conf.return_values('address') - bridge['address_remove'] = diff(eff_addr, act_addr) + bridge['address_remove'] = list_diff(eff_addr, act_addr) # Priority for this bridge if conf.exists('priority'): diff --git a/src/conf_mode/interface-dummy.py b/src/conf_mode/interface-dummy.py index 03afdc668..4a1179672 100755 --- a/src/conf_mode/interface-dummy.py +++ b/src/conf_mode/interface-dummy.py @@ -19,8 +19,10 @@ from os import environ from copy import deepcopy from sys import exit -from vyos.config import Config + from vyos.ifconfig import DummyIf +from vyos.configdict import list_diff +from vyos.config import Config from vyos import ConfigError default_config_data = { @@ -32,10 +34,6 @@ default_config_data = { 'intf': '' } -def diff(first, second): - second = set(second) - return [item for item in first if item not in second] - def get_config(): dummy = deepcopy(default_config_data) conf = Config() @@ -70,7 +68,7 @@ def get_config(): # address is no longer valid and needs to be removed from the interface eff_addr = conf.return_effective_values('address') act_addr = conf.return_values('address') - dummy['address_remove'] = diff(eff_addr, act_addr) + dummy['address_remove'] = list_diff(eff_addr, act_addr) return dummy diff --git a/src/conf_mode/interface-loopback.py b/src/conf_mode/interface-loopback.py index be47324c1..e2df37655 100755 --- a/src/conf_mode/interface-loopback.py +++ b/src/conf_mode/interface-loopback.py @@ -18,7 +18,9 @@ from os import environ from sys import exit from copy import deepcopy + from vyos.ifconfig import LoopbackIf +from vyos.configdict import list_diff from vyos.config import Config from vyos import ConfigError @@ -29,9 +31,6 @@ default_config_data = { 'description': '', } -def diff(first, second): - second = set(second) - return [item for item in first if item not in second] def get_config(): loopback = deepcopy(default_config_data) @@ -62,7 +61,7 @@ def get_config(): # address is no longer valid and needs to be removed from the interface eff_addr = conf.return_effective_values('address') act_addr = conf.return_values('address') - loopback['address_remove'] = diff(eff_addr, act_addr) + loopback['address_remove'] = list_diff(eff_addr, act_addr) return loopback -- cgit v1.2.3 From 952871200ecee584e7ed1bcb37bdaa06111e3a72 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 4 Sep 2019 21:50:46 +0200 Subject: Python/configdict: add function vlan_to_dict A generic function which can parse the VLAN (vif, vif-s, cif-c) nodes in a config session. A dictionary describing the VLAN is returned. A good example will be the interface-bonding.py script used to generate bond interfaces in the system. It is used as follows: if conf.exists('vif'): for vif in conf.list_nodes('vif'): # set config level to vif interface conf.set_level(cfg_base + ' vif ' + vif) bond['vif'].append(vlan_to_dict(conf)) --- python/vyos/configdict.py | 108 ++++++++++++++++++++++++++++++++++++ src/conf_mode/interface-bonding.py | 109 +------------------------------------ 2 files changed, 109 insertions(+), 108 deletions(-) (limited to 'src') diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index a723c5322..4bc8863bb 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -18,6 +18,7 @@ A library for retrieving value dicts from VyOS configs in a declarative fashion. """ +from vyos import ConfigError def retrieve_config(path_hash, base_path, config): """ @@ -86,3 +87,110 @@ def list_diff(first, second): """ second = set(second) return [item for item in first if item not in second] + + +def get_ethertype(ethertype_val): + if ethertype_val == '0x88A8': + return '802.1ad' + elif ethertype_val == '0x8100': + return '802.1q' + else: + raise ConfigError('invalid ethertype "{}"'.format(ethertype_val)) + + +def vlan_to_dict(conf): + """ + Common used function which will extract VLAN related information from config + and represent the result as Python dictionary. + + Function call's itself recursively if a vif-s/vif-c pair is detected. + """ + vlan = { + 'id': conf.get_level().split()[-1], # get the '100' in 'interfaces bonding bond0 vif-s 100' + 'address': [], + 'address_remove': [], + 'description': '', + 'dhcp_client_id': '', + 'dhcp_hostname': '', + 'dhcpv6_prm_only': False, + 'dhcpv6_temporary': False, + 'disable': False, + 'disable_link_detect': 1, + 'mac': '', + 'mtu': 1500 + } + # retrieve configured interface addresses + if conf.exists('address'): + vlan['address'] = conf.return_values('address') + + # Determine interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed from the bond + eff_addr = conf.return_effective_values('address') + act_addr = conf.return_values('address') + vlan['address_remove'] = list_diff(eff_addr, act_addr) + + # retrieve interface description + if conf.exists('description'): + vlan['description'] = conf.return_value('description') + + # get DHCP client identifier + if conf.exists('dhcp-options client-id'): + vlan['dhcp_client_id'] = conf.return_value('dhcp-options client-id') + + # DHCP client host name (overrides the system host name) + if conf.exists('dhcp-options host-name'): + vlan['dhcp_hostname'] = conf.return_value('dhcp-options host-name') + + # DHCPv6 only acquire config parameters, no address + if conf.exists('dhcpv6-options parameters-only'): + vlan['dhcpv6_prm_only'] = conf.return_value('dhcpv6-options parameters-only') + + # DHCPv6 temporary IPv6 address + if conf.exists('dhcpv6-options temporary'): + vlan['dhcpv6_temporary'] = conf.return_value('dhcpv6-options temporary') + + # ignore link state changes + if conf.exists('disable-link-detect'): + vlan['disable_link_detect'] = 2 + + # disable bond interface + if conf.exists('disable'): + vlan['disable'] = True + + # Media Access Control (MAC) address + if conf.exists('mac'): + vlan['mac'] = conf.return_value('mac') + + # Maximum Transmission Unit (MTU) + if conf.exists('mtu'): + vlan['mtu'] = int(conf.return_value('mtu')) + + # ethertype is mandatory on vif-s nodes and only exists here! + # check if this is a vif-s node at all: + if conf.get_level().split()[-2] == 'vif-s': + vlan['vif_c'] = [] + vlan['vif_c_remove'] = [] + + # ethertype uses a default of 0x88A8 + tmp = '0x88A8' + if conf.exists('ethertype'): + tmp = conf.return_value('ethertype') + vlan['ethertype'] = get_ethertype(tmp) + + # get vif-c interfaces (currently effective) - to determine which vif-c + # interface is no longer present and needs to be removed + eff_intf = conf.list_effective_nodes('vif-c') + act_intf = conf.list_nodes('vif-c') + vlan['vif_c_remove'] = list_diff(eff_intf, act_intf) + + # check if there is a Q-in-Q vlan customer interface + # and call this function recursively + if conf.exists('vif-c'): + cfg_level = conf.get_level() + # add new key (vif-c) to dictionary + for vif in conf.list_nodes('vif-c'): + # set config level to vif interface + conf.set_level(cfg_level + ' vif-c ' + vif) + vlan['vif_c'].append(vlan_to_dict(conf)) + + return vlan diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index ba4577900..2ec764965 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -23,7 +23,7 @@ from sys import exit from netifaces import interfaces from vyos.ifconfig import BondIf, EthernetIf -from vyos.configdict import list_diff +from vyos.configdict import list_diff, vlan_to_dict from vyos.config import Config from vyos import ConfigError @@ -76,113 +76,6 @@ def get_bond_mode(mode): raise ConfigError('invalid bond mode "{}"'.format(mode)) -def get_ethertype(ethertype_val): - if ethertype_val == '0x88A8': - return '802.1ad' - elif ethertype_val == '0x8100': - return '802.1q' - else: - raise ConfigError('invalid ethertype "{}"'.format(ethertype_val)) - - -def vlan_to_dict(conf): - """ - Common used function which will extract VLAN related information from config - and represent the result as Python dictionary. - - Function call's itself recursively if a vif-s/vif-c pair is detected. - """ - vlan = { - 'id': conf.get_level().split()[-1], # get the '100' in 'interfaces bonding bond0 vif-s 100' - 'address': [], - 'address_remove': [], - 'description': '', - 'dhcp_client_id': '', - 'dhcp_hostname': '', - 'dhcpv6_prm_only': False, - 'dhcpv6_temporary': False, - 'disable': False, - 'disable_link_detect': 1, - 'mac': '', - 'mtu': 1500 - } - # retrieve configured interface addresses - if conf.exists('address'): - vlan['address'] = conf.return_values('address') - - # Determine interface addresses (currently effective) - to determine which - # address is no longer valid and needs to be removed from the bond - eff_addr = conf.return_effective_values('address') - act_addr = conf.return_values('address') - vlan['address_remove'] = diff(eff_addr, act_addr) - - # retrieve interface description - if conf.exists('description'): - vlan['description'] = conf.return_value('description') - - # get DHCP client identifier - if conf.exists('dhcp-options client-id'): - vlan['dhcp_client_id'] = conf.return_value('dhcp-options client-id') - - # DHCP client host name (overrides the system host name) - if conf.exists('dhcp-options host-name'): - vlan['dhcp_hostname'] = conf.return_value('dhcp-options host-name') - - # DHCPv6 only acquire config parameters, no address - if conf.exists('dhcpv6-options parameters-only'): - vlan['dhcpv6_prm_only'] = conf.return_value('dhcpv6-options parameters-only') - - # DHCPv6 temporary IPv6 address - if conf.exists('dhcpv6-options temporary'): - vlan['dhcpv6_temporary'] = conf.return_value('dhcpv6-options temporary') - - # ignore link state changes - if conf.exists('disable-link-detect'): - vlan['disable_link_detect'] = 2 - - # disable bond interface - if conf.exists('disable'): - vlan['disable'] = True - - # Media Access Control (MAC) address - if conf.exists('mac'): - vlan['mac'] = conf.return_value('mac') - - # Maximum Transmission Unit (MTU) - if conf.exists('mtu'): - vlan['mtu'] = int(conf.return_value('mtu')) - - # ethertype is mandatory on vif-s nodes and only exists here! - # check if this is a vif-s node at all: - if conf.get_level().split()[-2] == 'vif-s': - vlan['vif_c'] = [] - vlan['vif_c_remove'] = [] - - # ethertype uses a default of 0x88A8 - tmp = '0x88A8' - if conf.exists('ethertype'): - tmp = conf.return_value('ethertype') - vlan['ethertype'] = get_ethertype(tmp) - - # get vif-c interfaces (currently effective) - to determine which vif-c - # interface is no longer present and needs to be removed - eff_intf = conf.list_effective_nodes('vif-c') - act_intf = conf.list_nodes('vif-c') - vlan['vif_c_remove'] = diff(eff_intf, act_intf) - - # check if there is a Q-in-Q vlan customer interface - # and call this function recursively - if conf.exists('vif-c'): - cfg_level = conf.get_level() - # add new key (vif-c) to dictionary - for vif in conf.list_nodes('vif-c'): - # set config level to vif interface - conf.set_level(cfg_level + ' vif-c ' + vif) - vlan['vif_c'].append(vlan_to_dict(conf)) - - return vlan - - def apply_vlan_config(vlan, config): """ Generic function to apply a VLAN configuration from a dictionary -- cgit v1.2.3 From 4778f7d2f771b09df1705cea5c7c00e798a7f776 Mon Sep 17 00:00:00 2001 From: hagbard Date: Wed, 4 Sep 2019 14:37:15 -0700 Subject: [wireguard] - T1628: renaming member functions, removing wg_ prefix --- python/vyos/ifconfig.py | 34 +-- src/conf_mode/interface-wireguard.py | 427 ++++++++++++++++++----------------- 2 files changed, 240 insertions(+), 221 deletions(-) (limited to 'src') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 0bc4eff17..30bfa5735 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -1288,7 +1288,7 @@ class WireGuardIf(Interface): def __init__(self, ifname): super().__init__(ifname, type='wireguard') - self.wg_config = { + self.config = { 'port': 0, 'private-key': None, 'pubkey': None, @@ -1299,36 +1299,36 @@ class WireGuardIf(Interface): 'keepalive': 0 } - def wg_update(self): - if not self.wg_config['private-key']: + def update(self): + if not self.config['private-key']: raise ValueError("private key required") else: # fmask permission check? pass cmd = "wg set {} ".format(self._ifname) - cmd += "listen-port {} ".format(self.wg_config['port']) - cmd += "fwmark {} ".format(str(self.wg_config['fwmark'])) - cmd += "private-key {} ".format(self.wg_config['private-key']) - cmd += "peer {} ".format(self.wg_config['pubkey']) - cmd += " preshared-key {} ".format(self.wg_config['psk']) + cmd += "listen-port {} ".format(self.config['port']) + cmd += "fwmark {} ".format(str(self.config['fwmark'])) + cmd += "private-key {} ".format(self.config['private-key']) + cmd += "peer {} ".format(self.config['pubkey']) + cmd += " preshared-key {} ".format(self.config['psk']) cmd += " allowed-ips " - for aip in self.wg_config['allowed-ips']: - if aip != self.wg_config['allowed-ips'][-1]: + for aip in self.config['allowed-ips']: + if aip != self.config['allowed-ips'][-1]: cmd += aip + "," else: cmd += aip - if self.wg_config['endpoint']: - cmd += " endpoint {}".format(self.wg_config['endpoint']) - cmd += " persistent-keepalive {}".format(self.wg_config['keepalive']) + if self.config['endpoint']: + cmd += " endpoint {}".format(self.config['endpoint']) + cmd += " persistent-keepalive {}".format(self.config['keepalive']) self._cmd(cmd) # remove psk since it isn't required anymore and is saved in the cli # config only !! - if self.wg_config['psk'] != '/dev/null': - if os.path.exists(self.wg_config['psk']): - os.remove(self.wg_config['psk']) + if self.config['psk'] != '/dev/null': + if os.path.exists(self.config['psk']): + os.remove(self.config['psk']) """ Remove a peer of an interface, peers are identified by their public key. @@ -1336,7 +1336,7 @@ class WireGuardIf(Interface): and the interface is needed, to remove the entry. """ - def wg_remove_peer(self, peerkey): + def remove_peer(self, peerkey): cmd = "sudo wg set {0} peer {1} remove".format( self._ifname, str(peerkey)) self._cmd(cmd) diff --git a/src/conf_mode/interface-wireguard.py b/src/conf_mode/interface-wireguard.py index 40356da51..265491993 100755 --- a/src/conf_mode/interface-wireguard.py +++ b/src/conf_mode/interface-wireguard.py @@ -24,227 +24,246 @@ import subprocess from vyos.config import Config from vyos import ConfigError -from vyos.ifconfig import WireGuardIf as wg_if +from vyos.ifconfig import WireGuardIf ifname = str(os.environ['VYOS_TAGNODE_VALUE']) -wg_intfc = wg_if(ifname) +intfc = WireGuardIf(ifname) dir = r'/config/auth/wireguard' pk = dir + '/private.key' pub = dir + '/public.key' psk_file = dir + '/psk' + def check_kmod(): - if not os.path.exists('/sys/module/wireguard'): - sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod") - if os.system('sudo modprobe wireguard') != 0: - sl.syslog(sl.LOG_NOTICE, "modprobe wireguard failed") - raise ConfigError("modprobe wireguard failed") + if not os.path.exists('/sys/module/wireguard'): + sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod") + if os.system('sudo modprobe wireguard') != 0: + sl.syslog(sl.LOG_NOTICE, "modprobe wireguard failed") + raise ConfigError("modprobe wireguard failed") + def get_config(): - c = Config() - if not c.exists('interfaces wireguard'): - return None - - config_data = { - ifname : { - 'addr' : '', - 'descr' : ifname, - 'lport' : None, - 'status' : 'exists', - 'state' : 'enabled', - 'fwmark' : 0x00, - 'mtu' : 1420, - 'peer' : {} - } - } - - c.set_level('interfaces wireguard') - if not c.exists_effective(ifname): - config_data[ifname]['status'] = 'create' - - if not c.exists(ifname) and c.exists_effective(ifname): - config_data[ifname]['status'] = 'delete' - - if config_data[ifname]['status'] != 'delete': - if c.exists(ifname + ' address'): - config_data[ifname]['addr'] = c.return_values(ifname + ' address') - if c.exists(ifname + ' disable'): - config_data[ifname]['state'] = 'disable' - if c.exists(ifname + ' port'): - config_data[ifname]['lport'] = c.return_value(ifname + ' port') - if c.exists(ifname + ' fwmark'): - config_data[ifname]['fwmark'] = c.return_value(ifname + ' fwmark') - if c.exists(ifname + ' description'): - config_data[ifname]['descr'] = c.return_value(ifname + ' description') - if c.exists(ifname + ' mtu'): - config_data[ifname]['mtu'] = c.return_value(ifname + ' mtu') - if c.exists(ifname + ' peer'): - for p in c.list_nodes(ifname + ' peer'): - if not c.exists(ifname + ' peer ' + p + ' disable'): - config_data[ifname]['peer'].update( - { - p : { - 'allowed-ips' : [], - 'endpoint' : '', - 'pubkey' : '' - } - } - ) - if c.exists(ifname + ' peer ' + p + ' pubkey'): - config_data[ifname]['peer'][p]['pubkey'] = c.return_value(ifname + ' peer ' + p + ' pubkey') - if c.exists(ifname + ' peer ' + p + ' allowed-ips'): - config_data[ifname]['peer'][p]['allowed-ips'] = c.return_values(ifname + ' peer ' + p + ' allowed-ips') - if c.exists(ifname + ' peer ' + p + ' endpoint'): - config_data[ifname]['peer'][p]['endpoint'] = c.return_value(ifname + ' peer ' + p + ' endpoint') - if c.exists(ifname + ' peer ' + p + ' persistent-keepalive'): - config_data[ifname]['peer'][p]['persistent-keepalive'] = c.return_value(ifname + ' peer ' + p + ' persistent-keepalive') - if c.exists(ifname + ' peer ' + p + ' preshared-key'): - config_data[ifname]['peer'][p]['psk'] = c.return_value(ifname + ' peer ' + p + ' preshared-key') - - return config_data + c = Config() + if not c.exists('interfaces wireguard'): + return None + + config_data = { + ifname: { + 'addr': '', + 'descr': ifname, + 'lport': None, + 'status': 'exists', + 'state': 'enabled', + 'fwmark': 0x00, + 'mtu': 1420, + 'peer': {} + } + } + + c.set_level('interfaces wireguard') + if not c.exists_effective(ifname): + config_data[ifname]['status'] = 'create' + + if not c.exists(ifname) and c.exists_effective(ifname): + config_data[ifname]['status'] = 'delete' + + if config_data[ifname]['status'] != 'delete': + if c.exists(ifname + ' address'): + config_data[ifname]['addr'] = c.return_values(ifname + ' address') + if c.exists(ifname + ' disable'): + config_data[ifname]['state'] = 'disable' + if c.exists(ifname + ' port'): + config_data[ifname]['lport'] = c.return_value(ifname + ' port') + if c.exists(ifname + ' fwmark'): + config_data[ifname]['fwmark'] = c.return_value(ifname + ' fwmark') + if c.exists(ifname + ' description'): + config_data[ifname]['descr'] = c.return_value( + ifname + ' description') + if c.exists(ifname + ' mtu'): + config_data[ifname]['mtu'] = c.return_value(ifname + ' mtu') + if c.exists(ifname + ' peer'): + for p in c.list_nodes(ifname + ' peer'): + if not c.exists(ifname + ' peer ' + p + ' disable'): + config_data[ifname]['peer'].update( + { + p: { + 'allowed-ips': [], + 'endpoint': '', + 'pubkey': '' + } + } + ) + if c.exists(ifname + ' peer ' + p + ' pubkey'): + config_data[ifname]['peer'][p]['pubkey'] = c.return_value( + ifname + ' peer ' + p + ' pubkey') + if c.exists(ifname + ' peer ' + p + ' allowed-ips'): + config_data[ifname]['peer'][p]['allowed-ips'] = c.return_values( + ifname + ' peer ' + p + ' allowed-ips') + if c.exists(ifname + ' peer ' + p + ' endpoint'): + config_data[ifname]['peer'][p]['endpoint'] = c.return_value( + ifname + ' peer ' + p + ' endpoint') + if c.exists(ifname + ' peer ' + p + ' persistent-keepalive'): + config_data[ifname]['peer'][p]['persistent-keepalive'] = c.return_value( + ifname + ' peer ' + p + ' persistent-keepalive') + if c.exists(ifname + ' peer ' + p + ' preshared-key'): + config_data[ifname]['peer'][p]['psk'] = c.return_value( + ifname + ' peer ' + p + ' preshared-key') + + return config_data + def verify(c): - if not c: - return None + if not c: + return None - if not os.path.exists(pk): - raise ConfigError("No keys found, generate them by executing: \'run generate wireguard keypair\'") + if not os.path.exists(pk): + raise ConfigError( + "No keys found, generate them by executing: \'run generate wireguard keypair\'") + + if c[ifname]['status'] != 'delete': + if not c[ifname]['addr']: + raise ConfigError("ERROR: IP address required") + if not c[ifname]['peer']: + raise ConfigError("ERROR: peer required") + for p in c[ifname]['peer']: + if not c[ifname]['peer'][p]['allowed-ips']: + raise ConfigError("ERROR: allowed-ips required for peer " + p) + if not c[ifname]['peer'][p]['pubkey']: + raise ConfigError("peer pubkey required for peer " + p) - if c[ifname]['status'] != 'delete': - if not c[ifname]['addr']: - raise ConfigError("ERROR: IP address required") - if not c[ifname]['peer']: - raise ConfigError("ERROR: peer required") - for p in c[ifname]['peer']: - if not c[ifname]['peer'][p]['allowed-ips']: - raise ConfigError("ERROR: allowed-ips required for peer " + p) - if not c[ifname]['peer'][p]['pubkey']: - raise ConfigError("peer pubkey required for peer " + p) def apply(c): - ### no wg config left, delete all wireguard devices, if any - if not c: - net_devs = os.listdir('/sys/class/net/') - for dev in net_devs: - if os.path.isdir('/sys/class/net/' + dev): - buf = open('/sys/class/net/' + dev + '/uevent', 'r').read() - if re.search("DEVTYPE=wireguard", buf, re.I|re.M): - wg_intf = re.sub("INTERFACE=", "", re.search("INTERFACE=.*", buf, re.I|re.M).group(0)) - sl.syslog(sl.LOG_NOTICE, "removing interface " + wg_intf) - subprocess.call(['ip l d dev ' + wg_intf + ' >/dev/null'], shell=True) - return None - - ### interface removal - if c[ifname]['status'] == 'delete': - sl.syslog(sl.LOG_NOTICE, "removing interface " + ifname) - wg_intfc.remove() - return None - - c_eff = Config() - c_eff.set_level('interfaces wireguard') - - ## interface state - if c[ifname]['state'] == 'disable': - sl.syslog(sl.LOG_NOTICE, "disable interface " + ifname) - wg_intfc.state = 'down' - else: - if not wg_intfc.state == 'up': - sl.syslog(sl.LOG_NOTICE, "enable interface " + ifname) - wg_intfc.state = 'up' - - ## IP address - if not c_eff.exists_effective(ifname + ' address'): - for ip in c[ifname]['addr']: - wg_intfc.add_addr(ip) - else: - addr_eff = c_eff.return_effective_values(ifname + ' address') - addr_rem = list(set(addr_eff) - set(c[ifname]['addr'])) - addr_add = list(set(c[ifname]['addr']) - set(addr_eff)) - - if len(addr_rem) !=0: - for ip in addr_rem: - sl.syslog(sl.LOG_NOTICE, "remove IP address {0} from {1}".format(ip,ifname)) - wg_intfc.del_addr(ip) - - if len(addr_add) !=0: - for ip in addr_add: - sl.syslog(sl.LOG_NOTICE, "add IP address {0} to {1}".format(ip,ifname)) - wg_intfc.add_addr(ip) - - ## interface MTU - if c[ifname]['mtu'] != 1420: - wg_intfc.mtu = int(c[ifname]['mtu']) - else: - ## default is set to 1420 in config_data - wg_intfc.mtu = int(c[ifname]['mtu']) - - ## ifalias for snmp from description - descr_eff = c_eff.return_effective_value(ifname + ' description') - if descr_eff != c[ifname]['descr']: - wg_intfc.ifalias = str(c[ifname]['descr']) - - ## peer deletion - peer_eff = c_eff.list_effective_nodes(ifname + ' peer') - peer_cnf = [] - - try: + # no wg config left, delete all wireguard devices, if any + if not c: + net_devs = os.listdir('/sys/class/net/') + for dev in net_devs: + if os.path.isdir('/sys/class/net/' + dev): + buf = open('/sys/class/net/' + dev + '/uevent', 'r').read() + if re.search("DEVTYPE=wireguard", buf, re.I | re.M): + wg_intf = re.sub("INTERFACE=", "", re.search( + "INTERFACE=.*", buf, re.I | re.M).group(0)) + sl.syslog(sl.LOG_NOTICE, "removing interface " + wg_intf) + subprocess.call( + ['ip l d dev ' + wg_intf + ' >/dev/null'], shell=True) + return None + + # interface removal + if c[ifname]['status'] == 'delete': + sl.syslog(sl.LOG_NOTICE, "removing interface " + ifname) + intfc.remove() + return None + + c_eff = Config() + c_eff.set_level('interfaces wireguard') + + # interface state + if c[ifname]['state'] == 'disable': + sl.syslog(sl.LOG_NOTICE, "disable interface " + ifname) + intfc.state = 'down' + else: + if not intfc.state == 'up': + sl.syslog(sl.LOG_NOTICE, "enable interface " + ifname) + intfc.state = 'up' + + # IP address + if not c_eff.exists_effective(ifname + ' address'): + for ip in c[ifname]['addr']: + intfc.add_addr(ip) + else: + addr_eff = c_eff.return_effective_values(ifname + ' address') + addr_rem = list(set(addr_eff) - set(c[ifname]['addr'])) + addr_add = list(set(c[ifname]['addr']) - set(addr_eff)) + + if len(addr_rem) != 0: + for ip in addr_rem: + sl.syslog( + sl.LOG_NOTICE, "remove IP address {0} from {1}".format(ip, ifname)) + intfc.del_addr(ip) + + if len(addr_add) != 0: + for ip in addr_add: + sl.syslog( + sl.LOG_NOTICE, "add IP address {0} to {1}".format(ip, ifname)) + intfc.add_addr(ip) + + # interface MTU + if c[ifname]['mtu'] != 1420: + intfc.mtu = int(c[ifname]['mtu']) + else: + # default is set to 1420 in config_data + intfc.mtu = int(c[ifname]['mtu']) + + # ifalias for snmp from description + descr_eff = c_eff.return_effective_value(ifname + ' description') + if descr_eff != c[ifname]['descr']: + intfc.ifalias = str(c[ifname]['descr']) + + # peer deletion + peer_eff = c_eff.list_effective_nodes(ifname + ' peer') + peer_cnf = [] + + try: + for p in c[ifname]['peer']: + peer_cnf.append(p) + except KeyError: + pass + + peer_rem = list(set(peer_eff) - set(peer_cnf)) + for p in peer_rem: + pkey = c_eff.return_effective_value(ifname + ' peer ' + p + ' pubkey') + intfc.remove_peer(pkey) + + # peer key update + for p in peer_eff: + if p in peer_cnf: + ekey = c_eff.return_effective_value( + ifname + ' peer ' + p + ' pubkey') + nkey = c[ifname]['peer'][p]['pubkey'] + if nkey != ekey: + sl.syslog( + sl.LOG_NOTICE, "peer {0} pubkey changed from {1} to {2} on interface {3}".format(p, ekey, nkey, ifname)) + print ( + "peer {0} pubkey changed from {1} to {2} on interface {3}".format(p, ekey, nkey, ifname)) + intfc.remove_peer(ekey) + + intfc.config['private-key'] = pk for p in c[ifname]['peer']: - peer_cnf.append(p) - except KeyError: - pass - - peer_rem = list(set(peer_eff) - set(peer_cnf)) - for p in peer_rem: - pkey = c_eff.return_effective_value( ifname + ' peer ' + p +' pubkey') - wg_intfc.wg_remove_peer(pkey) - - ## peer key update - for p in peer_eff: - if p in peer_cnf: - ekey = c_eff.return_effective_value( ifname + ' peer ' + p +' pubkey') - nkey = c[ifname]['peer'][p]['pubkey'] - if nkey != ekey: - sl.syslog(sl.LOG_NOTICE, "peer {0} pubkey changed from {1} to {2} on interface {3}".format(p, ekey, nkey, ifname)) - print ("peer {0} pubkey changed from {1} to {2} on interface {3}".format(p, ekey, nkey, ifname)) - wg_intfc.wg_remove_peer(ekey) - - wg_intfc.wg_config['private-key'] = pk - for p in c[ifname]['peer']: - wg_intfc.wg_config['pubkey'] = str(c[ifname]['peer'][p]['pubkey']) - wg_intfc.wg_config['allowed-ips'] = (c[ifname]['peer'][p]['allowed-ips']) - - ## listen-port - if c[ifname]['lport']: - wg_intfc.wg_config['port'] = c[ifname]['lport'] - - ## fwmark - if c[ifname]['fwmark']: - wg_intfc.wg_config['fwmark'] = c[ifname]['fwmark'] - - ## endpoint - if c[ifname]['peer'][p]['endpoint']: - wg_intfc.wg_config['endpoint'] = c[ifname]['peer'][p]['endpoint'] - - ## persistent-keepalive - if 'persistent-keepalive' in c[ifname]['peer'][p]: - wg_intfc.wg_config['keepalive'] = c[ifname]['peer'][p]['persistent-keepalive'] - - ## preshared-key - needs to be read from a file - if 'psk' in c[ifname]['peer'][p]: - old_umask = os.umask(0o077) - open(psk_file, 'w').write(str(c[ifname]['peer'][p]['psk'])) - os.umask(old_umask) - wg_intfc.wg_config['psk'] = psk_file - - wg_intfc.wg_update() + intfc.config['pubkey'] = str(c[ifname]['peer'][p]['pubkey']) + intfc.config['allowed-ips'] = (c[ifname]['peer'][p]['allowed-ips']) + + # listen-port + if c[ifname]['lport']: + intfc.config['port'] = c[ifname]['lport'] + + # fwmark + if c[ifname]['fwmark']: + intfc.config['fwmark'] = c[ifname]['fwmark'] + + # endpoint + if c[ifname]['peer'][p]['endpoint']: + intfc.config['endpoint'] = c[ifname]['peer'][p]['endpoint'] + + # persistent-keepalive + if 'persistent-keepalive' in c[ifname]['peer'][p]: + intfc.config['keepalive'] = c[ifname][ + 'peer'][p]['persistent-keepalive'] + + # preshared-key - needs to be read from a file + if 'psk' in c[ifname]['peer'][p]: + old_umask = os.umask(0o077) + open(psk_file, 'w').write(str(c[ifname]['peer'][p]['psk'])) + os.umask(old_umask) + intfc.config['psk'] = psk_file + + intfc.update() if __name__ == '__main__': - try: - check_kmod() - c = get_config() - verify(c) - apply(c) - except ConfigError as e: - print(e) - sys.exit(1) + try: + check_kmod() + c = get_config() + verify(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) -- cgit v1.2.3 From 35c7d66165da2102ee8986c3999fe9fea16c38da Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 6 Sep 2019 10:10:11 +0200 Subject: Python/ifconfig: T1557: {add,del}_addr() now supports dhcp/dhcpv6 Instead of manually starting DHCP/DHCPv6 for every interface and have an identical if/elif/else statement checking for dhcp/dhcpv6 rather move this repeating stement into add_addr()/del_addr(). Single source is always preferred. --- python/vyos/ifconfig.py | 111 ++++++++++++++++++++++++------------- src/conf_mode/interface-bonding.py | 56 ++++++++++--------- src/conf_mode/interface-bridge.py | 35 ++++-------- 3 files changed, 112 insertions(+), 90 deletions(-) (limited to 'src') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index c5d3e447b..1886addfc 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -61,6 +61,7 @@ class Interface: >>> i = Interface('eth0') """ self._ifname = str(ifname) + self._state = 'down' if not os.path.exists('/sys/class/net/{}'.format(ifname)) and not type: raise Exception('interface "{}" not found'.format(self._ifname)) @@ -111,8 +112,8 @@ class Interface: # All subinterfaces are now removed, continue on the physical interface # stop DHCP(v6) if running - self.del_dhcp() - self.del_dhcpv6() + self._del_dhcp() + self._del_dhcpv6() # NOTE (Improvement): # after interface removal no other commands should be allowed @@ -359,6 +360,8 @@ class Interface: if state not in ['up', 'down']: raise ValueError('state must be "up" or "down"') + self._state = state + # Assemble command executed on system. Unfortunately there is no way # to up/down an interface via sysfs cmd = 'ip link set dev {} {}'.format(self._ifname, state) @@ -495,8 +498,14 @@ class Interface: def add_addr(self, addr): """ - Add IP address to interface. Address is only added if it yet not added - to that interface. + Add IP(v6) address to interface. Address is only added if it is not + already assigned to that interface. + + addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6! + IPv4: add IPv4 address to interface + IPv6: add IPv6 address to interface + dhcp: start dhclient (IPv4) on interface + dhcpv6: start dhclient (IPv6) on interface Example: >>> from vyos.ifconfig import Interface @@ -506,13 +515,25 @@ class Interface: >>> j.get_addr() ['192.0.2.1/24', '2001:db8::ffff/64'] """ - if not is_intf_addr_assigned(self._ifname, addr): - cmd = 'ip addr add "{}" dev "{}"'.format(addr, self._ifname) - self._cmd(cmd) + if addr == 'dhcp': + self._set_dhcp() + elif addr == 'dhcpv6': + self._set_dhcpv6() + else: + if not is_intf_addr_assigned(self._ifname, addr): + cmd = 'ip addr add "{}" dev "{}"'.format(addr, self._ifname) + self._cmd(cmd) def del_addr(self, addr): """ - Remove IP address from interface. + Delete IP(v6) address to interface. Address is only added if it is + assigned to that interface. + + addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6! + IPv4: delete IPv4 address from interface + IPv6: delete IPv6 address from interface + dhcp: stop dhclient (IPv4) on interface + dhcpv6: stop dhclient (IPv6) on interface Example: >>> from vyos.ifconfig import Interface @@ -525,12 +546,17 @@ class Interface: >>> j.get_addr() ['2001:db8::ffff/64'] """ - if is_intf_addr_assigned(self._ifname, addr): - cmd = 'ip addr del "{}" dev "{}"'.format(addr, self._ifname) - self._cmd(cmd) + if addr == 'dhcp': + self._del_dhcp() + elif addr == 'dhcpv6': + self._del_dhcpv6() + else: + if is_intf_addr_assigned(self._ifname, addr): + cmd = 'ip addr del "{}" dev "{}"'.format(addr, self._ifname) + self._cmd(cmd) # replace dhcpv4/v6 with systemd.networkd? - def set_dhcp(self): + def _set_dhcp(self): """ Configure interface as DHCP client. The dhclient binary is automatically started in background! @@ -557,15 +583,17 @@ class Interface: with open(self._dhcp_cfg_file, 'w') as f: f.write(dhcp_text) - cmd = 'start-stop-daemon --start --quiet --pidfile ' + \ - self._dhcp_pid_file - cmd += ' --exec /sbin/dhclient --' - # now pass arguments to dhclient binary - cmd += ' -4 -nw -cf {} -pf {} -lf {} {}'.format( - self._dhcp_cfg_file, self._dhcp_pid_file, self._dhcp_lease_file, self._ifname) - self._cmd(cmd) + if self._state == 'up': + cmd = 'start-stop-daemon --start --quiet --pidfile ' + \ + self._dhcp_pid_file + cmd += ' --exec /sbin/dhclient --' + # now pass arguments to dhclient binary + cmd += ' -4 -nw -cf {} -pf {} -lf {} {}'.format( + self._dhcp_cfg_file, self._dhcp_pid_file, self._dhcp_lease_file, self._ifname) + self._cmd(cmd) - def del_dhcp(self): + + def _del_dhcp(self): """ De-configure interface as DHCP clinet. All auto generated files like pid, config and lease will be removed. @@ -601,7 +629,8 @@ class Interface: if os.path.isfile(self._dhcp_lease_file): os.remove(self._dhcp_lease_file) - def set_dhcpv6(self): + + def _set_dhcpv6(self): """ Configure interface as DHCPv6 client. The dhclient binary is automatically started in background! @@ -622,25 +651,28 @@ class Interface: with open(self._dhcpv6_cfg_file, 'w') as f: f.write(dhcpv6_text) - # https://bugs.launchpad.net/ubuntu/+source/ifupdown/+bug/1447715 - # - # wee need to wait for IPv6 DAD to finish once and interface is added - # this suxx :-( - sleep(5) + if self._state == 'up': + # https://bugs.launchpad.net/ubuntu/+source/ifupdown/+bug/1447715 + # + # wee need to wait for IPv6 DAD to finish once and interface is added + # this suxx :-( + sleep(5) - # no longer accept router announcements on this interface - cmd = 'sysctl -q -w net.ipv6.conf.{}.accept_ra=0'.format(self._ifname) - self._cmd(cmd) + # no longer accept router announcements on this interface + cmd = 'sysctl -q -w net.ipv6.conf.{}.accept_ra=0'.format(self._ifname) + self._cmd(cmd) - cmd = 'start-stop-daemon --start --quiet --pidfile ' + \ - self._dhcpv6_pid_file - cmd += ' --exec /sbin/dhclient --' - # now pass arguments to dhclient binary - cmd += ' -6 -nw -cf {} -pf {} -lf {} {}'.format( - self._dhcpv6_cfg_file, self._dhcpv6_pid_file, self._dhcpv6_lease_file, self._ifname) - self._cmd(cmd) + # assemble command-line to start DHCPv6 client (dhclient) + cmd = 'start-stop-daemon --start --quiet --pidfile ' + \ + self._dhcpv6_pid_file + cmd += ' --exec /sbin/dhclient --' + # now pass arguments to dhclient binary + cmd += ' -6 -nw -cf {} -pf {} -lf {} {}'.format( + self._dhcpv6_cfg_file, self._dhcpv6_pid_file, self._dhcpv6_lease_file, self._ifname) + self._cmd(cmd) - def del_dhcpv6(self): + + def _del_dhcpv6(self): """ De-configure interface as DHCPv6 clinet. All auto generated files like pid, config and lease will be removed. @@ -1281,7 +1313,6 @@ class BondIf(EthernetIf): class WireGuardIf(Interface): - """ Wireguard interface class, contains a comnfig dictionary since wireguard VPN is being comnfigured via the wg command rather than @@ -1293,11 +1324,11 @@ class WireGuardIf(Interface): >>> from vyos.ifconfig import WireGuardIf as wg_if >>> wg_intfc = wg_if("wg01") >>> print (wg_intfc.wg_config) - {'private-key': None, 'keepalive': 0, 'endpoint': None, 'port': 0, + {'private-key': None, 'keepalive': 0, 'endpoint': None, 'port': 0, 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} >>> wg_intfc.wg_config['keepalive'] = 100 >>> print (wg_intfc.wg_config) - {'private-key': None, 'keepalive': 100, 'endpoint': None, 'port': 0, + {'private-key': None, 'keepalive': 100, 'endpoint': None, 'port': 0, 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} """ diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index 2ec764965..81667289d 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -85,12 +85,6 @@ def apply_vlan_config(vlan, config): if type(vlan) != type(EthernetIf("lo")): raise TypeError() - # Configure interface address(es) - for addr in config['address_remove']: - vlan.del_addr(addr) - for addr in config['address']: - vlan.add_addr(addr) - # update interface description used e.g. within SNMP vlan.ifalias = config['description'] # ignore link state changes @@ -107,6 +101,14 @@ def apply_vlan_config(vlan, config): else: vlan.state = 'up' + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second + for addr in config['address_remove']: + vlan.del_addr(addr) + for addr in config['address']: + vlan.add_addr(addr) + def get_config(): # initialize kernel module if not loaded @@ -139,6 +141,11 @@ def get_config(): if conf.exists('address'): bond['address'] = conf.return_values('address') + # get interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed from the bond + eff_addr = conf.return_effective_values('address') + bond['address_remove'] = list_diff(eff_addr, bond['address']) + # ARP link monitoring frequency in milliseconds if conf.exists('arp-monitor interval'): bond['arp_mon_intvl'] = int(conf.return_value('arp-monitor interval')) @@ -209,12 +216,6 @@ def get_config(): if conf.exists('member interface'): bond['member'] = conf.return_values('member interface') - # get interface addresses (currently effective) - to determine which - # address is no longer valid and needs to be removed from the bond - eff_addr = conf.return_effective_values('address') - act_addr = conf.return_values('address') - bond['address_remove'] = list_diff(eff_addr, act_addr) - # Primary device interface if conf.exists('primary'): bond['primary'] = conf.return_value('primary') @@ -314,6 +315,7 @@ def apply(bond): b = BondIf(bond['intf']) if bond['deleted']: + # # delete bonding interface b.remove() else: @@ -326,12 +328,6 @@ def apply(bond): for intf in b.get_slaves(): b.del_port(intf) - # Configure interface address(es) - for addr in bond['address_remove']: - b.del_addr(addr) - for addr in bond['address']: - b.add_addr(addr) - # ARP link monitoring frequency b.arp_interval = bond['arp_mon_intvl'] # reset miimon on arp-montior deletion @@ -348,8 +344,8 @@ def apply(bond): # from the kernel side this looks valid to me. We won't run into an error # when a user added manual adresses which would result in having more # then 16 adresses in total. - cur_addr = list(map(str, b.arp_ip_target.split())) - for addr in cur_addr: + arp_tgt_addr = list(map(str, b.arp_ip_target.split())) + for addr in arp_tgt_addr: b.arp_ip_target = '-' + addr # Add configured ARP target addresses @@ -391,6 +387,20 @@ def apply(bond): for intf in bond['member']: b.add_port(intf) + # As the bond interface is always disabled first when changing + # parameters we will only re-enable the interface if it is not + # administratively disabled + if not bond['disable']: + b.state = 'up' + + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second + for addr in bond['address_remove']: + b.del_addr(addr) + for addr in bond['address']: + b.add_addr(addr) + # remove no longer required service VLAN interfaces (vif-s) for vif_s in bond['vif_s_remove']: b.del_vlan(vif_s) @@ -420,12 +430,6 @@ def apply(bond): vlan = b.add_vlan(vif['id']) apply_vlan_config(vlan, vif) - # As the bond interface is always disabled first when changing - # parameters we will only re-enable the interface if it is not - # administratively disabled - if not bond['disable']: - b.state = 'up' - return None if __name__ == '__main__': diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index b165428ee..8996fceec 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -61,9 +61,7 @@ def get_config(): # Check if bridge has been removed if not conf.exists('interfaces bridge ' + bridge['intf']): bridge['deleted'] = True - # we should not bail out early here b/c we should - # find possible DHCP interfaces later on. - # DHCP interfaces invoke dhclient which should be stopped, too + return bridge # set new configuration level conf.set_level('interfaces bridge ' + bridge['intf']) @@ -72,6 +70,11 @@ def get_config(): if conf.exists('address'): bridge['address'] = conf.return_values('address') + # Determine interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed from the bridge + eff_addr = conf.return_effective_values('address') + bridge['address_remove'] = list_diff(eff_addr, bridge['address']) + # retrieve aging - how long addresses are retained if conf.exists('aging'): bridge['aging'] = int(conf.return_value('aging')) @@ -137,12 +140,6 @@ def get_config(): act_intf = conf.list_nodes('member interface') bridge['member_remove'] = list_diff(eff_intf, act_intf) - # Determine interface addresses (currently effective) - to determine which - # address is no longer valid and needs to be removed from the bridge - eff_addr = conf.return_effective_values('address') - act_addr = conf.return_values('address') - bridge['address_remove'] = list_diff(eff_addr, act_addr) - # Priority for this bridge if conf.exists('priority'): bridge['priority'] = int(conf.return_value('priority')) @@ -225,23 +222,13 @@ def apply(bridge): if bridge['disable']: br.state = 'down' - # remove configured network interface addresses/DHCP(v6) configuration + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second for addr in bridge['address_remove']: - if addr == 'dhcp': - br.del_dhcp() - elif addr == 'dhcpv6': - br.del_dhcpv6() - else: - br.del_addr(addr) - - # add configured network interface addresses/DHCP(v6) configuration + br.del_addr(addr) for addr in bridge['address']: - if addr == 'dhcp': - br.set_dhcp() - elif addr == 'dhcpv6': - br.set_dhcpv6() - else: - br.add_addr(addr) + br.add_addr(addr) # configure additional bridge member options for member in bridge['member']: -- cgit v1.2.3 From 96e0f5697b181be4f2f3c5c763821ef574881a93 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 6 Sep 2019 10:41:55 +0200 Subject: bonding: T1614: enslaved interfaces can be added to only one bond at a time --- src/conf_mode/interface-bonding.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'src') diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index 81667289d..7b86a0528 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -270,6 +270,12 @@ def verify(bond): conf = Config() for intf in bond['member']: + # a bond member is only allowed to be assigned to any one bond + for tmp in conf.list_nodes('interfaces bonding'): + if conf.exists('interfaces bonding ' + tmp + ' member interface ' + intf): + raise ConfigError('can not add interface {} that is part of another bond ({}) to {}'.format( + intf, tmp, bond['intf'])) + # we can not add disabled slave interfaces to our bond if conf.exists('interfaces ethernet ' + intf + ' disable'): raise ConfigError('can not add disabled interface {} to {}'.format(intf, bond['intf'])) -- cgit v1.2.3 From f9eff4fcdf30223638169e6ed5fa7ca49fcfa223 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 6 Sep 2019 10:49:02 +0200 Subject: bonding: T1614: reword verify() error messages --- src/conf_mode/interface-bonding.py | 46 +++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 18 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index 7b86a0528..acf088de8 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -257,10 +257,12 @@ def verify(bond): if bond['primary']: if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: - raise ConfigError('Mode dependency failed, primary not supported in this mode.'.format()) + raise ConfigError('Mode dependency failed, primary not supported ' \ + 'in this mode.'.format()) if bond['primary'] not in bond['member']: - raise ConfigError('Interface "{}" is not part of the bond'.format(bond['primary'])) + raise ConfigError('Interface "{}" is not part of the bond' \ + .format(bond['primary'])) for vif_s in bond['vif_s']: for vif in bond['vif']: @@ -273,42 +275,50 @@ def verify(bond): # a bond member is only allowed to be assigned to any one bond for tmp in conf.list_nodes('interfaces bonding'): if conf.exists('interfaces bonding ' + tmp + ' member interface ' + intf): - raise ConfigError('can not add interface {} that is part of another bond ({}) to {}'.format( - intf, tmp, bond['intf'])) + raise ConfigError('can not enslave interface {} which already ' \ + 'belongs to bond {}'.format(intf, tmp)) # we can not add disabled slave interfaces to our bond if conf.exists('interfaces ethernet ' + intf + ' disable'): - raise ConfigError('can not add disabled interface {} to {}'.format(intf, bond['intf'])) + raise ConfigError('can not enslave disabled interface {}' \ + .format(intf)) # can not add interfaces with an assigned address to a bond if conf.exists('interfaces ethernet ' + intf + ' address'): - raise ConfigError('can not add interface {} with an assigned address to {}'.format(intf, bond['intf'])) + raise ConfigError('can not enslave interface {} which has an address ' \ + 'assigned'.format(intf)) # bond members are not allowed to be bridge members, too - for bridge in conf.list_nodes('interfaces bridge'): - if conf.exists('interfaces bridge ' + bridge + ' member interface ' + intf): - raise ConfigError('can not add interface {} that is part of bridge {} to {}'.format(intf, bridge, bond['intf'])) + for tmp in conf.list_nodes('interfaces bridge'): + if conf.exists('interfaces bridge ' + tmp + ' member interface ' + intf): + raise ConfigError('can not enslave interface {} which belongs to ' \ + 'bridge {}'.format(intf, tmp)) # bond members are not allowed to be vrrp members, too - for vrrp in conf.list_nodes('high-availability vrrp group'): - if conf.exists('high-availability vrrp group ' + vrrp + ' interface ' + intf): - raise ConfigError('can not add interface {} which belongs to a VRRP group to {}'.format(intf, bond['intf'])) + for tmp in conf.list_nodes('high-availability vrrp group'): + if conf.exists('high-availability vrrp group ' + tmp + ' interface ' + intf): + raise ConfigError('can not enslave interface {} which belongs to ' \ + 'VRRP group {}'.format(intf, tmp)) # bond members are not allowed to be underlaying psuedo-ethernet devices - for peth in conf.list_nodes('interfaces pseudo-ethernet'): - if conf.exists('interfaces pseudo-ethernet ' + peth + ' link ' + intf): - raise ConfigError('can not add interface {} used by pseudo-ethernet {} to {}'.format(intf, peth, bond['intf'])) + for tmp in conf.list_nodes('interfaces pseudo-ethernet'): + if conf.exists('interfaces pseudo-ethernet ' + tmp + ' link ' + intf): + raise ConfigError('can not enslave interface {} which belongs to ' \ + 'pseudo-ethernet {}'.format(intf, tmp)) if bond['primary']: if bond['primary'] not in bond['member']: - raise ConfigError('primary interface must be a member interface of {}'.format(bond['intf'])) + raise ConfigError('primary interface must be a member interface of {}' \ + .format(bond['intf'])) if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: - raise ConfigError('primary interface only works for mode active-backup, transmit-load-balance or adaptive-load-balance') + raise ConfigError('primary interface only works for mode active-backup, ' \ + 'transmit-load-balance or adaptive-load-balance') if bond['arp_mon_intvl'] > 0: if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']: - raise ConfigError('ARP link monitoring does not work for mode 802.3ad, transmit-load-balance or adaptive-load-balance') + raise ConfigError('ARP link monitoring does not work for mode 802.3ad, ' \ + 'transmit-load-balance or adaptive-load-balance') return None -- cgit v1.2.3 From 5f87266d9ef3b72908b6f429e37df280f1be8cdf Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 6 Sep 2019 10:54:50 +0200 Subject: bonding: T1614: members are not allowed to be underlaying vxlan devices --- src/conf_mode/interface-bonding.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'src') diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index acf088de8..bce8f98fb 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -306,6 +306,13 @@ def verify(bond): raise ConfigError('can not enslave interface {} which belongs to ' \ 'pseudo-ethernet {}'.format(intf, tmp)) + # bond members are not allowed to be underlaying vxlan devices + for tmp in conf.list_nodes('interfaces vxlan'): + if conf.exists('interfaces vxlan ' + tmp + ' link ' + intf): + raise ConfigError('can not enslave interface {} which belongs to ' \ + 'vxlan {}'.format(intf, tmp)) + + if bond['primary']: if bond['primary'] not in bond['member']: raise ConfigError('primary interface must be a member interface of {}' \ -- cgit v1.2.3 From c38097eb62dfe5eb309d90752e3ce611999a48d1 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 6 Sep 2019 12:28:51 +0200 Subject: dummy: loopback: T1580: T1601: synchronize comments --- src/conf_mode/interface-dummy.py | 2 ++ src/conf_mode/interface-loopback.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/conf_mode/interface-dummy.py b/src/conf_mode/interface-dummy.py index 4a1179672..614fe08db 100755 --- a/src/conf_mode/interface-dummy.py +++ b/src/conf_mode/interface-dummy.py @@ -91,6 +91,8 @@ def apply(dummy): du.ifalias = dummy['description'] # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second for addr in dummy['address_remove']: du.del_addr(addr) for addr in dummy['address']: diff --git a/src/conf_mode/interface-loopback.py b/src/conf_mode/interface-loopback.py index e2df37655..a1a807868 100755 --- a/src/conf_mode/interface-loopback.py +++ b/src/conf_mode/interface-loopback.py @@ -77,7 +77,10 @@ def apply(loopback): # update interface description used e.g. within SNMP # update interface description used e.g. within SNMP lo.ifalias = loopback['description'] - # configure interface address(es) + + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second for addr in loopback['address']: lo.add_addr(addr) -- cgit v1.2.3 From 98aafc8f704ef54b6ece514c038b6aea414df734 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Thu, 5 Sep 2019 19:35:43 +0200 Subject: vxlan: T1636: initial rewrite with XML and Python Tested using: Site 1 (VyOS 1.2.2) ------------------- set interfaces vxlan vxlan100 address '10.10.10.2/24' set interfaces vxlan vxlan100 remote '172.18.201.10' set interfaces vxlan vxlan100 vni '100' Site 2 (rewrite) ---------------- set interfaces vxlan vxlan100 address '10.10.10.1/24' set interfaces vxlan vxlan100 description 'VyOS VXLAN' set interfaces vxlan vxlan100 remote '172.18.202.10' set interfaces vxlan vxlan100 vni '100' --- Makefile | 1 + interface-definitions/interfaces-vxlan.xml | 49 +++++++ python/vyos/ifconfig.py | 37 +++-- src/conf_mode/interface-vxlan.py | 208 +++++++++++++++++++++++++++++ 4 files changed, 282 insertions(+), 13 deletions(-) create mode 100755 src/conf_mode/interface-vxlan.py (limited to 'src') diff --git a/Makefile b/Makefile index 03b4712fc..d7b3f047d 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,7 @@ interface_definitions: rm -f $(TMPL_DIR)/interfaces/node.def rm -f $(TMPL_DIR)/interfaces/bridge/node.tag/ip/node.def rm -f $(TMPL_DIR)/interfaces/bonding/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/vxlan/node.tag/ip/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/interface-definitions/interfaces-vxlan.xml b/interface-definitions/interfaces-vxlan.xml index 35a43f92c..b06c2860c 100644 --- a/interface-definitions/interfaces-vxlan.xml +++ b/interface-definitions/interfaces-vxlan.xml @@ -95,6 +95,55 @@ + + + Maximum Transmission Unit (MTU) + + 1450-9000 + Maximum Transmission Unit + + + + + MTU must be between 1450 and 9000 + + + + + Remote address of VXLAN tunnel + + ipv4 + Remote address of VXLAN tunnel + + + + + + + + + Destination port of VXLAN tunnel (default: 8472) + + 1-65535 + Numeric IP port + + + + + + + + + Virtual Network Identifier + + 0-16777214 + VXLAN virtual network identifier + + + + + + diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index bc22478a6..0479e3672 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -1407,32 +1407,43 @@ class VXLANIf(Interface, ): """ def __init__(self, ifname, config=''): if config: + self._ifname = ifname + if not os.path.exists('/sys/class/net/{}'.format(self._ifname)): # we assume that by default a multicast interface is created group = 'group {}'.format(config['group']) + # if remote host is specified we ignore the multicast address if config['remote']: group = 'remote {}'.format(config['remote']) + # an underlay device is not always specified dev = '' if config['dev']: - dev = 'dev'.format(config['dev']) + dev = 'dev {}'.format(config['dev']) - cmd = 'ip link add dev {intf} type vxlan id {vni} {group} {dev} {port}' - .format(intf=self._ifname, config['vni'], group=group, dev=dev, port=config['port']) + cmd = 'ip link add {intf} type vxlan id {vni} {grp_rem} {dev} dstport {port}' \ + .format(intf=self._ifname, vni=config['vni'], grp_rem=group, dev=dev, port=config['port']) self._cmd(cmd) super().__init__(ifname, type='vxlan') + @staticmethod + def get_config(): + """ + VXLAN interfaces require a configuration when they are added using + iproute2. This static method will provide the configuration dictionary + used by this class. - @staticmethod - def get_config(): - config = { - 'vni': 0, - 'dev': '', - 'group': '', - 'port': 8472 # The Linux implementation of VXLAN pre-dates + Example: + >> dict = VXLANIf().get_config() + """ + config = { + 'vni': 0, + 'dev': '', + 'group': '', + 'port': 8472, # The Linux implementation of VXLAN pre-dates # the IANA's selection of a standard destination port - 'remote': '', - 'ttl': 16 - } + 'remote': '' + } + return config diff --git a/src/conf_mode/interface-vxlan.py b/src/conf_mode/interface-vxlan.py new file mode 100755 index 000000000..59022238e --- /dev/null +++ b/src/conf_mode/interface-vxlan.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +from os import environ +from sys import exit +from copy import deepcopy + +from vyos.configdict import list_diff +from vyos.config import Config +from vyos.ifconfig import VXLANIf, Interface +from vyos.interfaces import get_type_of_interface +from vyos import ConfigError +from netifaces import interfaces + +default_config_data = { + 'address': [], + 'address_remove': [], + 'deleted': False, + 'description': '', + 'disable': False, + 'group': '', + 'intf': '', + 'ip_arp_cache_tmo': 30, + 'ip_proxy_arp': 0, + 'link': '', + 'mtu': 1450, + 'remote': '', + 'remote_port': 8472 # The Linux implementation of VXLAN pre-dates + # the IANA's selection of a standard destination port +} + + +def get_config(): + vxlan = deepcopy(default_config_data) + conf = Config() + + # determine tagNode instance + try: + vxlan['intf'] = environ['VYOS_TAGNODE_VALUE'] + except KeyError as E: + print("Interface not specified") + + # Check if interface has been removed + if not conf.exists('interfaces vxlan ' + vxlan['intf']): + vxlan['deleted'] = True + return vxlan + + # set new configuration level + conf.set_level('interfaces vxlan ' + vxlan['intf']) + + # retrieve configured interface addresses + if conf.exists('address'): + vxlan['address'] = conf.return_values('address') + + # Determine interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed from the interface + eff_addr = conf.return_effective_values('address') + act_addr = conf.return_values('address') + vxlan['address_remove'] = list_diff(eff_addr, act_addr) + + # retrieve interface description + if conf.exists('description'): + vxlan['description'] = conf.return_value('description') + + # Disable this interface + if conf.exists('disable'): + vxlan['disable'] = True + + # VXLAN multicast grou + if conf.exists('group'): + vxlan['group'] = conf.return_value('group') + + # ARP cache entry timeout in seconds + if conf.exists('ip arp-cache-timeout'): + vxlan['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) + + # Enable proxy-arp on this interface + if conf.exists('ip enable-proxy-arp'): + vxlan['ip_proxy_arp'] = 1 + + # VXLAN underlay interface + if conf.exists('link'): + vxlan['link'] = conf.return_value('link') + + # Maximum Transmission Unit (MTU) + if conf.exists('mtu'): + vxlan['mtu'] = int(conf.return_value('mtu')) + + # Remote address of VXLAN tunnel + if conf.exists('remote'): + vxlan['remote'] = conf.return_value('remote') + + # Remote port of VXLAN tunnel + if conf.exists('port'): + vxlan['remote_port'] = int(conf.return_value('port')) + + # Virtual Network Identifier + if conf.exists('vni'): + vxlan['vni'] = conf.return_value('vni') + + return vxlan + + +def verify(vxlan): + if vxlan['deleted']: + # bail out early + return None + + if vxlan['mtu'] < 1500: + print('WARNING: RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU') + + if vxlan['group'] and not vxlan['link']: + raise ConfigError('Multicast VXLAN requires an underlaying interface ') + + if not (vxlan['group'] or vxlan['remote']): + raise ConfigError('Group or remote must be configured') + + if not vxlan['vni']: + raise ConfigError('Must configure VNI for VXLAN') + + if vxlan['link']: + # VXLAN adds a 50 byte overhead - we need to check the underlaying MTU + # if our configured MTU is at least 50 bytes less + underlay_mtu = int(Interface(vxlan['link']).mtu) + if underlay_mtu < (vxlan['mtu'] + 50): + raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \ + 'MTU is to small ({})'.format(underlay_mtu)) + + return None + + +def generate(vxlan): + return None + + +def apply(vxlan): + # Check if the VXLAN interface already exists + if vxlan['intf'] in interfaces(): + v = VXLANIf(vxlan['intf']) + # VXLAN is super picky and the tunnel always needs to be recreated, + # thus we can simply always delete it first. + v.remove() + + if not vxlan['deleted']: + # VXLAN interface needs to be created on-block + # instead of passing a ton of arguments, I just use a dict + # that is managed by vyos.ifconfig + conf = deepcopy(VXLANIf.get_config()) + + # Assign VXLAN instance configuration parameters to config dict + conf['vni'] = vxlan['vni'] + conf['group'] = vxlan['group'] + conf['dev'] = vxlan['link'] + conf['remote'] = vxlan['remote'] + conf['port'] = vxlan['remote_port'] + + # Finally create the new interface + v = VXLANIf(vxlan['intf'], config=conf) + # update interface description used e.g. by SNMP + v.ifalias = vxlan['description'] + # Maximum Transfer Unit (MTU) + v.mtu = vxlan['mtu'] + + # configure ARP cache timeout in milliseconds + v.arp_cache_tmp = vxlan['ip_arp_cache_tmo'] + # Enable proxy-arp on this interface + v.proxy_arp = vxlan['ip_proxy_arp'] + + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second + for addr in vxlan['address_remove']: + v.del_addr(addr) + for addr in vxlan['address']: + v.add_addr(addr) + + # As the bond interface is always disabled first when changing + # parameters we will only re-enable the interface if it is not + # administratively disabled + if not vxlan['disable']: + v.state='up' + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) -- cgit v1.2.3 From 2e48db607e3f228cf6f40b53478966e5fbe00dbc Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 6 Sep 2019 15:12:20 +0200 Subject: openvpn: T1630: support adding routes as unpriviledged user --- src/conf_mode/interface-openvpn.py | 5 +++-- src/system/unpriv-ip | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100755 src/system/unpriv-ip (limited to 'src') diff --git a/src/conf_mode/interface-openvpn.py b/src/conf_mode/interface-openvpn.py index 91679084e..5f5cbd67d 100755 --- a/src/conf_mode/interface-openvpn.py +++ b/src/conf_mode/interface-openvpn.py @@ -34,8 +34,8 @@ from vyos.config import Config from vyos import ConfigError from vyos.validate import is_addr_assigned -user = 'nobody' -group = 'nogroup' +user = 'openvpn' +group = 'openvpn' # Please be careful if you edit the template. config_tmpl = """ @@ -58,6 +58,7 @@ dev {{ intf }} user {{ uid }} group {{ gid }} persist-key +iproute /usr/libexec/vyos/system/unpriv-ip proto {% if 'tcp-active' in protocol -%}tcp-client{% elif 'tcp-passive' in protocol -%}tcp-server{% else %}udp{% endif %} diff --git a/src/system/unpriv-ip b/src/system/unpriv-ip new file mode 100755 index 000000000..1ea0d626a --- /dev/null +++ b/src/system/unpriv-ip @@ -0,0 +1,2 @@ +#!/bin/sh +sudo /sbin/ip $* -- cgit v1.2.3 From 2e3b701f9045c4a557c99ee311c11d4a6f4f7b78 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 6 Sep 2019 18:02:40 +0200 Subject: openvpn: T1548: cleanup import statements --- src/conf_mode/interface-openvpn.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interface-openvpn.py b/src/conf_mode/interface-openvpn.py index 5f5cbd67d..fe08cd51e 100755 --- a/src/conf_mode/interface-openvpn.py +++ b/src/conf_mode/interface-openvpn.py @@ -18,15 +18,15 @@ import os import re -import pwd -import grp import sys import stat -import copy import jinja2 -import psutil -from ipaddress import ip_address,ip_network,IPv4Interface +from copy import deepcopy +from grp import getgrnam +from ipaddress import ip_address,ip_network,IPv4Interface +from psutil import pid_exists +from pwd import getpwnam from signal import SIGUSR1 from subprocess import Popen, PIPE @@ -302,8 +302,8 @@ def openvpn_mkdir(directory): # fix permissions - corresponds to mode 755 os.chmod(directory, stat.S_IRWXU|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH) - uid = pwd.getpwnam(user).pw_uid - gid = grp.getgrnam(group).gr_gid + uid = getpwnam(user).pw_uid + gid = getgrnam(group).gr_gid os.chown(directory, uid, gid) def fixup_permission(filename, permission=stat.S_IRUSR): @@ -315,8 +315,8 @@ def fixup_permission(filename, permission=stat.S_IRUSR): os.chmod(filename, permission) # make file owned by root / vyattacfg - uid = pwd.getpwnam('root').pw_uid - gid = grp.getgrnam('vyattacfg').gr_gid + uid = getpwnam('root').pw_uid + gid = getgrnam('vyattacfg').gr_gid os.chown(filename, uid, gid) def checkCertHeader(header, filename): @@ -335,7 +335,7 @@ def checkCertHeader(header, filename): return False def get_config(): - openvpn = copy.deepcopy(default_config_data) + openvpn = deepcopy(default_config_data) conf = Config() # determine tagNode instance @@ -793,8 +793,8 @@ def generate(openvpn): fixup_permission(auth_file) # get numeric uid/gid - uid = pwd.getpwnam(user).pw_uid - gid = grp.getgrnam(group).gr_gid + uid = getpwnam(user).pw_uid + gid = getgrnam(group).gr_gid # Generate client specific configuration for client in openvpn['client']: @@ -834,7 +834,7 @@ def apply(openvpn): # we only need to stop the demon if it's running # daemon could have died or killed by someone - if psutil.pid_exists(pid): + if pid_exists(pid): cmd = 'start-stop-daemon --stop --quiet' cmd += ' --pidfile ' + pidfile subprocess_cmd(cmd) @@ -857,7 +857,7 @@ def apply(openvpn): return None # Send SIGUSR1 to the process instead of creating a new process - if psutil.pid_exists(pid): + if pid_exists(pid): os.kill(pid, SIGUSR1) return None -- cgit v1.2.3 From 26ace9a7b92020bffebe85897cd3790342820612 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 6 Sep 2019 18:09:50 +0200 Subject: openvpn: T1548: always restart OpenVPN Previous implementations sent a SIGUSR1 to OpenVPN to initialte a restart after the configuration changed - as this was the same as the client keepalive mechanism did. Unfortunately on SIGUSR1 OpenVPN does not re-read the configuration file. Thus changed options were never taken into account. --- src/conf_mode/interface-openvpn.py | 54 +++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 27 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interface-openvpn.py b/src/conf_mode/interface-openvpn.py index fe08cd51e..548c78535 100755 --- a/src/conf_mode/interface-openvpn.py +++ b/src/conf_mode/interface-openvpn.py @@ -25,10 +25,11 @@ import jinja2 from copy import deepcopy 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 signal import SIGUSR1 from subprocess import Popen, PIPE +from time import sleep from vyos.config import Config from vyos import ConfigError @@ -819,47 +820,46 @@ def generate(openvpn): return None def apply(openvpn): - interface = openvpn['intf'] - pid = 0 - pidfile = '/var/run/openvpn/{}.pid'.format(interface) + 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()) - # If tunnel interface has been deleted - stop service - if openvpn['deleted'] or openvpn['disable']: - directory = os.path.dirname(get_config_name(interface)) - - # we only need to stop the demon if it's running - # daemon could have died or killed by someone - if pid_exists(pid): - cmd = 'start-stop-daemon --stop --quiet' - cmd += ' --pidfile ' + pidfile - subprocess_cmd(cmd) + # 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): + cmd = 'start-stop-daemon --stop --quiet' + cmd += ' --pidfile ' + pidfile + subprocess_cmd(cmd) - # cleanup old PID file - if os.path.isfile(pidfile): - os.remove(pidfile) + # cleanup old PID file + if os.path.isfile(pidfile): + os.remove(pidfile) + # Do some cleanup when OpenVPN is disabled/deleted + if openvpn['deleted'] or openvpn['disable']: # cleanup old configuration file - if os.path.isfile(get_config_name(interface)): - os.remove(get_config_name(interface)) + if os.path.isfile(get_config_name(openvpn['intf'])): + os.remove(get_config_name(openvpn['intf'])) # cleanup client config dir - if os.path.isdir(directory + '/ccd/' + interface): + directory = os.path.dirname(get_config_name(openvpn['intf'])) + if os.path.isdir(directory + '/ccd/' + openvpn['intf']): try: - os.remove(directory + '/ccd/' + interface + '/*') + os.remove(directory + '/ccd/' + openvpn['intf'] + '/*') except: pass return None - # Send SIGUSR1 to the process instead of creating a new process - if pid_exists(pid): - os.kill(pid, SIGUSR1) - return None + # On configuration change we need to wait for the 'old' interface to + # vanish from the Kernel, if it is not gone, OpenVPN will report: + # ERROR: Cannot ioctl TUNSETIFF vtun10: Device or resource busy (errno=16) + while openvpn['intf'] in interfaces(): + sleep(0.250) # 250ms # No matching OpenVPN process running - maybe it got killed or none # existed - nevertheless, spawn new OpenVPN process @@ -868,13 +868,13 @@ def apply(openvpn): cmd += ' --exec /usr/sbin/openvpn' # now pass arguments to openvpn binary cmd += ' --' - cmd += ' --config ' + get_config_name(interface) + cmd += ' --config ' + get_config_name(openvpn['intf']) # execute assembled command subprocess_cmd(cmd) - return None + if __name__ == '__main__': try: c = get_config() -- cgit v1.2.3 From 189ae4f7096abf7ca7100a4a31e038ce9e3e19c2 Mon Sep 17 00:00:00 2001 From: hagbard Date: Fri, 6 Sep 2019 14:46:43 -0700 Subject: [wireguard] - T1639: wireguard pubkey change error - sudo added to wg call - debug print removed when pubkey changes --- python/vyos/ifconfig.py | 2 +- src/conf_mode/interface-wireguard.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) (limited to 'src') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 0479e3672..5b1c11a47 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -1349,7 +1349,7 @@ class WireGuardIf(Interface): # fmask permission check? pass - cmd = "wg set {} ".format(self._ifname) + cmd = "sudo wg set {} ".format(self._ifname) cmd += "listen-port {} ".format(self.config['port']) cmd += "fwmark {} ".format(str(self.config['fwmark'])) cmd += "private-key {} ".format(self.config['private-key']) diff --git a/src/conf_mode/interface-wireguard.py b/src/conf_mode/interface-wireguard.py index 265491993..e7b9a267f 100755 --- a/src/conf_mode/interface-wireguard.py +++ b/src/conf_mode/interface-wireguard.py @@ -223,8 +223,6 @@ def apply(c): if nkey != ekey: sl.syslog( sl.LOG_NOTICE, "peer {0} pubkey changed from {1} to {2} on interface {3}".format(p, ekey, nkey, ifname)) - print ( - "peer {0} pubkey changed from {1} to {2} on interface {3}".format(p, ekey, nkey, ifname)) intfc.remove_peer(ekey) intfc.config['private-key'] = pk -- cgit v1.2.3 From a9756cfd49b169e93afe70415ce9155ebf4e5ffa Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 7 Sep 2019 14:18:23 +0200 Subject: bridge: bonding: minor comment cleanup --- src/conf_mode/interface-bonding.py | 2 +- src/conf_mode/interface-bridge.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index bce8f98fb..cd4c447ac 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -142,7 +142,7 @@ def get_config(): bond['address'] = conf.return_values('address') # get interface addresses (currently effective) - to determine which - # address is no longer valid and needs to be removed from the bond + # address is no longer valid and needs to be removed eff_addr = conf.return_effective_values('address') bond['address_remove'] = list_diff(eff_addr, bond['address']) diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index 8996fceec..401182a0d 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -71,7 +71,7 @@ def get_config(): bridge['address'] = conf.return_values('address') # Determine interface addresses (currently effective) - to determine which - # address is no longer valid and needs to be removed from the bridge + # address is no longer valid and needs to be removed eff_addr = conf.return_effective_values('address') bridge['address_remove'] = list_diff(eff_addr, bridge['address']) -- cgit v1.2.3 From 6f666f0a62fb98fcab800be813141f44dd1ab8a7 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 7 Sep 2019 15:30:53 +0200 Subject: bonding: T1614: bugfix in validate - enslave failed Forgot to exclude our current bond interface in the search for duplicate interface enslavement. --- src/conf_mode/interface-bonding.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/conf_mode/interface-bonding.py b/src/conf_mode/interface-bonding.py index cd4c447ac..dc0363fb7 100755 --- a/src/conf_mode/interface-bonding.py +++ b/src/conf_mode/interface-bonding.py @@ -272,11 +272,14 @@ def verify(bond): conf = Config() for intf in bond['member']: - # a bond member is only allowed to be assigned to any one bond - for tmp in conf.list_nodes('interfaces bonding'): + # a bonding member interface is only allowed to be assigned to one bond! + all_bonds = conf.list_nodes('interfaces bonding') + # We do not need to check our own bond + all_bonds.remove(bond['intf']) + for tmp in all_bonds: if conf.exists('interfaces bonding ' + tmp + ' member interface ' + intf): raise ConfigError('can not enslave interface {} which already ' \ - 'belongs to bond {}'.format(intf, tmp)) + 'belongs to {}'.format(intf, tmp)) # we can not add disabled slave interfaces to our bond if conf.exists('interfaces ethernet ' + intf + ' disable'): -- cgit v1.2.3 From f7456361b5b94f3c69f8fa0f34f8bff0ef68f9aa Mon Sep 17 00:00:00 2001 From: hagbard Date: Mon, 9 Sep 2019 09:32:57 -0700 Subject: [wireguard] - T1639: wireguard pubkey change error - removed sudo as is already runs as root - set privte key as variable in preparation to support multiple pk's --- python/vyos/ifconfig.py | 4 ++-- src/conf_mode/interface-wireguard.py | 15 +++++---------- 2 files changed, 7 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py index 5b1c11a47..62bf94d79 100644 --- a/python/vyos/ifconfig.py +++ b/python/vyos/ifconfig.py @@ -1349,7 +1349,7 @@ class WireGuardIf(Interface): # fmask permission check? pass - cmd = "sudo wg set {} ".format(self._ifname) + cmd = "wg set {} ".format(self._ifname) cmd += "listen-port {} ".format(self.config['port']) cmd += "fwmark {} ".format(str(self.config['fwmark'])) cmd += "private-key {} ".format(self.config['private-key']) @@ -1380,7 +1380,7 @@ class WireGuardIf(Interface): Giving it a readable name is a vyos feature, to remove a peer the pubkey and the interface is needed, to remove the entry. """ - cmd = "sudo wg set {0} peer {1} remove".format( + cmd = "wg set {0} peer {1} remove".format( self._ifname, str(peerkey)) self._cmd(cmd) diff --git a/src/conf_mode/interface-wireguard.py b/src/conf_mode/interface-wireguard.py index e7b9a267f..4c0e90ca6 100755 --- a/src/conf_mode/interface-wireguard.py +++ b/src/conf_mode/interface-wireguard.py @@ -29,12 +29,6 @@ from vyos.ifconfig import WireGuardIf ifname = str(os.environ['VYOS_TAGNODE_VALUE']) intfc = WireGuardIf(ifname) -dir = r'/config/auth/wireguard' -pk = dir + '/private.key' -pub = dir + '/public.key' -psk_file = dir + '/psk' - - def check_kmod(): if not os.path.exists('/sys/module/wireguard'): sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod") @@ -57,7 +51,8 @@ def get_config(): 'state': 'enabled', 'fwmark': 0x00, 'mtu': 1420, - 'peer': {} + 'peer': {}, + 'pk' : '/config/auth/wireguard/private.key' } } @@ -112,12 +107,11 @@ def get_config(): return config_data - def verify(c): if not c: return None - if not os.path.exists(pk): + if not os.path.exists(c[ifname]['pk']): raise ConfigError( "No keys found, generate them by executing: \'run generate wireguard keypair\'") @@ -225,7 +219,7 @@ def apply(c): sl.LOG_NOTICE, "peer {0} pubkey changed from {1} to {2} on interface {3}".format(p, ekey, nkey, ifname)) intfc.remove_peer(ekey) - intfc.config['private-key'] = pk + intfc.config['private-key'] = c[ifname]['pk'] for p in c[ifname]['peer']: intfc.config['pubkey'] = str(c[ifname]['peer'][p]['pubkey']) intfc.config['allowed-ips'] = (c[ifname]['peer'][p]['allowed-ips']) @@ -249,6 +243,7 @@ def apply(c): # preshared-key - needs to be read from a file if 'psk' in c[ifname]['peer'][p]: + psk_file = '/config/auth/wireguard/psk' old_umask = os.umask(0o077) open(psk_file, 'w').write(str(c[ifname]['peer'][p]['psk'])) os.umask(old_umask) -- cgit v1.2.3 From 1017c8103f12ebd6db4f250d8a154571fff32db1 Mon Sep 17 00:00:00 2001 From: hagbard Date: Mon, 9 Sep 2019 11:55:54 -0700 Subject: [wireguard]: T1572 - Wireguard keyPair per interface - param key location added in op-mode script - param delkey and listkey implemented in op-mode script - param delkey implemented in op-mode script - generate and store named keys - interface implementation tu use cli option 'private-key' --- Makefile | 1 + interface-definitions/interfaces-wireguard.xml | 8 ++ op-mode-definitions/wireguard.xml | 53 ++++++- src/conf_mode/interface-wireguard.py | 11 +- src/op_mode/wireguard.py | 188 ++++++++++++++++--------- 5 files changed, 188 insertions(+), 73 deletions(-) (limited to 'src') diff --git a/Makefile b/Makefile index d7b3f047d..ad05acff5 100644 --- a/Makefile +++ b/Makefile @@ -42,6 +42,7 @@ op_mode_definitions: rm -f $(OP_TMPL_DIR)/generate/node.def rm -f $(OP_TMPL_DIR)/show/vpn/node.def rm -f $(OP_TMPL_DIR)/show/system/node.def + rm -f $(OP_TMPL_DIR)/delete/node.def .PHONY: all all: clean interface_definitions op_mode_definitions diff --git a/interface-definitions/interfaces-wireguard.xml b/interface-definitions/interfaces-wireguard.xml index 6e2622018..f2a7cc316 100644 --- a/interface-definitions/interfaces-wireguard.xml +++ b/interface-definitions/interfaces-wireguard.xml @@ -77,6 +77,14 @@ + + + Private key to use on that interface + + + + + peer alias diff --git a/op-mode-definitions/wireguard.xml b/op-mode-definitions/wireguard.xml index fa5e4a206..785af202c 100644 --- a/op-mode-definitions/wireguard.xml +++ b/op-mode-definitions/wireguard.xml @@ -20,6 +20,12 @@ ${vyos_op_scripts_dir}/wireguard.py --genpsk + + + Generates named wireguard keypairs + + sudo ${vyos_op_scripts_dir}/wireguard.py --genkey --location "$4" + @@ -33,7 +39,7 @@ - show wireguard public key + Show wireguard public key ${vyos_op_scripts_dir}/wireguard.py --showpub @@ -43,6 +49,31 @@ ${vyos_op_scripts_dir}/wireguard.py --showpriv + + + Shows named wireguard keys + + + + + Show wireguard private named key + + + + + ${vyos_op_scripts_dir}/wireguard.py --showpub --location "$5" + + + + Show wireguard public named key + + + + + ${vyos_op_scripts_dir}/wireguard.py --showpriv --location "$5" + + + @@ -81,5 +112,25 @@ + + + + + Delete wireguard properties + + + + + Delete wireguard named keypair + + + + + sudo ${vyos_op_scripts_dir}/wireguard.py --delkdir --location "$4" + + + + + diff --git a/src/conf_mode/interface-wireguard.py b/src/conf_mode/interface-wireguard.py index 4c0e90ca6..0f9e66aa6 100755 --- a/src/conf_mode/interface-wireguard.py +++ b/src/conf_mode/interface-wireguard.py @@ -29,6 +29,9 @@ from vyos.ifconfig import WireGuardIf ifname = str(os.environ['VYOS_TAGNODE_VALUE']) intfc = WireGuardIf(ifname) +kdir = r'/config/auth/wireguard' + + def check_kmod(): if not os.path.exists('/sys/module/wireguard'): sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod") @@ -52,7 +55,7 @@ def get_config(): 'fwmark': 0x00, 'mtu': 1420, 'peer': {}, - 'pk' : '/config/auth/wireguard/private.key' + 'pk': '{}/private.key'.format(kdir) } } @@ -77,6 +80,9 @@ def get_config(): ifname + ' description') if c.exists(ifname + ' mtu'): config_data[ifname]['mtu'] = c.return_value(ifname + ' mtu') + if c.exists(ifname + ' private-key'): + config_data[ifname]['pk'] = "{0}/{1}/private.key".format( + kdir, c.return_value(ifname + ' private-key')) if c.exists(ifname + ' peer'): for p in c.list_nodes(ifname + ' peer'): if not c.exists(ifname + ' peer ' + p + ' disable'): @@ -107,13 +113,14 @@ def get_config(): return config_data + def verify(c): if not c: return None if not os.path.exists(c[ifname]['pk']): raise ConfigError( - "No keys found, generate them by executing: \'run generate wireguard keypair\'") + "No keys found, generate them by executing: \'run generate wireguard [keypair|named-keypairs]\'") if c[ifname]['status'] != 'delete': if not c[ifname]['addr']: diff --git a/src/op_mode/wireguard.py b/src/op_mode/wireguard.py index 66622c04c..e48da2e40 100755 --- a/src/op_mode/wireguard.py +++ b/src/op_mode/wireguard.py @@ -19,91 +19,139 @@ import argparse import os import sys +import shutil import subprocess import syslog as sl + from vyos import ConfigError dir = r'/config/auth/wireguard' -pk = dir + '/private.key' -pub = dir + '/public.key' psk = dir + '/preshared.key' + def check_kmod(): - """ check if kmod is loaded, if not load it """ - if not os.path.exists('/sys/module/wireguard'): - sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod") - if os.system('sudo modprobe wireguard') != 0: - sl.syslog(sl.LOG_ERR, "modprobe wireguard failed") - raise ConfigError("modprobe wireguard failed") - -def generate_keypair(): - """ generates a keypair which is stored in /config/auth/wireguard """ - ret = subprocess.call(['wg genkey | tee ' + pk + '|wg pubkey > ' + pub], shell=True) - if ret != 0: - raise ConfigError("wireguard key-pair generation failed") - else: - sl.syslog(sl.LOG_NOTICE, "new keypair wireguard key generated in " + dir) - -def genkey(): - """ helper function to check, regenerate the keypair """ - old_umask = os.umask(0o077) - if os.path.exists(pk) and os.path.exists(pub): - try: - choice = input("You already have a wireguard key-pair already, do you want to re-generate? [y/n] ") - if choice == 'y' or choice == 'Y': - generate_keypair() - except KeyboardInterrupt: - sys.exit(0) - else: - """ if keypair is bing executed from a running iso """ - if not os.path.exists(dir): - os.umask(old_umask) - subprocess.call(['sudo mkdir -p ' + dir], shell=True) - subprocess.call(['sudo chgrp vyattacfg ' + dir], shell=True) - subprocess.call(['sudo chmod 770 ' + dir], shell=True) - generate_keypair() - os.umask(old_umask) + """ check if kmod is loaded, if not load it """ + if not os.path.exists('/sys/module/wireguard'): + sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod") + if os.system('sudo modprobe wireguard') != 0: + sl.syslog(sl.LOG_ERR, "modprobe wireguard failed") + raise ConfigError("modprobe wireguard failed") -def showkey(key): - """ helper function to show privkey or pubkey """ - if key == "pub": - if os.path.exists(pub): - print ( open(pub).read().strip() ) + +def generate_keypair(pk, pub): + """ generates a keypair which is stored in /config/auth/wireguard """ + old_umask = os.umask(0o027) + ret = subprocess.call( + ['wg genkey | tee ' + pk + '|wg pubkey > ' + pub], shell=True) + if ret != 0: + raise ConfigError("wireguard key-pair generation failed") else: - print("no public key found") + sl.syslog( + sl.LOG_NOTICE, "new keypair wireguard key generated in " + dir) + os.umask(old_umask) - if key == "pk": - if os.path.exists(pk): - print ( open(pk).read().strip() ) + +def genkey(location): + """ helper function to check, regenerate the keypair """ + pk = "{}/private.key".format(location) + pub = "{}/public.key".format(location) + old_umask = os.umask(0o027) + if os.path.exists(pk) and os.path.exists(pub): + try: + choice = input( + "You already have a wireguard key-pair, do you want to re-generate? [y/n] ") + if choice == 'y' or choice == 'Y': + generate_keypair(pk, pub) + except KeyboardInterrupt: + sys.exit(0) + else: + """ if keypair is bing executed from a running iso """ + if not os.path.exists(location): + subprocess.call(['sudo mkdir -p ' + location], shell=True) + subprocess.call(['sudo chgrp vyattacfg ' + location], shell=True) + subprocess.call(['sudo chmod 750 ' + location], shell=True) + generate_keypair(pk, pub) + os.umask(old_umask) + + +def showkey(key): + """ helper function to show privkey or pubkey """ + if os.path.exists(key): + print (open(key).read().strip()) else: - print("no private key found") + print ("{} not found".format(key)) + def genpsk(): - """ generates a preshared key and shows it on stdout, it's stroed only in the config """ - subprocess.call(['wg genpsk'], shell=True) + """ + generates a preshared key and shows it on stdout, + it's stored only in the cli config + """ + + subprocess.call(['wg genpsk'], shell=True) + + +def list_key_dirs(): + """ lists all dirs under /config/auth/wireguard """ + if os.path.exists(dir): + nks = next(os.walk(dir))[1] + for nk in nks: + print (nk) + + +def del_key_dir(kname): + """ deletes /config/auth/wireguard/ """ + kdir = "{0}/{1}".format(dir, kname) + if not os.path.isdir(kdir): + print ("named keypair {} not found".format(kname)) + return 1 + shutil.rmtree(kdir) + if __name__ == '__main__': - check_kmod() - - parser = argparse.ArgumentParser(description='wireguard key management') - parser.add_argument('--genkey', action="store_true", help='generate key-pair') - parser.add_argument('--showpub', action="store_true", help='shows public key') - parser.add_argument('--showpriv', action="store_true", help='shows private key') - parser.add_argument('--genpsk', action="store_true", help='generates preshared-key') - args = parser.parse_args() - - try: - if args.genkey: - genkey() - if args.showpub: - showkey("pub") - if args.showpriv: - showkey("pk") - if args.genpsk: - genpsk() - - except ConfigError as e: - print(e) - sys.exit(1) + check_kmod() + parser = argparse.ArgumentParser(description='wireguard key management') + parser.add_argument( + '--genkey', action="store_true", help='generate key-pair') + parser.add_argument( + '--showpub', action="store_true", help='shows public key') + parser.add_argument( + '--showpriv', action="store_true", help='shows private key') + parser.add_argument( + '--genpsk', action="store_true", help='generates preshared-key') + parser.add_argument( + '--location', action="store", help='key location within {}'.format(dir)) + parser.add_argument( + '--listkdir', action="store_true", help='lists named keydirectories') + parser.add_argument( + '--delkdir', action="store_true", help='removes named keydirectories') + args = parser.parse_args() + + try: + if args.genkey: + if args.location: + genkey("{0}/{1}".format(dir, args.location)) + else: + genkey(dir) + + if args.showpub: + if args.location: + showkey("{0}/{1}/public.key".format(dir, args.location)) + else: + showkey("{}/public.key".format(dir)) + if args.showpriv: + if args.location: + showkey("{0}/{1}/private.key".format(dir, args.location)) + else: + showkey("{}/private".format(dir)) + if args.genpsk: + genpsk() + if args.listkdir: + list_key_dirs() + if args.delkdir: + del_key_dir(args.location) + except ConfigError as e: + print(e) + sys.exit(1) -- cgit v1.2.3 From db07e6fa76d90eaf80a06729753fb89266437674 Mon Sep 17 00:00:00 2001 From: hagbard Date: Tue, 10 Sep 2019 11:28:53 -0700 Subject: [wireguard]: T1650 - cli option to delete default wg key --- op-mode-definitions/wireguard.xml | 6 +++--- src/conf_mode/interface-wireguard.py | 7 ++----- src/op_mode/wireguard.py | 18 +++++++++--------- 3 files changed, 14 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/op-mode-definitions/wireguard.xml b/op-mode-definitions/wireguard.xml index 785af202c..3c54c81bd 100644 --- a/op-mode-definitions/wireguard.xml +++ b/op-mode-definitions/wireguard.xml @@ -12,7 +12,7 @@ generate a wireguard keypair - ${vyos_op_scripts_dir}/wireguard.py --genkey + sudo ${vyos_op_scripts_dir}/wireguard.py --genkey @@ -119,9 +119,9 @@ Delete wireguard properties - + - Delete wireguard named keypair + Delete a wireguard keypair diff --git a/src/conf_mode/interface-wireguard.py b/src/conf_mode/interface-wireguard.py index 0f9e66aa6..d51a7a08d 100755 --- a/src/conf_mode/interface-wireguard.py +++ b/src/conf_mode/interface-wireguard.py @@ -31,7 +31,6 @@ intfc = WireGuardIf(ifname) kdir = r'/config/auth/wireguard' - def check_kmod(): if not os.path.exists('/sys/module/wireguard'): sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod") @@ -55,7 +54,7 @@ def get_config(): 'fwmark': 0x00, 'mtu': 1420, 'peer': {}, - 'pk': '{}/private.key'.format(kdir) + 'pk' : '{}/default/private.key'.format(kdir) } } @@ -81,8 +80,7 @@ def get_config(): if c.exists(ifname + ' mtu'): config_data[ifname]['mtu'] = c.return_value(ifname + ' mtu') if c.exists(ifname + ' private-key'): - config_data[ifname]['pk'] = "{0}/{1}/private.key".format( - kdir, c.return_value(ifname + ' private-key')) + config_data[ifname]['pk'] = "{0}/{1}/private.key".format(kdir,c.return_value(ifname + ' private-key')) if c.exists(ifname + ' peer'): for p in c.list_nodes(ifname + ' peer'): if not c.exists(ifname + ' peer ' + p + ' disable'): @@ -113,7 +111,6 @@ def get_config(): return config_data - def verify(c): if not c: return None diff --git a/src/op_mode/wireguard.py b/src/op_mode/wireguard.py index e48da2e40..4e93ec6aa 100755 --- a/src/op_mode/wireguard.py +++ b/src/op_mode/wireguard.py @@ -91,18 +91,16 @@ def genpsk(): subprocess.call(['wg genpsk'], shell=True) - def list_key_dirs(): - """ lists all dirs under /config/auth/wireguard """ + """ lists all dirs under /config/auth/wireguard """ if os.path.exists(dir): nks = next(os.walk(dir))[1] for nk in nks: print (nk) - def del_key_dir(kname): """ deletes /config/auth/wireguard/ """ - kdir = "{0}/{1}".format(dir, kname) + kdir = "{0}/{1}".format(dir,kname) if not os.path.isdir(kdir): print ("named keypair {} not found".format(kname)) return 1 @@ -133,24 +131,26 @@ if __name__ == '__main__': if args.location: genkey("{0}/{1}".format(dir, args.location)) else: - genkey(dir) - + genkey("{}/default".format(dir)) if args.showpub: if args.location: showkey("{0}/{1}/public.key".format(dir, args.location)) else: - showkey("{}/public.key".format(dir)) + showkey("{}/default/public.key".format(dir)) if args.showpriv: if args.location: showkey("{0}/{1}/private.key".format(dir, args.location)) else: - showkey("{}/private".format(dir)) + showkey("{}/default/private.key".format(dir)) if args.genpsk: genpsk() if args.listkdir: list_key_dirs() if args.delkdir: - del_key_dir(args.location) + if args.location: + del_key_dir(args.location) + else: + del_key_dir("default") except ConfigError as e: print(e) -- cgit v1.2.3 From d34fd745438951d55c5c4899b2b3c7bfa5d08026 Mon Sep 17 00:00:00 2001 From: hagbard Date: Tue, 10 Sep 2019 14:56:37 -0700 Subject: [syslog] - T1597: 'del system system' stops now rsyslog --- src/conf_mode/syslog.py | 418 +++++++++++++++++++++++++----------------------- 1 file changed, 221 insertions(+), 197 deletions(-) (limited to 'src') diff --git a/src/conf_mode/syslog.py b/src/conf_mode/syslog.py index 7b79c701b..c4f3d2c9c 100755 --- a/src/conf_mode/syslog.py +++ b/src/conf_mode/syslog.py @@ -24,16 +24,16 @@ import jinja2 from vyos.config import Config from vyos import ConfigError -########### config templates +# config templates -#### /etc/rsyslog.d/vyos-rsyslog.conf ### +# /etc/rsyslog.d/vyos-rsyslog.conf ### configs = ''' ## generated by syslog.py ## ## file based logging {% if files['global']['marker'] -%} $ModLoad immark {% if files['global']['marker-interval'] %} -$MarkMessagePeriod {{files['global']['marker-interval']}} +$MarkMessagePeriod {{files['global']['marker-interval']}} {% endif %} {% endif -%} {% if files['global']['preserver_fqdn'] -%} @@ -80,217 +80,241 @@ logrotate_configs = ''' } {% endfor %} ''' -############# config templates end +# config templates end + def get_config(): - c = Config() - if not c.exists('system syslog'): - return None - c.set_level('system syslog') - - config_data = { - 'files' : {}, - 'console' : {}, - 'hosts' : {}, - 'user' : {} - } - - ##### - # /etc/rsyslog.d/vyos-rsyslog.conf - # 'set system syslog global' - ##### - config_data['files'].update( - { - 'global' : { - 'log-file' : '/var/log/messages', - 'max-size' : 262144, - 'action-on-max-size' : '/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog', - 'selectors' : '*.notice;local7.debug', - 'max-files' : '5', - 'preserver_fqdn' : False - } - } - ) - - if c.exists('global marker'): - config_data['files']['global']['marker'] = True - if c.exists('global marker interval'): - config_data['files']['global']['marker-interval'] = c.return_value('global marker interval') - if c.exists('global facility'): - config_data['files']['global']['selectors'] = generate_selectors(c, 'global facility') - if c.exists('global archive size'): - config_data['files']['global']['max-size'] = int(c.return_value('global archive size'))* 1024 - if c.exists('global archive file'): - config_data['files']['global']['max-files'] = c.return_value('global archive file') - if c.exists('global preserve-fqdn'): - config_data['files']['global']['preserver_fqdn'] = True - - ### - # set system syslog file - ### - - if c.exists('file'): - filenames = c.list_nodes('file') - for filename in filenames: - config_data['files'].update( - { - filename : { - 'log-file' : '/var/log/user/' + filename, - 'max-files' : '5', - 'action-on-max-size' : '/usr/sbin/logrotate /etc/logrotate.d/' + filename, - 'selectors' : '*.err', - 'max-size' : 262144 - } - } - ) - - if c.exists('file ' + filename + ' facility'): - config_data['files'][filename]['selectors'] = generate_selectors(c, 'file ' + filename + ' facility') - if c.exists('file ' + filename + ' archive size'): - config_data['files'][filename]['max-size'] = int(c.return_value('file ' + filename + ' archive size'))* 1024 - if c.exists('file ' + filename + ' archive files'): - config_data['files'][filename]['max-files'] = c.return_value('file ' + filename + ' archive files') - - ## set system syslog console - if c.exists('console'): - config_data['console'] = { - '/dev/console' : { - 'selectors' : '*.err' - } + c = Config() + if not c.exists('system syslog'): + return None + c.set_level('system syslog') + + config_data = { + 'files': {}, + 'console': {}, + 'hosts': {}, + 'user': {} } - - for f in c.list_nodes('console facility'): - if c.exists('console facility ' + f + ' level'): - config_data['console'] = { - '/dev/console' : { - 'selectors' : generate_selectors(c, 'console facility') - } - } - - ## set system syslog host - if c.exists('host'): - proto = 'udp' - rhosts = c.list_nodes('host') - for rhost in rhosts: - for fac in c.list_nodes('host ' + rhost + ' facility'): - if c.exists('host ' + rhost + ' facility ' + fac + ' protocol'): - proto = c.return_value('host ' + rhost + ' facility ' + fac + ' protocol') - - config_data['hosts'].update( + + # + # /etc/rsyslog.d/vyos-rsyslog.conf + # 'set system syslog global' + # + config_data['files'].update( { - rhost : { - 'selectors' : generate_selectors(c, 'host ' + rhost + ' facility'), - 'proto' : proto - } + 'global': { + 'log-file': '/var/log/messages', + 'max-size': 262144, + 'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog', + 'selectors': '*.notice;local7.debug', + 'max-files': '5', + 'preserver_fqdn': False + } } - ) + ) - ## set system syslog user - if c.exists('user'): - usrs = c.list_nodes('user') - for usr in usrs: - config_data['user'].update( - { - usr : { - 'selectors' : generate_selectors(c, 'user ' + usr + ' facility') - } + if c.exists('global marker'): + config_data['files']['global']['marker'] = True + if c.exists('global marker interval'): + config_data['files']['global'][ + 'marker-interval'] = c.return_value('global marker interval') + if c.exists('global facility'): + config_data['files']['global'][ + 'selectors'] = generate_selectors(c, 'global facility') + if c.exists('global archive size'): + config_data['files']['global']['max-size'] = int( + c.return_value('global archive size')) * 1024 + if c.exists('global archive file'): + config_data['files']['global'][ + 'max-files'] = c.return_value('global archive file') + if c.exists('global preserve-fqdn'): + config_data['files']['global']['preserver_fqdn'] = True + + # + # set system syslog file + # + + if c.exists('file'): + filenames = c.list_nodes('file') + for filename in filenames: + config_data['files'].update( + { + filename: { + 'log-file': '/var/log/user/' + filename, + 'max-files': '5', + 'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/' + filename, + 'selectors': '*.err', + 'max-size': 262144 + } + } + ) + + if c.exists('file ' + filename + ' facility'): + config_data['files'][filename]['selectors'] = generate_selectors( + c, 'file ' + filename + ' facility') + if c.exists('file ' + filename + ' archive size'): + config_data['files'][filename]['max-size'] = int( + c.return_value('file ' + filename + ' archive size')) * 1024 + if c.exists('file ' + filename + ' archive files'): + config_data['files'][filename]['max-files'] = c.return_value( + 'file ' + filename + ' archive files') + + # set system syslog console + if c.exists('console'): + config_data['console'] = { + '/dev/console': { + 'selectors': '*.err' + } } - ) - - return config_data + + for f in c.list_nodes('console facility'): + if c.exists('console facility ' + f + ' level'): + config_data['console'] = { + '/dev/console': { + 'selectors': generate_selectors(c, 'console facility') + } + } + + # set system syslog host + if c.exists('host'): + proto = 'udp' + rhosts = c.list_nodes('host') + for rhost in rhosts: + for fac in c.list_nodes('host ' + rhost + ' facility'): + if c.exists('host ' + rhost + ' facility ' + fac + ' protocol'): + proto = c.return_value( + 'host ' + rhost + ' facility ' + fac + ' protocol') + + config_data['hosts'].update( + { + rhost: { + 'selectors': generate_selectors(c, 'host ' + rhost + ' facility'), + 'proto': proto + } + } + ) + + # set system syslog user + if c.exists('user'): + usrs = c.list_nodes('user') + for usr in usrs: + config_data['user'].update( + { + usr: { + 'selectors': generate_selectors(c, 'user ' + usr + ' facility') + } + } + ) + + return config_data + def generate_selectors(c, config_node): -## protocols and security are being mapped here -## for backward compatibility with old configs -## security and protocol mappings can be removed later - if c.is_tag(config_node): - nodes = c.list_nodes(config_node) - selectors = "" - for node in nodes: - lvl = c.return_value( config_node + ' ' + node + ' level') - if lvl == None: - lvl = "err" - if lvl == 'all': - lvl = '*' - if node == 'all' and node != nodes[-1]: - selectors += "*." + lvl + ";" - elif node == 'all': - selectors += "*." + lvl - elif node != nodes[-1]: - if node == 'protocols': - node = 'local7' - if node == 'security': - node = 'auth' - selectors += node + "." + lvl + ";" - else: - if node == 'protocols': - node = 'local7' - if node == 'security': - node = 'auth' - selectors += node + "." + lvl - return selectors +# protocols and security are being mapped here +# for backward compatibility with old configs +# security and protocol mappings can be removed later + if c.is_tag(config_node): + nodes = c.list_nodes(config_node) + selectors = "" + for node in nodes: + lvl = c.return_value(config_node + ' ' + node + ' level') + if lvl == None: + lvl = "err" + if lvl == 'all': + lvl = '*' + if node == 'all' and node != nodes[-1]: + selectors += "*." + lvl + ";" + elif node == 'all': + selectors += "*." + lvl + elif node != nodes[-1]: + if node == 'protocols': + node = 'local7' + if node == 'security': + node = 'auth' + selectors += node + "." + lvl + ";" + else: + if node == 'protocols': + node = 'local7' + if node == 'security': + node = 'auth' + selectors += node + "." + lvl + return selectors + def generate(c): - if c == None: - return None + if c == None: + return None - tmpl = jinja2.Template(configs, trim_blocks=True) - config_text = tmpl.render(c) - with open('/etc/rsyslog.d/vyos-rsyslog.conf', 'w') as f: - f.write(config_text) + tmpl = jinja2.Template(configs, trim_blocks=True) + config_text = tmpl.render(c) + with open('/etc/rsyslog.d/vyos-rsyslog.conf', 'w') as f: + f.write(config_text) + + # eventually write for each file its own logrotate file, since size is + # defined it shouldn't matter + tmpl = jinja2.Template(logrotate_configs, trim_blocks=True) + config_text = tmpl.render(c) + with open('/etc/logrotate.d/vyos-rsyslog', 'w') as f: + f.write(config_text) - ## eventually write for each file its own logrotate file, since size is defined it shouldn't matter - tmpl = jinja2.Template(logrotate_configs, trim_blocks=True) - config_text = tmpl.render(c) - with open('/etc/logrotate.d/vyos-rsyslog', 'w') as f: - f.write(config_text) def verify(c): - if c == None: - return None - # - # /etc/rsyslog.conf is generated somewhere and copied over the original (exists in /opt/vyatta/etc/rsyslog.conf) - # it interferes with the global logging, to make sure we are using a single base, template is enforced here - # - if not os.path.islink('/etc/rsyslog.conf'): - os.remove('/etc/rsyslog.conf') - os.symlink('/usr/share/vyos/templates/rsyslog/rsyslog.conf', '/etc/rsyslog.conf') - - # /var/log/vyos-rsyslog were the old files, we may want to clean those up, but currently there - # is a chance that someone still needs it, so I don't automatically remove them - - if c == None: - return None - - fac = ['*','auth','authpriv','cron','daemon','kern','lpr','mail','mark','news','protocols','security',\ - 'syslog','user','uucp','local0','local1','local2','local3','local4','local5','local6','local7'] - lvl = ['emerg','alert','crit','err','warning','notice','info','debug','*'] - - for conf in c: - if c[conf]: - for item in c[conf]: - for s in c[conf][item]['selectors'].split(";"): - f = re.sub("\..*$","",s) - if f not in fac: - print (c[conf]) - raise ConfigError('Invalid facility ' + s + ' set in '+ conf + ' ' + item) - l = re.sub("^.+\.","",s) - if l not in lvl: - raise ConfigError('Invalid logging level ' + s + ' set in '+ conf + ' ' + item) + if c == None: + return None + # + # /etc/rsyslog.conf is generated somewhere and copied over the original (exists in /opt/vyatta/etc/rsyslog.conf) + # it interferes with the global logging, to make sure we are using a single base, template is enforced here + # + if not os.path.islink('/etc/rsyslog.conf'): + os.remove('/etc/rsyslog.conf') + os.symlink( + '/usr/share/vyos/templates/rsyslog/rsyslog.conf', '/etc/rsyslog.conf') + + # /var/log/vyos-rsyslog were the old files, we may want to clean those up, but currently there + # is a chance that someone still needs it, so I don't automatically remove + # them + + if c == None: + return None + + fac = [ + '*', 'auth', 'authpriv', 'cron', 'daemon', 'kern', 'lpr', 'mail', 'mark', 'news', 'protocols', 'security', + 'syslog', 'user', 'uucp', 'local0', 'local1', 'local2', 'local3', 'local4', 'local5', 'local6', 'local7'] + lvl = ['emerg', 'alert', 'crit', 'err', + 'warning', 'notice', 'info', 'debug', '*'] + + for conf in c: + if c[conf]: + for item in c[conf]: + for s in c[conf][item]['selectors'].split(";"): + f = re.sub("\..*$", "", s) + if f not in fac: + print (c[conf]) + raise ConfigError( + 'Invalid facility ' + s + ' set in ' + conf + ' ' + item) + l = re.sub("^.+\.", "", s) + if l not in lvl: + raise ConfigError( + 'Invalid logging level ' + s + ' set in ' + conf + ' ' + item) + def apply(c): - if not os.path.exists('/var/run/rsyslogd.pid'): - os.system("sudo systemctl start rsyslog >/dev/null") - else: - os.system("sudo systemctl restart rsyslog >/dev/null") + if not c and os.path.exists('/var/run/rsyslogd.pid'): + os.system("sudo systemctl stop syslog.socket") + os.system("sudo systemctl stop rsyslog") + else: + if not os.path.exists('/var/run/rsyslogd.pid'): + os.system("sudo systemctl start rsyslog >/dev/null") + else: + os.system("sudo systemctl restart rsyslog >/dev/null") if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - sys.exit(1) + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) -- cgit v1.2.3 From 59e5e64cfbb67a5eb1a9d4d21dd54d946897b8d7 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 11 Sep 2019 23:14:23 +0200 Subject: T1598: annotate the vyos-hostsd unit file. --- src/systemd/vyos-hostsd.service | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/systemd/vyos-hostsd.service b/src/systemd/vyos-hostsd.service index 2444f5352..731e570c9 100644 --- a/src/systemd/vyos-hostsd.service +++ b/src/systemd/vyos-hostsd.service @@ -1,6 +1,12 @@ [Unit] Description=VyOS DNS configuration keeper + +# Without this option, lots of default dependencies are added, +# among them network.target, which creates a dependency cycle DefaultDependencies=no + +# Seemingly sensible way to say "as early as the system is ready" +# All vyos-hostsd needs is read/write mounted root After=systemd-remount-fs.service [Service] @@ -13,9 +19,13 @@ SyslogFacility=daemon Restart=on-failure -# Does't work but leave it here +# Does't work in Jessie but leave it here User=root Group=vyattacfg [Install] + +# Note: After= doesn't actually create a dependency, +# it just sets order for the case when both services are to start, +# and without RequiredBy it *does not* set vyos-hostsd to start. RequiredBy=cloud-init-local.service vyos-router.service -- cgit v1.2.3