summaryrefslogtreecommitdiff
path: root/src/op_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/op_mode')
-rwxr-xr-xsrc/op_mode/firewall.py85
-rwxr-xr-xsrc/op_mode/qos.py242
-rw-r--r--src/op_mode/show_techsupport_report.py23
3 files changed, 337 insertions, 13 deletions
diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py
index 15fbb65a2..c197ca434 100755
--- a/src/op_mode/firewall.py
+++ b/src/op_mode/firewall.py
@@ -63,10 +63,10 @@ def get_nftables_details(family, hook, priority):
aux=''
if hook == 'name' or hook == 'ipv6-name':
- command = f'sudo nft list chain {suffix} vyos_filter {name_prefix}{priority}'
+ command = f'nft list chain {suffix} vyos_filter {name_prefix}{priority}'
else:
up_hook = hook.upper()
- command = f'sudo nft list chain {suffix} vyos_filter VYOS_{aux}{up_hook}_{priority}'
+ command = f'nft list chain {suffix} vyos_filter VYOS_{aux}{up_hook}_{priority}'
try:
results = cmd(command)
@@ -90,12 +90,42 @@ def get_nftables_details(family, hook, priority):
out[rule_id] = rule
return out
+def get_nftables_state_details(family):
+ if family == 'ipv6':
+ suffix = 'ip6'
+ name_suffix = 'POLICY6'
+ elif family == 'ipv4':
+ suffix = 'ip'
+ name_suffix = 'POLICY'
+ else:
+ # no state policy for bridge
+ return {}
+
+ command = f'nft list chain {suffix} vyos_filter VYOS_STATE_{name_suffix}'
+ try:
+ results = cmd(command)
+ except:
+ return {}
+
+ out = {}
+ for line in results.split('\n'):
+ rule = {}
+ for state in ['established', 'related', 'invalid']:
+ if state in line:
+ counter_search = re.search(r'counter packets (\d+) bytes (\d+)', line)
+ if counter_search:
+ rule['packets'] = counter_search[1]
+ rule['bytes'] = counter_search[2]
+ rule['conditions'] = re.sub(r'(\b(counter packets \d+ bytes \d+|drop|reject|return|log)\b|comment "[\w\-]+")', '', line).strip()
+ out[state] = rule
+ return out
+
def get_nftables_group_members(family, table, name):
prefix = 'ip6' if family == 'ipv6' else 'ip'
out = []
try:
- results_str = cmd(f'sudo nft -j list set {prefix} {table} {name}')
+ results_str = cmd(f'nft -j list set {prefix} {table} {name}')
results = json.loads(results_str)
except:
return out
@@ -172,6 +202,34 @@ def output_firewall_name(family, hook, priority, firewall_conf, single_rule_id=N
rows[rows.index(i)].pop(1)
print(tabulate.tabulate(rows, header) + '\n')
+def output_firewall_state_policy(family):
+ if family == 'bridge':
+ return {}
+ print(f'\n---------------------------------\n{family} State Policy\n')
+
+ details = get_nftables_state_details(family)
+ rows = []
+
+ for state, state_conf in details.items():
+ row = [state, state_conf['conditions']]
+ row.append(state_conf.get('packets', 0))
+ row.append(state_conf.get('bytes', 0))
+ row.append(state_conf.get('conditions'))
+ rows.append(row)
+
+ if rows:
+ if args.rule:
+ rows.pop()
+
+ if args.detail:
+ header = ['State', 'Conditions', 'Packets', 'Bytes']
+ output_firewall_vertical(rows, header)
+ else:
+ header = ['State', 'Packets', 'Bytes', 'Conditions']
+ for i in rows:
+ rows[rows.index(i)].pop(1)
+ print(tabulate.tabulate(rows, header) + '\n')
+
def output_firewall_name_statistics(family, hook, prior, prior_conf, single_rule_id=None):
print(f'\n---------------------------------\n{family} Firewall "{hook} {prior}"\n')
@@ -305,6 +363,10 @@ def show_firewall():
return
for family in ['ipv4', 'ipv6', 'bridge']:
+ if 'global_options' in firewall:
+ if 'state_policy' in firewall['global_options']:
+ output_firewall_state_policy(family)
+
if family in firewall:
for hook, hook_conf in firewall[family].items():
for prior, prior_conf in firewall[family][hook].items():
@@ -316,12 +378,17 @@ def show_firewall_family(family):
conf = Config()
firewall = get_config_node(conf)
- if not firewall or family not in firewall:
+ if not firewall:
return
- for hook, hook_conf in firewall[family].items():
- for prior, prior_conf in firewall[family][hook].items():
- output_firewall_name(family, hook, prior, prior_conf)
+ if 'global_options' in firewall:
+ if 'state_policy' in firewall['global_options']:
+ output_firewall_state_policy(family)
+
+ if family in firewall:
+ for hook, hook_conf in firewall[family].items():
+ for prior, prior_conf in firewall[family][hook].items():
+ output_firewall_name(family, hook, prior, prior_conf)
def show_firewall_name(family, hook, priority):
print('Ruleset Information')
@@ -622,6 +689,10 @@ def show_statistics():
return
for family in ['ipv4', 'ipv6', 'bridge']:
+ if 'global_options' in firewall:
+ if 'state_policy' in firewall['global_options']:
+ output_firewall_state_policy(family)
+
if family in firewall:
for hook, hook_conf in firewall[family].items():
for prior, prior_conf in firewall[family][hook].items():
diff --git a/src/op_mode/qos.py b/src/op_mode/qos.py
new file mode 100755
index 000000000..b8ca149a0
--- /dev/null
+++ b/src/op_mode/qos.py
@@ -0,0 +1,242 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# This script parses output from the 'tc' command and provides table or list output
+import sys
+import typing
+import json
+from tabulate import tabulate
+
+import vyos.opmode
+from vyos.configquery import op_mode_config_dict
+from vyos.utils.process import cmd
+from vyos.utils.network import interface_exists
+
+def detailed_output(dataset, headers):
+ for data in dataset:
+ adjusted_rule = data + [""] * (len(headers) - len(data)) # account for different header length, like default-action
+ transformed_rule = [[header, adjusted_rule[i]] for i, header in enumerate(headers) if i < len(adjusted_rule)] # create key-pair list from headers and rules lists; wrap at 100 char
+
+ print(tabulate(transformed_rule, tablefmt="presto"))
+ print()
+
+def get_tc_info(interface_dict, interface_name, policy_type):
+ policy_name = interface_dict.get(interface_name, {}).get('egress')
+ if not policy_name:
+ return None, None
+
+ class_dict = op_mode_config_dict(['qos', 'policy', policy_type, policy_name], key_mangling=('-', '_'),
+ get_first_key=True)
+ if not class_dict:
+ return None, None
+
+ return policy_name, class_dict
+
+def format_data_type(num, suffix):
+ if num < 10**3:
+ return f"{num} {suffix}"
+ elif num < 10**6:
+ return f"{num / 10**3:.3f} K{suffix}"
+ elif num < 10**9:
+ return f"{num / 10**6:.3f} M{suffix}"
+ elif num < 10**12:
+ return f"{num / 10**9:.3f} G{suffix}"
+ elif num < 10**15:
+ return f"{num / 10**12:.3f} T{suffix}"
+ elif num < 10**18:
+ return f"{num / 10**15:.3f} P{suffix}"
+ else:
+ return f"{num / 10**18:.3f} E{suffix}"
+
+def show_shaper(raw: bool, ifname: typing.Optional[str], classn: typing.Optional[str], detail: bool):
+ # Scope which interfaces will output data
+ if ifname:
+ if not interface_exists(ifname):
+ raise vyos.opmode.Error(f"{ifname} does not exist!")
+
+ interface_dict = {ifname: op_mode_config_dict(['qos', 'interface', ifname], key_mangling=('-', '_'),
+ get_first_key=True)}
+ if not interface_dict[ifname]:
+ raise vyos.opmode.Error(f"QoS is not applied to {ifname}!")
+
+ else:
+ interface_dict = op_mode_config_dict(['qos', 'interface'], key_mangling=('-', '_'),
+ get_first_key=True)
+ if not interface_dict:
+ raise vyos.opmode.Error(f"QoS is not applied to any interface!")
+
+
+ raw_dict = {'qos': {}}
+ for i in interface_dict.keys():
+ interface_name = i
+ output_list = []
+ output_dict = {'classes': {}}
+ raw_dict['qos'][interface_name] = {}
+
+ # Get configuration node data
+ policy_name, class_dict = get_tc_info(interface_dict, interface_name, 'shaper')
+ if not policy_name:
+ continue
+
+ class_data = json.loads(cmd(f"tc -j -s class show dev {i}"))
+ qdisc_data = json.loads(cmd(f"tc -j qdisc show dev {i}"))
+
+ if class_dict:
+ # Gather qdisc information (e.g. Queue Type)
+ qdisc_dict = {}
+ for qdisc in qdisc_data:
+ if qdisc.get('root'):
+ qdisc_dict['root'] = qdisc
+ continue
+
+ class_id = int(qdisc.get('parent').split(':')[1], 16)
+
+ if class_dict.get('class', {}).get(str(class_id)):
+ qdisc_dict[str(class_id)] = qdisc
+ else:
+ qdisc_dict['default'] = qdisc
+
+ # Gather class information
+ for classes in class_data:
+ if classes.get('rate'):
+ class_id = int(classes.get('handle').split(':')[1], 16)
+
+ # Get name of class
+ if classes.get('root'):
+ class_name = 'root'
+ output_dict['classes'][class_name] = {}
+ elif class_dict.get('class', {}).get(str(class_id)):
+ class_name = str(class_id)
+ output_dict['classes'][class_name] = {}
+ else:
+ class_name = 'default'
+ output_dict['classes'][class_name] = {}
+
+ if classn:
+ if classn != class_name and class_name != 'default' and class_name != 'root':
+ output_dict['classes'].pop(class_name, None)
+ continue
+
+ tmp = output_dict['classes'][class_name]
+
+ tmp['interface_name'] = interface_name
+ tmp['policy_name'] = policy_name
+ tmp['direction'] = 'egress'
+ tmp['class_name'] = class_name
+ tmp['queue_type'] = qdisc_dict.get(class_name, {}).get('kind')
+ tmp['rate'] = str(round(int(classes.get('rate'))*8))
+ tmp['ceil'] = str(round(int(classes.get('ceil'))*8))
+ tmp['bytes'] = classes.get('stats', {}).get('bytes', 0)
+ tmp['packets'] = classes.get('stats', {}).get('packets', 0)
+ tmp['drops'] = classes.get('stats', {}).get('drops', 0)
+ tmp['queued'] = classes.get('stats', {}).get('backlog', 0)
+ tmp['overlimits'] = classes.get('stats', {}).get('overlimits', 0)
+ tmp['requeues'] = classes.get('stats', {}).get('requeues', 0)
+ tmp['lended'] = classes.get('stats', {}).get('lended', 0)
+ tmp['borrowed'] = classes.get('stats', {}).get('borrowed', 0)
+ tmp['giants'] = classes.get('stats', {}).get('giants', 0)
+
+ output_dict['classes'][class_name] = tmp
+ raw_dict['qos'][interface_name][class_name] = tmp
+
+ # Skip printing of values for this interface. All interfaces will be returned in a single dictionary if 'raw' is called
+ if raw:
+ continue
+
+ # Default class may be out of order in original JSON. This moves it to the end
+ move_default = output_dict.get('classes', {}).pop('default', None)
+ if move_default:
+ output_dict.get('classes')['default'] = move_default
+
+ # Create the tables for outputs
+ for output in output_dict.get('classes'):
+ data = output_dict.get('classes').get(output)
+
+ # Add values for detailed (list view) output
+ if detail:
+ output_list.append([data['interface_name'],
+ data['policy_name'],
+ data['direction'],
+ data['class_name'],
+ data['queue_type'],
+ data['rate'],
+ data['ceil'],
+ data['bytes'],
+ data['packets'],
+ data['drops'],
+ data['queued'],
+ data['overlimits'],
+ data['requeues'],
+ data['lended'],
+ data['borrowed'],
+ data['giants']]
+ )
+ # Add values for normal (table view) output
+ else:
+ output_list.append([data['class_name'],
+ data['queue_type'],
+ format_data_type(int(data['rate']), 'b'),
+ format_data_type(int(data['ceil']), 'b'),
+ format_data_type(int(data['bytes']), 'B'),
+ data['packets'],
+ data['drops'],
+ data['queued']]
+ )
+
+ if output_list:
+ if detail:
+ # Headers for detailed (list view) output
+ headers = ['Interface', 'Policy Name', 'Direction', 'Class', 'Type', 'Bandwidth', 'Max. BW', 'Bytes', 'Packets', 'Drops', 'Queued', 'Overlimit', 'Requeue', 'Lended', 'Borrowed', 'Giants']
+
+ print('-' * 35)
+ print(f"Interface: {interface_name}")
+ print(f"Policy Name: {policy_name}\n")
+ detailed_output(output_list, headers)
+ else:
+ # Headers for table output
+ headers = ['Class', 'Type', 'Bandwidth', 'Max. BW', 'Bytes', 'Pkts', 'Drops', 'Queued']
+ align = ('left','left','right','right','right','right','right','right')
+
+ print('-' * 80)
+ print(f"Interface: {interface_name}")
+ print(f"Policy Name: {policy_name}\n")
+ print(tabulate(output_list, headers, colalign=align))
+ print(" \n")
+
+ # Return dictionary with all interfaces if 'raw' is called
+ if raw:
+ return raw_dict
+
+def show_cake(raw: bool, ifname: typing.Optional[str]):
+ if not interface_exists(ifname):
+ raise vyos.opmode.Error(f"{ifname} does not exist!")
+
+ cake_data = json.loads(cmd(f"tc -j -s qdisc show dev {ifname}"))[0]
+ if cake_data:
+ if cake_data.get('kind') == 'cake':
+ if raw:
+ return {'qos': {ifname: cake_data}}
+ else:
+ print(cmd(f"tc -s qdisc show dev {ifname}"))
+
+if __name__ == '__main__':
+ try:
+ res = vyos.opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, vyos.opmode.Error) as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/show_techsupport_report.py b/src/op_mode/show_techsupport_report.py
index 230fb252d..32cf67778 100644
--- a/src/op_mode/show_techsupport_report.py
+++ b/src/op_mode/show_techsupport_report.py
@@ -14,10 +14,12 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+import os
+import sys
from typing import List
-from vyos.utils.process import rc_cmd
from vyos.ifconfig import Section
from vyos.ifconfig import Interface
+from vyos.utils.process import rc_cmd
def print_header(command: str) -> None:
@@ -50,7 +52,15 @@ def execute_command(command: str, header_text: str) -> None:
print_header(header_text)
try:
rc, output = rc_cmd(command)
- print(output)
+ # Enable unbuffered print param to improve responsiveness of printed
+ # output to end user
+ print(output, flush=True)
+ # Exit gracefully when user interrupts program output
+ # Flush standard streams; redirect remaining output to devnull
+ # Resolves T5633: Bug #1 and 3
+ except (BrokenPipeError, KeyboardInterrupt):
+ os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())
+ sys.exit(1)
except Exception as e:
print(f"Error executing command: {command}")
print(f"Error message: {e}")
@@ -155,13 +165,13 @@ def show_route() -> None:
"show ip route supernets-only",
"show ip route table all",
"show ip route vrf all",
- "show ipv6 route bgp | head 108",
+ "show ipv6 route bgp | head -108",
"show ipv6 route cache",
"show ipv6 route connected",
"show ipv6 route forward",
"show ipv6 route isis",
"show ipv6 route kernel",
- "show ipv6 route ospf",
+ "show ipv6 route ospfv3",
"show ipv6 route rip",
"show ipv6 route static",
"show ipv6 route summary",
@@ -179,8 +189,9 @@ def show_firewall() -> None:
def show_system() -> None:
"""Prints system parameters."""
- execute_command(op('show system image version'), 'Show System Image Version')
- execute_command(op('show system image storage'), 'Show System Image Storage')
+ execute_command(op('show version'), 'Show System Version')
+ execute_command(op('show system storage'), 'Show System Storage')
+ execute_command(op('show system image details'), 'Show System Image Details')
def show_date() -> None: