From fdfd050431f786f9c9b7bfdcb51b5e8aca3f79f5 Mon Sep 17 00:00:00 2001 From: Thomas Mangin Date: Tue, 14 Apr 2020 09:43:19 +0100 Subject: vrrp: T2223: move VRRP within ifconfig Tidied up the code and moved it under VRRP in view to use with show-interface (which has VRRP filtering) No change in functionality --- src/conf_mode/vrrp.py | 21 ++++++------- src/op_mode/vrrp.py | 87 +++++---------------------------------------------- 2 files changed, 16 insertions(+), 92 deletions(-) (limited to 'src') diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py index b9b0405e2..f358891a5 100755 --- a/src/conf_mode/vrrp.py +++ b/src/conf_mode/vrrp.py @@ -22,16 +22,13 @@ from json import dumps from pathlib import Path import vyos.config -import vyos.keepalived from vyos import ConfigError from vyos.util import call from vyos.template import render +from vyos.ifconfig.vrrp import VRRP -daemon_file = "/etc/default/keepalived" -config_file = "/etc/keepalived/keepalived.conf" -config_dict_path = "/run/keepalived_config.dict" def get_config(): vrrp_groups = [] @@ -127,7 +124,7 @@ def get_config(): sync_groups.append(sync_group) # create a file with dict with proposed configuration - with open("{}.temp".format(config_dict_path), 'w') as dict_file: + with open("{}.temp".format(VRRP.location['vyos']), 'w') as dict_file: dict_file.write(dumps({'vrrp_groups': vrrp_groups, 'sync_groups': sync_groups})) return (vrrp_groups, sync_groups) @@ -212,9 +209,9 @@ def generate(data): # Filter out disabled groups vrrp_groups = list(filter(lambda x: x["disable"] is not True, vrrp_groups)) - render(config_file, 'vrrp/keepalived.conf.tmpl', - {"groups": vrrp_groups, "sync_groups": sync_groups}) - render(daemon_file, 'vrrp/daemon.tmpl', {}) + render(VRRP.location['config'], 'vrrp/keepalived.conf.tmpl', + {"groups": vrrp_groups, "sync_groups": sync_groups}) + render(VRRP.location['daemon'], 'vrrp/daemon.tmpl', {}) return None @@ -223,12 +220,12 @@ def apply(data): if vrrp_groups: # safely rename a temporary file with configuration dict try: - dict_file = Path("{}.temp".format(config_dict_path)) - dict_file.rename(Path(config_dict_path)) + dict_file = Path("{}.temp".format(VRRP.location['vyos'])) + dict_file.rename(Path(VRRP.location['vyos'])) except Exception as err: print("Unable to rename the file with keepalived config for FIFO pipe: {}".format(err)) - if not vyos.keepalived.vrrp_running(): + if not VRRP.is_running(): print("Starting the VRRP process") ret = call("sudo systemctl restart keepalived.service") else: @@ -241,7 +238,7 @@ def apply(data): # VRRP is removed in the commit print("Stopping the VRRP process") call("sudo systemctl stop keepalived.service") - os.unlink(config_file) + os.unlink(VRRP.location['daemon']) return None diff --git a/src/op_mode/vrrp.py b/src/op_mode/vrrp.py index 8a993f92c..923cfa4d4 100755 --- a/src/op_mode/vrrp.py +++ b/src/op_mode/vrrp.py @@ -24,81 +24,8 @@ import tabulate import vyos.keepalived import vyos.util -config_dict_path = '/run/keepalived_config.dict' - - -# get disabled instances from a config -def vrrp_get_disabled(): - # read the dictionary file with configuration - with open(config_dict_path, 'r') as dict_file: - vrrp_config_dict = json.load(dict_file) - vrrp_disabled = [] - # add disabled groups to the list - for vrrp_group in vrrp_config_dict['vrrp_groups']: - if vrrp_group['disable']: - vrrp_disabled.append([vrrp_group['name'], vrrp_group['interface'], vrrp_group['vrid'], 'DISABLED', '']) - # return list with disabled instances - return vrrp_disabled - - -def print_summary(): - try: - vyos.keepalived.force_json_dump() - # Wait for keepalived to produce the data - # Replace with inotify or similar if it proves problematic - time.sleep(0.2) - json_data = vyos.keepalived.get_json_data() - vyos.keepalived.remove_vrrp_data("json") - except: - print("VRRP information is not available") - sys.exit(1) - - groups = [] - for group in json_data: - data = group["data"] - - name = data["iname"] - - ltrans_timestamp = float(data["last_transition"]) - ltrans_time = vyos.util.seconds_to_human(int(time.time() - ltrans_timestamp)) - - interface = data["ifp_ifname"] - vrid = data["vrid"] - - state = vyos.keepalived.decode_state(data["state"]) - - row = [name, interface, vrid, state, ltrans_time] - groups.append(row) - - # add to the active list disabled instances - groups.extend(vrrp_get_disabled()) - headers = ["Name", "Interface", "VRID", "State", "Last Transition"] - output = tabulate.tabulate(groups, headers) - print(output) - - -def print_statistics(): - try: - vyos.keepalived.force_stats_dump() - time.sleep(0.2) - output = vyos.keepalived.get_statistics() - print(output) - vyos.keepalived.remove_vrrp_data("stats") - except: - print("VRRP statistics are not available") - sys.exit(1) - - -def print_state_data(): - try: - vyos.keepalived.force_state_data_dump() - time.sleep(0.2) - output = vyos.keepalived.get_state_data() - print(output) - vyos.keepalived.remove_vrrp_data("state") - except: - print("VRRP information is not available") - sys.exit(1) +from vyos.ifconfig.vrrp import VRRP +from vyos.ifconfig.vrrp import VRRPError parser = argparse.ArgumentParser() @@ -110,16 +37,16 @@ group.add_argument("-d", "--data", action="store_true", help="Print detailed VRR args = parser.parse_args() # Exit early if VRRP is dead or not configured -if not vyos.keepalived.vrrp_running(): - print("VRRP is not running") +if not VRRP.is_running(): + print('VRRP is not running') sys.exit(0) if args.summary: - print_summary() + print(VRRP.format(VRRP.collect('json'))) elif args.statistics: - print_statistics() + print(VRRP.collect('stats')) elif args.data: - print_state_data() + print(VRRP.collect('state')) else: parser.print_help() sys.exit(1) -- cgit v1.2.3 From e0aa8aa20b835f17a74e0cf2a0dc2ca6e7823a3c Mon Sep 17 00:00:00 2001 From: Thomas Mangin Date: Tue, 14 Apr 2020 09:46:46 +0100 Subject: ifconfig: T2223: group all operational commands All operational command are moved within an Operational class and an inherited on for wireguard. --- python/vyos/ifconfig/__init__.py | 4 + python/vyos/ifconfig/interface.py | 80 ++++-------- python/vyos/ifconfig/operational.py | 179 +++++++++++++++++++++++++++ python/vyos/ifconfig/section.py | 18 ++- python/vyos/ifconfig/wireguard.py | 239 +++++++++++++++++++----------------- src/op_mode/wireguard.py | 2 +- 6 files changed, 346 insertions(+), 176 deletions(-) create mode 100644 python/vyos/ifconfig/operational.py (limited to 'src') diff --git a/python/vyos/ifconfig/__init__.py b/python/vyos/ifconfig/__init__.py index cd1696ca1..4d98901b7 100644 --- a/python/vyos/ifconfig/__init__.py +++ b/python/vyos/ifconfig/__init__.py @@ -15,7 +15,11 @@ from vyos.ifconfig.section import Section +from vyos.ifconfig.control import Control from vyos.ifconfig.interface import Interface +from vyos.ifconfig.operational import Operational +from vyos.ifconfig.dhcp import DHCP +from vyos.ifconfig.vrrp import VRRP from vyos.ifconfig.bond import BondIf from vyos.ifconfig.bridge import BridgeIf diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 43f823eca..32ce1a80c 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -14,23 +14,19 @@ # License along with this library. If not, see . import os -import re import json -import glob -import time -from time import sleep -from os.path import isfile from copy import deepcopy -from datetime import timedelta -from hurry.filesize import size, alternative -from ipaddress import IPv4Network, IPv6Address, IPv6Network -from netifaces import ifaddresses, AF_INET, AF_INET6 -from tabulate import tabulate +from ipaddress import IPv4Network +from ipaddress import IPv6Address +from ipaddress import IPv6Network +from netifaces import ifaddresses +# this is not the same as socket.AF_INET/INET6 +from netifaces import AF_INET +from netifaces import AF_INET6 -from vyos.util import mac2eui64 from vyos import ConfigError -from vyos.ifconfig.dhcp import DHCP +from vyos.util import mac2eui64 from vyos.validate import is_ipv4 from vyos.validate import is_ipv6 from vyos.validate import is_intf_addr_assigned @@ -42,9 +38,17 @@ from vyos.validate import assert_positive from vyos.validate import assert_range from vyos.ifconfig.control import Control +from vyos.ifconfig.dhcp import DHCP +from vyos.ifconfig.vrrp import VRRP +from vyos.ifconfig.operational import Operational class Interface(Control): + # This is the class which will be used to create + # self.operational, it allows subclasses, such as + # WireGuard to modify their display behaviour + OperationalClass = Operational + options = [] required = [] default = { @@ -154,6 +158,10 @@ class Interface(Control): }, } + @classmethod + def exists(cls, ifname): + return os.path.exists(f'/sys/class/net/{ifname}') + def __init__(self, ifname, **kargs): """ This is the base interface class which supports basic IP/MAC address @@ -184,14 +192,15 @@ class Interface(Control): # we must have updated config before initialising the Interface super().__init__(**kargs) + self.ifname = ifname self.dhcp = DHCP(ifname) - if not os.path.exists('/sys/class/net/{}'.format(self.config['ifname'])): + if not self.exists(ifname): # Any instance of Interface, such as Interface('eth0') # can be used safely to access the generic function in this class # as 'type' is unset, the class can not be created if not self.config['type']: - raise Exception('interface "{}" not found'.format(self.config['ifname'])) + raise Exception(f'interface "{ifname}" not found') # Should an Instance of a child class (EthernetIf, DummyIf, ..) # be required, then create should be set to False to not accidentally create it. @@ -211,6 +220,9 @@ class Interface(Control): # list of assigned IP addresses self._addr = [] + self.operational = self.OperationalClass(ifname) + self.vrrp = VRRP(ifname) + def _create(self): cmd = 'ip link add dev {ifname} type {type}'.format(**self.config) self._cmd(cmd) @@ -556,19 +568,6 @@ class Interface(Control): """ return self.set_interface('admin_state', state) - def get_oper_state(self): - """ - Get interface operational state - - Example: - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').get_oper_sate() - 'up' - """ - # https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-net - # "unknown", "notpresent", "down", "lowerlayerdown", "testing", "dormant", "up" - return self.get_interface('oper_state') - def set_proxy_arp(self, enable): """ Set per interface proxy ARP configuration @@ -713,30 +712,3 @@ class Interface(Control): if is_intf_addr_assigned(self.config['ifname'], addr): cmd = 'ip addr del "{}" dev "{}"'.format(addr, self.config['ifname']) return self._cmd(cmd) - - def op_show_interface_stats(self): - stats = self.get_interface_stats() - rx = [['bytes','packets','errors','dropped','overrun','mcast'],[stats['rx_bytes'],stats['rx_packets'],stats['rx_errors'],stats['rx_dropped'],stats['rx_over_errors'],stats['multicast']]] - tx = [['bytes','packets','errors','dropped','carrier','collisions'],[stats['tx_bytes'],stats['tx_packets'],stats['tx_errors'],stats['tx_dropped'],stats['tx_carrier_errors'],stats['collisions']]] - output = "RX: \n" - output += tabulate(rx,headers="firstrow",numalign="right",tablefmt="plain") - output += "\n\nTX: \n" - output += tabulate(tx,headers="firstrow",numalign="right",tablefmt="plain") - print(' '.join(('\n'+output.lstrip()).splitlines(True))) - - def get_interface_stats(self): - interface_stats = dict() - devices = [f for f in glob.glob("/sys/class/net/**/statistics")] - for dev_path in devices: - metrics = [f for f in glob.glob(dev_path +"/**")] - dev = re.findall(r"/sys/class/net/(.*)/statistics",dev_path)[0] - dev_dict = dict() - for metric_path in metrics: - metric = metric_path.replace(dev_path+"/","") - if isfile(metric_path): - data = open(metric_path, 'r').read()[:-1] - dev_dict[metric] = int(data) - interface_stats[dev] = dev_dict - - return interface_stats[self.config['ifname']] - diff --git a/python/vyos/ifconfig/operational.py b/python/vyos/ifconfig/operational.py new file mode 100644 index 000000000..5a292f90b --- /dev/null +++ b/python/vyos/ifconfig/operational.py @@ -0,0 +1,179 @@ +# 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 os +from time import time +from datetime import datetime +from functools import reduce + +from tabulate import tabulate + +from vyos.ifconfig import Control + + +class Operational(Control): + """ + A class able to load Interface statistics + """ + + cache_magic = 'XYZZYX' + + _stat_names = { + 'rx': ['bytes', 'packets', 'errors', 'dropped', 'overrun', 'mcast'], + 'tx': ['bytes', 'packets', 'errors', 'dropped', 'carrier', 'collisions'], + } + + _stats_dir = { + 'rx': ['rx_bytes', 'rx_packets', 'rx_errors', 'rx_dropped', 'rx_over_errors', 'multicast'], + 'tx': ['tx_bytes', 'tx_packets', 'tx_errors', 'tx_dropped', 'tx_carrier_errors', 'collisions'], + } + + # a list made of the content of _stats_dir['rx'] + _stats_dir['tx'] + _stats_all = reduce(lambda x, y: x+y, _stats_dir.values()) + + # this is not an interface but will be able to be controlled like one + _sysfs_get = { + 'oper_state':{ + 'location': '/sys/class/net/{ifname}/operstate', + }, + } + + + @classmethod + def cachefile (cls, ifname): + # the file where we are saving the counters + return f'/var/run/vyatta/{ifname}.stats' + + + def __init__(self, ifname): + """ + Operational provide access to the counters of an interface + It behave like an interface when it comes to access sysfs + + interface is an instance of the interface for which we want + to look at (a subclass of Interface, such as EthernetIf) + """ + + # add a self.config to minic Interface behaviour and make + # coding similar. Perhaps part of class Interface could be + # moved into a shared base class. + self.config = { + 'ifname': ifname, + 'create': False, + 'debug': False, + } + super().__init__(**self.config) + self.ifname = ifname + + # adds all the counters of an interface + for stat in self._stats_all: + self._sysfs_get[stat] = { + 'location': '/sys/class/net/{ifname}/statistics/'+stat, + } + + def get_state(self): + """ + Get interface operational state + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').operational.get_sate() + 'up' + """ + # https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-net + # "unknown", "notpresent", "down", "lowerlayerdown", "testing", "dormant", "up" + return self.get_interface('oper_state') + + @classmethod + def strtime (cls, epoc): + """ + represent an epoc/unix date in the format used by operation commands + """ + return datetime.fromtimestamp(epoc).strftime("%a %b %d %R:%S %Z %Y") + + def save_counters(self, stats): + """ + record the provided stats to a file keeping vyatta compatibility + """ + + with open(self.cachefile(self.ifname), 'w') as f: + f.write(self.cache_magic) + f.write('\n') + f.write(str(int(time()))) + f.write('\n') + for k,v in stats.items(): + if v: + f.write(f'{k},{v}\n') + + def load_counters(self): + """ + load the stats from a file keeping vyatta compatibility + return a dict() with the value for each interface counter for the cache + """ + ifname = self.config['ifname'] + + stats = {} + no_stats = {} + for name in self._stats_all: + stats[name] = 0 + no_stats[name] = 0 + + try: + with open(self.cachefile(self.ifname),'r') as f: + magic = f.readline().strip() + if magic != self.cache_magic: + print(f'bad magic {ifname}') + return no_stats + stats['timestamp'] = f.readline().strip() + for line in f: + k, v = line.split(',') + stats[k] = int(v) + return stats + except IOError: + return stats + + def clear_counters(self, counters=None): + clear = self._stats_all if counters is None else [] + stats = self.load_counters() + for counter, value in stats.items(): + stats[counter] = 0 if counter in clear else value + self.save_counters(stats) + + def reset_counters(self): + os.remove(self.cachefile(self.ifname)) + + def get_stats(self): + """ return a dict() with the value for each interface counter """ + stats = {} + for counter in self._stats_all: + stats[counter] = int(self.get_interface(counter)) + return stats + + def formated_stats(self, indent=4): + tabs = [] + stats = self.get_stats() + for rtx in self._stats_dir: + tabs.append([f'{rtx.upper()}:', ] + [_ for _ in self._stat_names[rtx]]) + tabs.append(['', ] + [stats[_] for _ in self._stats_dir[rtx]]) + + s = tabulate( + tabs, + stralign="right", + numalign="right", + tablefmt="plain" + ) + + p = ' '*indent + return f'{p}' + s.replace('\n', f'\n{p}') diff --git a/python/vyos/ifconfig/section.py b/python/vyos/ifconfig/section.py index ab340d247..092236fef 100644 --- a/python/vyos/ifconfig/section.py +++ b/python/vyos/ifconfig/section.py @@ -69,7 +69,14 @@ class Section: if name in cls._prefixes: return cls._prefixes[name].definition['section'] return '' - + + @classmethod + def sections(cls): + """ + return all the sections we found under 'set interfaces' + """ + return list(set([cls._prefixes[_].definition['section'] for _ in cls._prefixes])) + @classmethod def klass(cls, name, vlan=True): name = cls._basename(name, vlan) @@ -80,7 +87,8 @@ class Section: @classmethod def _intf_under_section (cls,section=''): """ - return a generator with the name of the interface which are under a section + return a generator with the name of the configured interface + which are under a section """ interfaces = netifaces.interfaces() @@ -97,7 +105,7 @@ class Section: @classmethod def interfaces(cls, section=''): """ - return a list of the name of the interface which are under a section + return a list of the name of the configured interface which are under a section if no section is provided, then it returns all configured interfaces """ return list(cls._intf_under_section(section)) @@ -105,7 +113,7 @@ class Section: @classmethod def _intf_with_feature(cls, feature=''): """ - return a generator with the name of the interface which have + return a generator with the name of the configured interface which have a particular feature set in their definition such as: bondable, broadcast, bridgeable, ... """ @@ -116,7 +124,7 @@ class Section: @classmethod def feature(cls, feature=''): """ - return list with the name of the interface which have + return list with the name of the configured interface which have a particular feature set in their definition such as: bondable, broadcast, bridgeable, ... """ diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py index e2b8a5924..ff945c9d0 100644 --- a/python/vyos/ifconfig/wireguard.py +++ b/python/vyos/ifconfig/wireguard.py @@ -18,13 +18,134 @@ import os import time from datetime import timedelta +from hurry.filesize import size +from hurry.filesize import alternative + from vyos.config import Config -from vyos.ifconfig.interface import Interface -from hurry.filesize import size,alternative +from vyos.ifconfig import Interface +from vyos.ifconfig import Operational + + +class WireGuardOperational(Operational): + def _dump(self): + """Dump wireguard data in a python friendly way.""" + last_device = None + output = {} + + # Dump wireguard connection data + _f = self._cmd('wg show all dump') + for line in _f.split('\n'): + if not line: + # Skip empty lines and last line + continue + items = line.split('\t') + + if last_device != items[0]: + # We are currently entering a new node + device, private_key, public_key, listen_port, fw_mark = items + last_device = device + + output[device] = { + 'private_key': None if private_key == '(none)' else private_key, + 'public_key': None if public_key == '(none)' else public_key, + 'listen_port': int(listen_port), + 'fw_mark': None if fw_mark == 'off' else int(fw_mark), + 'peers': {}, + } + else: + # We are entering a peer + device, public_key, preshared_key, endpoint, allowed_ips, latest_handshake, transfer_rx, transfer_tx, persistent_keepalive = items + if allowed_ips == '(none)': + allowed_ips = [] + else: + allowed_ips = allowed_ips.split('\t') + output[device]['peers'][public_key] = { + 'preshared_key': None if preshared_key == '(none)' else preshared_key, + 'endpoint': None if endpoint == '(none)' else endpoint, + 'allowed_ips': allowed_ips, + 'latest_handshake': None if latest_handshake == '0' else int(latest_handshake), + 'transfer_rx': int(transfer_rx), + 'transfer_tx': int(transfer_tx), + 'persistent_keepalive': None if persistent_keepalive == 'off' else int(persistent_keepalive), + } + return output + + def show_interface(self): + wgdump = self._dump().get(self.config['ifname'], None) + + c = Config() + + c.set_level(["interfaces", "wireguard", self.config['ifname']]) + description = c.return_effective_value(["description"]) + ips = c.return_effective_values(["address"]) + + answer = "interface: {}\n".format(self.config['ifname']) + if (description): + answer += " description: {}\n".format(description) + if (ips): + answer += " address: {}\n".format(", ".join(ips)) + + answer += " public key: {}\n".format(wgdump['public_key']) + answer += " private key: (hidden)\n" + answer += " listening port: {}\n".format(wgdump['listen_port']) + answer += "\n" + + for peer in c.list_effective_nodes(["peer"]): + if wgdump['peers']: + pubkey = c.return_effective_value(["peer", peer, "pubkey"]) + if pubkey in wgdump['peers']: + wgpeer = wgdump['peers'][pubkey] + + answer += " peer: {}\n".format(peer) + answer += " public key: {}\n".format(pubkey) + + """ figure out if the tunnel is recently active or not """ + status = "inactive" + if (wgpeer['latest_handshake'] is None): + """ no handshake ever """ + status = "inactive" + else: + if int(wgpeer['latest_handshake']) > 0: + delta = timedelta(seconds=int( + time.time() - wgpeer['latest_handshake'])) + answer += " latest handshake: {}\n".format(delta) + if (time.time() - int(wgpeer['latest_handshake']) < (60*5)): + """ Five minutes and the tunnel is still active """ + status = "active" + else: + """ it's been longer than 5 minutes """ + status = "inactive" + elif int(wgpeer['latest_handshake']) == 0: + """ no handshake ever """ + status = "inactive" + answer += " status: {}\n".format(status) + + if wgpeer['endpoint'] is not None: + answer += " endpoint: {}\n".format(wgpeer['endpoint']) + + if wgpeer['allowed_ips'] is not None: + answer += " allowed ips: {}\n".format( + ",".join(wgpeer['allowed_ips']).replace(",", ", ")) + + if wgpeer['transfer_rx'] > 0 or wgpeer['transfer_tx'] > 0: + rx_size = size( + wgpeer['transfer_rx'], system=alternative) + tx_size = size( + wgpeer['transfer_tx'], system=alternative) + answer += " transfer: {} received, {} sent\n".format( + rx_size, tx_size) + + if wgpeer['persistent_keepalive'] is not None: + answer += " persistent keepalive: every {} seconds\n".format( + wgpeer['persistent_keepalive']) + answer += '\n' + return answer + super().formated_stats() @Interface.register class WireGuardIf(Interface): + OperationalClass = WireGuardOperational + default = { 'type': 'wireguard', 'port': 0, @@ -106,117 +227,3 @@ class WireGuardIf(Interface): cmd = "wg set {0} peer {1} remove".format( self.config['ifname'], str(peerkey)) return self._cmd(cmd) - - def op_show_interface(self): - wgdump = self._dump().get( - self.config['ifname'], None) - - c = Config() - c.set_level(["interfaces", "wireguard", self.config['ifname']]) - description = c.return_effective_value(["description"]) - ips = c.return_effective_values(["address"]) - - print ("interface: {}".format(self.config['ifname'])) - if (description): - print (" description: {}".format(description)) - - if (ips): - print (" address: {}".format(", ".join(ips))) - print (" public key: {}".format(wgdump['public_key'])) - print (" private key: (hidden)") - print (" listening port: {}".format(wgdump['listen_port'])) - print () - - for peer in c.list_effective_nodes(["peer"]): - if wgdump['peers']: - pubkey = c.return_effective_value(["peer", peer, "pubkey"]) - if pubkey in wgdump['peers']: - wgpeer = wgdump['peers'][pubkey] - - print (" peer: {}".format(peer)) - print (" public key: {}".format(pubkey)) - - """ figure out if the tunnel is recently active or not """ - status = "inactive" - if (wgpeer['latest_handshake'] is None): - """ no handshake ever """ - status = "inactive" - else: - if int(wgpeer['latest_handshake']) > 0: - delta = timedelta(seconds=int( - time.time() - wgpeer['latest_handshake'])) - print (" latest handshake: {}".format(delta)) - if (time.time() - int(wgpeer['latest_handshake']) < (60*5)): - """ Five minutes and the tunnel is still active """ - status = "active" - else: - """ it's been longer than 5 minutes """ - status = "inactive" - elif int(wgpeer['latest_handshake']) == 0: - """ no handshake ever """ - status = "inactive" - print (" status: {}".format(status)) - - if wgpeer['endpoint'] is not None: - print (" endpoint: {}".format(wgpeer['endpoint'])) - - if wgpeer['allowed_ips'] is not None: - print (" allowed ips: {}".format( - ",".join(wgpeer['allowed_ips']).replace(",", ", "))) - - if wgpeer['transfer_rx'] > 0 or wgpeer['transfer_tx'] > 0: - rx_size = size( - wgpeer['transfer_rx'], system=alternative) - tx_size = size( - wgpeer['transfer_tx'], system=alternative) - print (" transfer: {} received, {} sent".format( - rx_size, tx_size)) - - if wgpeer['persistent_keepalive'] is not None: - print (" persistent keepalive: every {} seconds".format( - wgpeer['persistent_keepalive'])) - print() - super().op_show_interface_stats() - - def _dump(self): - """Dump wireguard data in a python friendly way.""" - last_device = None - output = {} - - # Dump wireguard connection data - _f = self._cmd('wg show all dump') - for line in _f.split('\n'): - if not line: - # Skip empty lines and last line - continue - items = line.split('\t') - - if last_device != items[0]: - # We are currently entering a new node - device, private_key, public_key, listen_port, fw_mark = items - last_device = device - - output[device] = { - 'private_key': None if private_key == '(none)' else private_key, - 'public_key': None if public_key == '(none)' else public_key, - 'listen_port': int(listen_port), - 'fw_mark': None if fw_mark == 'off' else int(fw_mark), - 'peers': {}, - } - else: - # We are entering a peer - device, public_key, preshared_key, endpoint, allowed_ips, latest_handshake, transfer_rx, transfer_tx, persistent_keepalive = items - if allowed_ips == '(none)': - allowed_ips = [] - else: - allowed_ips = allowed_ips.split('\t') - output[device]['peers'][public_key] = { - 'preshared_key': None if preshared_key == '(none)' else preshared_key, - 'endpoint': None if endpoint == '(none)' else endpoint, - 'allowed_ips': allowed_ips, - 'latest_handshake': None if latest_handshake == '0' else int(latest_handshake), - 'transfer_rx': int(transfer_rx), - 'transfer_tx': int(transfer_tx), - 'persistent_keepalive': None if persistent_keepalive == 'off' else int(persistent_keepalive), - } - return output diff --git a/src/op_mode/wireguard.py b/src/op_mode/wireguard.py index 1b90f4fa7..297ba599d 100755 --- a/src/op_mode/wireguard.py +++ b/src/op_mode/wireguard.py @@ -148,7 +148,7 @@ if __name__ == '__main__': list_key_dirs() if args.showinterface: intf = WireGuardIf(args.showinterface, create=False, debug=False) - intf.op_show_interface() + print(intf.operational.show_interface()) if args.delkdir: if args.location: del_key_dir(args.location) -- cgit v1.2.3 From 9eb979664c29a6e34b765df3e38cdf6d4c718be1 Mon Sep 17 00:00:00 2001 From: Thomas Mangin Date: Tue, 14 Apr 2020 09:49:08 +0100 Subject: op_mode: T2223 python version of show_interfaces --- src/op_mode/show_interfaces.py | 266 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100755 src/op_mode/show_interfaces.py (limited to 'src') diff --git a/src/op_mode/show_interfaces.py b/src/op_mode/show_interfaces.py new file mode 100755 index 000000000..c5d4c8ac6 --- /dev/null +++ b/src/op_mode/show_interfaces.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python3 + +# Copyright 2017, 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 os +import re +import sys +import datetime +import argparse +from subprocess import Popen, PIPE, STDOUT +import netifaces + +from vyos.ifconfig import Section +from vyos.ifconfig import Interface +from vyos.ifconfig import VRRP +from vyos.util import cmd + + +# interfaces = Sections.reserved() +interfaces = ['eno', 'ens', 'enp', 'enx', 'eth', 'vmnet', 'lo', 'tun', 'wan', 'pppoe', 'pppoa', 'adsl'] +glob_ifnames = '/sys/class/net/({})*'.format('|'.join(interfaces)) + + +actions = {} +def register (name): + """ + decorator to register a function into actions with a name + it allows to use actions[name] to call the registered function + """ + def _register(function): + actions[name] = function + return function + return _register + + +def filtered_interfaces(ifnames, iftypes, vif, vrrp): + """ + get all the interfaces from the OS and returns them + ifnames can be used to filter which interfaces should be considered + + ifnames: a list of interfaces names to consider, empty do not filter + return an instance of the interface class + """ + allnames = Section.interfaces() + allnames.sort() + + vrrp_interfaces = VRRP.active_interfaces() if vrrp else [] + + for ifname in allnames: + if ifnames and ifname not in ifnames: + continue + + # return the class which can handle this interface name + klass = Section.klass(ifname) + # connect to the interface + interface = klass(ifname, create=False, debug=False) + + if iftypes and interface.definition['section'] not in iftypes: + continue + + if vif and not '.' in ifname: + continue + + if vrrp and ifname not in vrrp_interfaces: + continue + + yield interface + + +def split_text(text, used=0): + """ + take a string and attempt to split it to fit with the width of the screen + + text: the string to split + used: number of characted already used in the screen + """ + returned = Popen('stty size', stdout=PIPE, stderr=STDOUT, shell=True).communicate()[0].strip().split() + if len(returned) == 2: + rows, columns = [int(_) for _ in returned] + else: + rows, columns = (40, 80) + + desc_len = columns - used + + line = '' + for word in text.split(): + if len(line) + len(word) >= desc_len: + yield f'{line} {word}'[1:] + line = '' + line = f'{line} {word}' + yield line[1:] + + +def get_vrrp_intf(): + return [intf for intf in Section.interfaces() if intf.is_vrrp()] + + +def get_counter_val(clear, now): + """ + attempt to correct a counter if it wrapped, copied from perl + + clear: previous counter + now: the current counter + """ + # This function has to deal with both 32 and 64 bit counters + if clear == 0: + return now + + # device is using 64 bit values assume they never wrap + value = now - clear + if (now >> 32) != 0: + return value + + # The counter has rolled. If the counter has rolled + # multiple times since the clear value, then this math + # is meaningless. + if (value < 0): + value = (4294967296 - clear) + now + + return value + + +@register('help') +def usage(*args): + print(f"Usage: {sys.argv[0]} [intf=NAME|intf-type=TYPE|vif|vrrp] action=ACTION") + print(f" NAME = " + ' | '.join(Section.interfaces())) + print(f" TYPE = " + ' | '.join(Section.sections())) + print(f" ACTION = " + ' | '.join(actions)) + sys.exit(1) + + +@register('allowed') +def run_allowed(**kwarg): + sys.stdout.write(' '.join(Section.interfaces())) + + +@register('show') +def run_show_intf(ifnames, iftypes, vif, vrrp): + for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp): + cache = interface.operational.load_counters() + + out = cmd(f'ip addr show {interface.ifname}') + out = re.sub(f'^\d+:\s+','',out) + if re.search("link/tunnel6", out): + tunnel = cmd(f'ip -6 tun show {interface.ifname}') + # tun0: ip/ipv6 remote ::2 local ::1 encaplimit 4 hoplimit 64 tclass inherit flowlabel inherit (flowinfo 0x00000000) + tunnel = re.sub('.*encap', 'encap', tunnel) + out = re.sub('(\n\s+)(link/tunnel6)', f'\g<1>{tunnel}\g<1>\g<2>', out) + + print(out) + + timestamp = int(cache.get('timestamp', 0)) + if timestamp: + when = interface.operational.strtime(timestamp) + print(f' Last clear: {when}') + + description = interface.get_alias() + if description: + print(f' Description: {description}') + + print() + print(interface.operational.formated_stats()) + + +@register('show-brief') +def run_show_intf_brief(ifnames, iftypes, vif, vrrp): + format1 = '%-16s %-33s %-4s %s' + format2 = '%-16s %s' + + print('Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down') + print(format1 % ("Interface", "IP Address", "S/L", "Description")) + print(format1 % ("---------", "----------", "---", "-----------")) + + for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp): + oper_state = interface.operational.get_state() + admin_state = interface.get_admin_state() + + intf = [interface.ifname,] + oper = ['u', ] if oper_state in ('up', 'unknown') else ['A', ] + admin = ['u', ] if oper_state in ('up', 'unknown') else ['D', ] + addrs = [_ for _ in interface.get_addr() if not _.startswith('fe80::')] or ['-', ] + # do not ask me why 56, it was the number in the perl code ... + descs = list(split_text(interface.get_alias(),56)) + + while intf or oper or admin or addrs or descs: + i = intf.pop(0) if intf else '' + a = addrs.pop(0) if addrs else '' + d = descs.pop(0) if descs else '' + s = [oper.pop(0)] if oper else [] + l = [admin.pop(0)] if admin else [] + if len(a) < 33: + print(format1 % (i, a, '/'.join(s+l), d)) + else: + print(format2 % (i, a)) + print(format1 % ('', '', '/'.join(s+l), d)) + + +@register('show-count') +def run_show_counters(ifnames, iftypes, vif, vrrp): + formating = '%-12s %10s %10s %10s %10s' + print(formating % ('Interface', 'Rx Packets', 'Rx Bytes', 'Tx Packets', 'Tx Bytes')) + + for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp): + oper = interface.operational.get_state() + + if oper not in ('up','unknown'): + continue + + stats = interface.operational.get_stats() + cache = interface.operational.load_counters() + print(formating % ( + interface.ifname, + get_counter_val(cache['rx_packets'], stats['rx_packets']), + get_counter_val(cache['rx_bytes'], stats['rx_bytes']), + get_counter_val(cache['tx_packets'], stats['tx_packets']), + get_counter_val(cache['tx_bytes'], stats['tx_bytes']), + )) + + +@register('clear') +def run_clear_intf(intf, iftypes, vif, vrrp): + for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp): + print(f'Clearing {interface.ifname}') + interface = Interface(ifname, create=False, debug=False) + interface.operational.clear_counters() + + +@register('reset') +def run_reset_intf(intf, iftypes, vif, vrrp): + os.remove() + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(add_help=False, description='Show interface information') + parser.add_argument('--intf', action="store", type=str, default='', help='only show the specified interface(s)') + parser.add_argument('--intf-type', action="store", type=str, default='', help='only show the specified interface type') + parser.add_argument('--action', action="store", type=str, default='show', help='action to perform') + parser.add_argument('--vif', action='store_true', default=False, help="only show vif interfaces") + parser.add_argument('--vrrp', action='store_true', default=False, help="only show vrrp interfaces") + parser.add_argument('--help', action='store_true', default=False, help="show help") + + args = parser.parse_args() + + def missing(*args): + print('Invalid action [{args.action}]') + usage() + + actions.get(args.action, missing)( + [_ for _ in args.intf.split(' ') if _], + [_ for _ in args.intf_type.split(' ') if _], + args.vif, + args.vrrp + ) -- cgit v1.2.3 From 7d04bfbcc74e062b80b337753e7018a6af81e70c Mon Sep 17 00:00:00 2001 From: Thomas Mangin Date: Tue, 14 Apr 2020 17:10:20 +0100 Subject: op_mode: T2223: two cosmetic change and bug fix --- python/vyos/ifconfig/operational.py | 2 +- src/op_mode/show_interfaces.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/python/vyos/ifconfig/operational.py b/python/vyos/ifconfig/operational.py index 5a292f90b..d585c1873 100644 --- a/python/vyos/ifconfig/operational.py +++ b/python/vyos/ifconfig/operational.py @@ -142,7 +142,7 @@ class Operational(Control): stats[k] = int(v) return stats except IOError: - return stats + return no_stats def clear_counters(self, counters=None): clear = self._stats_all if counters is None else [] diff --git a/src/op_mode/show_interfaces.py b/src/op_mode/show_interfaces.py index c5d4c8ac6..8b6690b7d 100755 --- a/src/op_mode/show_interfaces.py +++ b/src/op_mode/show_interfaces.py @@ -20,7 +20,6 @@ import re import sys import datetime import argparse -from subprocess import Popen, PIPE, STDOUT import netifaces from vyos.ifconfig import Section @@ -87,7 +86,7 @@ def split_text(text, used=0): text: the string to split used: number of characted already used in the screen """ - returned = Popen('stty size', stdout=PIPE, stderr=STDOUT, shell=True).communicate()[0].strip().split() + returned = cmd('stty size') if len(returned) == 2: rows, columns = [int(_) for _ in returned] else: @@ -240,7 +239,9 @@ def run_clear_intf(intf, iftypes, vif, vrrp): @register('reset') def run_reset_intf(intf, iftypes, vif, vrrp): - os.remove() + for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp): + interface = Interface(ifname, create=False, debug=False) + interface.operational.reset_counters() if __name__ == '__main__': -- cgit v1.2.3