summaryrefslogtreecommitdiff
path: root/src/op_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/op_mode')
-rwxr-xr-xsrc/op_mode/bonding.py103
-rwxr-xr-xsrc/op_mode/cgnat.py96
-rw-r--r--src/op_mode/evpn.py46
-rwxr-xr-xsrc/op_mode/firewall.py12
-rwxr-xr-xsrc/op_mode/image_installer.py24
-rwxr-xr-xsrc/op_mode/image_manager.py28
-rwxr-xr-xsrc/op_mode/ipoe-control.py6
-rwxr-xr-xsrc/op_mode/nat.py2
-rw-r--r--src/op_mode/ntp.py164
-rwxr-xr-xsrc/op_mode/version.py6
10 files changed, 475 insertions, 12 deletions
diff --git a/src/op_mode/bonding.py b/src/op_mode/bonding.py
new file mode 100755
index 000000000..07bccbd4b
--- /dev/null
+++ b/src/op_mode/bonding.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2016-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 will parse 'sudo cat /proc/net/bonding/<interface name>' and return table output for lacp related info
+
+import subprocess
+import re
+import sys
+import typing
+from tabulate import tabulate
+
+import vyos.opmode
+from vyos.configquery import ConfigTreeQuery
+
+def list_to_dict(data, headers, basekey):
+ data_list = {basekey: []}
+
+ for row in data:
+ row_dict = {headers[i]: row[i] for i in range(len(headers))}
+ data_list[basekey].append(row_dict)
+
+ return data_list
+
+def show_lacp_neighbors(raw: bool, interface: typing.Optional[str]):
+ headers = ["Interface", "Member", "Local ID", "Remote ID"]
+ data = subprocess.run(f"cat /proc/net/bonding/{interface}", stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True, text=False).stdout.decode('utf-8')
+ if 'Bonding Mode: IEEE 802.3ad Dynamic link aggregation' not in data:
+ raise vyos.opmode.DataUnavailable(f"{interface} is not present or not configured with mode 802.3ad")
+
+ pattern = re.compile(
+ r"Slave Interface: (?P<member>\w+\d+).*?"
+ r"system mac address: (?P<local_id>[0-9a-f:]+).*?"
+ r"details partner lacp pdu:.*?"
+ r"system mac address: (?P<remote_id>[0-9a-f:]+)",
+ re.DOTALL
+ )
+
+ interfaces = []
+
+ for match in re.finditer(pattern, data):
+ member = match.group("member")
+ local_id = match.group("local_id")
+ remote_id = match.group("remote_id")
+ interfaces.append([interface, member, local_id, remote_id])
+
+ if raw:
+ return list_to_dict(interfaces, headers, 'lacp')
+ else:
+ return tabulate(interfaces, headers)
+
+def show_lacp_detail(raw: bool, interface: typing.Optional[str]):
+ headers = ["Interface", "Members", "Mode", "Rate", "System-MAC", "Hash"]
+ query = ConfigTreeQuery()
+
+ if interface:
+ intList = [interface]
+ else:
+ intList = query.list_nodes(['interfaces', 'bonding'])
+
+ bondList = []
+
+ for interface in intList:
+ data = subprocess.run(f"cat /proc/net/bonding/{interface}", stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True, text=False).stdout.decode('utf-8')
+ if 'Bonding Mode: IEEE 802.3ad Dynamic link aggregation' not in data:
+ continue
+
+ mode_active = "active" if "LACP active: on" in data else "passive"
+ lacp_rate = re.search(r"LACP rate: (\w+)", data).group(1) if re.search(r"LACP rate: (\w+)", data) else "N/A"
+ hash_policy = re.search(r"Transmit Hash Policy: (.+?) \(\d+\)", data).group(1) if re.search(r"Transmit Hash Policy: (.+?) \(\d+\)", data) else "N/A"
+ system_mac = re.search(r"System MAC address: ([0-9a-f:]+)", data).group(1) if re.search(r"System MAC address: ([0-9a-f:]+)", data) else "N/A"
+ if raw:
+ members = re.findall(r"Slave Interface: ([a-zA-Z0-9:_-]+)", data)
+ else:
+ members = ",".join(set(re.findall(r"Slave Interface: ([a-zA-Z0-9:_-]+)", data)))
+
+ bondList.append([interface, members, mode_active, lacp_rate, system_mac, hash_policy])
+
+ if raw:
+ return list_to_dict(bondList, headers, 'lacp')
+ else:
+ return tabulate(bondList, headers)
+
+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/cgnat.py b/src/op_mode/cgnat.py
new file mode 100755
index 000000000..9ad8f92f9
--- /dev/null
+++ b/src/op_mode/cgnat.py
@@ -0,0 +1,96 @@
+#!/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/>.
+
+import json
+import sys
+import typing
+
+from tabulate import tabulate
+
+import vyos.opmode
+
+from vyos.configquery import ConfigTreeQuery
+from vyos.utils.process import cmd
+
+CGNAT_TABLE = 'cgnat'
+
+
+def _get_raw_data(external_address: str = '', internal_address: str = '') -> list[dict]:
+ """Get CGNAT dictionary and filter by external or internal address if provided."""
+ cmd_output = cmd(f'nft --json list table ip {CGNAT_TABLE}')
+ data = json.loads(cmd_output)
+
+ elements = data['nftables'][2]['map']['elem']
+ allocations = []
+ for elem in elements:
+ internal = elem[0] # internal
+ external = elem[1]['concat'][0] # external
+ start_port = elem[1]['concat'][1]['range'][0]
+ end_port = elem[1]['concat'][1]['range'][1]
+ port_range = f'{start_port}-{end_port}'
+
+ if (internal_address and internal != internal_address) or (
+ external_address and external != external_address
+ ):
+ continue
+
+ allocations.append(
+ {
+ 'internal_address': internal,
+ 'external_address': external,
+ 'port_range': port_range,
+ }
+ )
+
+ return allocations
+
+
+def _get_formatted_output(allocations: list[dict]) -> str:
+ # Convert the list of dictionaries to a list of tuples for tabulate
+ headers = ['Internal IP', 'External IP', 'Port range']
+ data = [
+ (alloc['internal_address'], alloc['external_address'], alloc['port_range'])
+ for alloc in allocations
+ ]
+ output = tabulate(data, headers, numalign="left")
+ return output
+
+
+def show_allocation(
+ raw: bool,
+ external_address: typing.Optional[str],
+ internal_address: typing.Optional[str],
+) -> str:
+ config = ConfigTreeQuery()
+ if not config.exists('nat cgnat'):
+ raise vyos.opmode.UnconfiguredSubsystem('CGNAT is not configured')
+
+ if raw:
+ return _get_raw_data(external_address, internal_address)
+
+ else:
+ raw_data = _get_raw_data(external_address, internal_address)
+ return _get_formatted_output(raw_data)
+
+
+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/evpn.py b/src/op_mode/evpn.py
new file mode 100644
index 000000000..cae4ab9f5
--- /dev/null
+++ b/src/op_mode/evpn.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2016-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 is a helper to run VTYSH commands for "show evpn", allowing for the --raw flag to output JSON
+
+import sys
+import typing
+import json
+
+import vyos.opmode
+from vyos.utils.process import cmd
+
+def show_evpn(raw: bool, command: typing.Optional[str]):
+ if raw:
+ command = f"{command} json"
+ evpnDict = {}
+ try:
+ evpnDict['evpn'] = json.loads(cmd(f"vtysh -c '{command}'"))
+ except:
+ raise vyos.opmode.DataUnavailable(f"\"{command.replace(' json', '')}\" is invalid or has no JSON option")
+
+ return evpnDict
+ else:
+ return cmd(f"vtysh -c '{command}'")
+
+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/firewall.py b/src/op_mode/firewall.py
index 442c186cc..15fbb65a2 100755
--- a/src/op_mode/firewall.py
+++ b/src/op_mode/firewall.py
@@ -531,9 +531,15 @@ def show_firewall_group(name=None):
continue
for idx, member in enumerate(members):
- val = member.get('val', 'N/D')
- timeout = str(member.get('timeout', 'N/D'))
- expires = str(member.get('expires', 'N/D'))
+ if isinstance(member, str):
+ # Only member, and no timeout:
+ val = member
+ timeout = "N/D"
+ expires = "N/D"
+ else:
+ val = member.get('val', 'N/D')
+ timeout = str(member.get('timeout', 'N/D'))
+ expires = str(member.get('expires', 'N/D'))
if args.detail:
row.append(f'{val} (timeout: {timeout}, expires: {expires})')
diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py
index ba0e3b6db..0d2d7076c 100755
--- a/src/op_mode/image_installer.py
+++ b/src/op_mode/image_installer.py
@@ -23,6 +23,8 @@ from shutil import copy, chown, rmtree, copytree
from glob import glob
from sys import exit
from os import environ
+from os import readlink
+from os import getpid, getppid
from typing import Union
from urllib.parse import urlparse
from passlib.hosts import linux_context
@@ -65,7 +67,7 @@ MSG_INPUT_PASSWORD: str = 'Please enter a password for the "vyos" user:'
MSG_INPUT_PASSWORD_CONFIRM: str = 'Please confirm password for the "vyos" user:'
MSG_INPUT_ROOT_SIZE_ALL: str = 'Would you like to use all the free space on the drive?'
MSG_INPUT_ROOT_SIZE_SET: str = 'Please specify the size (in GB) of the root partition (min is 1.5 GB)?'
-MSG_INPUT_CONSOLE_TYPE: str = 'What console should be used by default? (K: KVM, S: Serial, U: USB-Serial)?'
+MSG_INPUT_CONSOLE_TYPE: str = 'What console should be used by default? (K: KVM, S: Serial)?'
MSG_INPUT_COPY_DATA: str = 'Would you like to copy data to the new image?'
MSG_INPUT_CHOOSE_COPY_DATA: str = 'From which image would you like to save config information?'
MSG_INPUT_COPY_ENC_DATA: str = 'Would you like to copy the encrypted config to the new image?'
@@ -614,6 +616,20 @@ def copy_ssh_host_keys() -> bool:
return False
+def console_hint() -> str:
+ pid = getppid() if 'SUDO_USER' in environ else getpid()
+ try:
+ path = readlink(f'/proc/{pid}/fd/1')
+ except OSError:
+ path = '/dev/tty'
+
+ name = Path(path).name
+ if name == 'ttyS0':
+ return 'S'
+ else:
+ return 'K'
+
+
def cleanup(mounts: list[str] = [], remove_items: list[str] = []) -> None:
"""Clean up after installation
@@ -709,9 +725,9 @@ def install_image() -> None:
# ask for default console
console_type: str = ask_input(MSG_INPUT_CONSOLE_TYPE,
- default='K',
- valid_responses=['K', 'S', 'U'])
- console_dict: dict[str, str] = {'K': 'tty', 'S': 'ttyS', 'U': 'ttyUSB'}
+ default=console_hint(),
+ valid_responses=['K', 'S'])
+ console_dict: dict[str, str] = {'K': 'tty', 'S': 'ttyS'}
config_boot_list = ['/opt/vyatta/etc/config/config.boot',
'/opt/vyatta/etc/config.boot.default']
diff --git a/src/op_mode/image_manager.py b/src/op_mode/image_manager.py
index 1cfb5f5a1..fb4286dbc 100755
--- a/src/op_mode/image_manager.py
+++ b/src/op_mode/image_manager.py
@@ -21,7 +21,7 @@ from argparse import ArgumentParser, Namespace
from pathlib import Path
from shutil import rmtree
from sys import exit
-from typing import Optional
+from typing import Optional, Literal, TypeAlias, get_args
from vyos.system import disk, grub, image, compat
from vyos.utils.io import ask_yes_no, select_entry
@@ -33,6 +33,8 @@ DELETE_IMAGE_PROMPT_MSG: str = 'Select an image to delete:'
MSG_DELETE_IMAGE_RUNNING: str = 'Currently running image cannot be deleted; reboot into another image first'
MSG_DELETE_IMAGE_DEFAULT: str = 'Default image cannot be deleted; set another image as default first'
+ConsoleType: TypeAlias = Literal['tty', 'ttyS']
+
def annotate_list(images_list: list[str]) -> list[str]:
"""Annotate list of images with additional info
@@ -202,6 +204,15 @@ def rename_image(name_old: str, name_new: str) -> None:
exit(f'Unable to rename the encrypted config for "{name_old}" to "{name_new}": {err}')
+@compat.grub_cfg_update
+def set_console_type(console_type: ConsoleType) -> None:
+ console_choice = get_args(ConsoleType)
+ if console_type not in console_choice:
+ exit(f'console type \'{console_type}\' not available')
+
+ grub.set_console_type(console_type)
+
+
def list_images() -> None:
"""Print list of available images for CLI hints"""
images_list: list[str] = grub.version_list()
@@ -209,6 +220,13 @@ def list_images() -> None:
print(image_name)
+def list_console_types() -> None:
+ """Print list of console types for CLI hints"""
+ console_types: list[str] = list(get_args(ConsoleType))
+ for console_type in console_types:
+ print(console_type)
+
+
def parse_arguments() -> Namespace:
"""Parse arguments
@@ -217,7 +235,8 @@ def parse_arguments() -> Namespace:
"""
parser: ArgumentParser = ArgumentParser(description='Manage system images')
parser.add_argument('--action',
- choices=['delete', 'set', 'rename', 'list'],
+ choices=['delete', 'set', 'set_console_type',
+ 'rename', 'list', 'list_console_types'],
required=True,
help='action to perform with an image')
parser.add_argument('--no-prompt', action='store_true',
@@ -227,6 +246,7 @@ def parse_arguments() -> Namespace:
help=
'a name of an image to add, delete, install, rename, or set as default')
parser.add_argument('--image-new-name', help='a new name for image')
+ parser.add_argument('--console-type', help='console type for boot')
args: Namespace = parser.parse_args()
# Validate arguments
if args.action == 'rename' and (not args.image_name or
@@ -243,10 +263,14 @@ if __name__ == '__main__':
delete_image(args.image_name, args.no_prompt)
if args.action == 'set':
set_image(args.image_name)
+ if args.action == 'set_console_type':
+ set_console_type(args.console_type)
if args.action == 'rename':
rename_image(args.image_name, args.image_new_name)
if args.action == 'list':
list_images()
+ if args.action == 'list_console_types':
+ list_console_types()
exit()
diff --git a/src/op_mode/ipoe-control.py b/src/op_mode/ipoe-control.py
index 0f33beca7..b7d6a0c43 100755
--- a/src/op_mode/ipoe-control.py
+++ b/src/op_mode/ipoe-control.py
@@ -56,7 +56,11 @@ def main():
if args.selector in cmd_dict['selector'] and args.target:
run(cmd_dict['cmd_base'] + "{0} {1} {2}".format(args.action, args.selector, args.target))
else:
- output, err = popen(cmd_dict['cmd_base'] + cmd_dict['actions'][args.action], decode='utf-8')
+ if args.action == "show_sessions":
+ ses_pattern = " ifname,username,calling-sid,ip,ip6,ip6-dp,rate-limit,type,comp,state,uptime"
+ else:
+ ses_pattern = ""
+ output, err = popen(cmd_dict['cmd_base'] + cmd_dict['actions'][args.action] + ses_pattern, decode='utf-8')
if not err:
print(output)
else:
diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py
index 2bc7e24fe..4ab524fb7 100755
--- a/src/op_mode/nat.py
+++ b/src/op_mode/nat.py
@@ -263,7 +263,7 @@ def _get_formatted_translation(dict_data, nat_direction, family, verbose):
proto = meta['layer4']['protoname']
if direction == 'independent':
conn_id = meta['id']
- timeout = meta['timeout']
+ timeout = meta.get('timeout', 'n/a')
orig_src = f'{orig_src}:{orig_sport}' if orig_sport else orig_src
orig_dst = f'{orig_dst}:{orig_dport}' if orig_dport else orig_dst
reply_src = f'{reply_src}:{reply_sport}' if reply_sport else reply_src
diff --git a/src/op_mode/ntp.py b/src/op_mode/ntp.py
new file mode 100644
index 000000000..e14cc46d0
--- /dev/null
+++ b/src/op_mode/ntp.py
@@ -0,0 +1,164 @@
+#!/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/>.
+
+import csv
+import sys
+from itertools import chain
+
+import vyos.opmode
+from vyos.configquery import ConfigTreeQuery
+from vyos.utils.process import cmd
+
+def _get_raw_data(command: str) -> dict:
+ # Returns returns chronyc output as a dictionary
+
+ # Initialize dictionary keys to align with output of
+ # chrony -c. From some commands, its -c switch outputs
+ # more parameters, make sure to include them all below.
+ # See to chronyc(1) for definition of key variables
+ match command:
+ case "chronyc -c activity":
+ keys: list = [
+ 'sources_online',
+ 'sources_offline',
+ 'sources_doing_burst_return_online',
+ 'sources_doing_burst_return_offline',
+ 'sources_with_unknown_address'
+ ]
+
+ case "chronyc -c sources":
+ keys: list = [
+ 'm',
+ 's',
+ 'name_ip_address',
+ 'stratum',
+ 'poll',
+ 'reach',
+ 'last_rx',
+ 'last_sample_adj_offset',
+ 'last_sample_mes_offset',
+ 'last_sample_est_error'
+ ]
+
+ case "chronyc -c sourcestats":
+ keys: list = [
+ 'name_ip_address',
+ 'np',
+ 'nr',
+ 'span',
+ 'frequency',
+ 'freq_skew',
+ 'offset',
+ 'std_dev'
+ ]
+
+ case "chronyc -c tracking":
+ keys: list = [
+ 'ref_id',
+ 'ref_id_name',
+ 'stratum',
+ 'ref_time',
+ 'system_time',
+ 'last_offset',
+ 'rms_offset',
+ 'frequency',
+ 'residual_freq',
+ 'skew',
+ 'root_delay',
+ 'root_dispersion',
+ 'update_interval',
+ 'leap_status'
+ ]
+
+ case _:
+ raise ValueError(f"Raw mode: of {command} is not implemented")
+
+ # Get -c option command line output, splitlines,
+ # and save comma-separated values as a flat list
+ output = cmd(command).splitlines()
+ values = csv.reader(output)
+ values = list(chain.from_iterable(values))
+
+ # Divide values into chunks of size keys and transpose
+ if len(values) > len(keys):
+ values = _chunk_list(values,keys)
+ values = zip(*values)
+
+ return dict(zip(keys, values))
+
+def _chunk_list(in_list, n):
+ # Yields successive n-sized chunks from in_list
+ for i in range(0, len(in_list), len(n)):
+ yield in_list[i:i + len(n)]
+
+def _is_configured():
+ # Check if ntp is configured
+ config = ConfigTreeQuery()
+ if not config.exists("service ntp"):
+ raise vyos.opmode.UnconfiguredSubsystem("NTP service is not enabled.")
+
+def show_activity(raw: bool):
+ _is_configured()
+ command = f'chronyc'
+
+ if raw:
+ command += f" -c activity"
+ return _get_raw_data(command)
+ else:
+ command += f" activity"
+ return cmd(command)
+
+def show_sources(raw: bool):
+ _is_configured()
+ command = f'chronyc'
+
+ if raw:
+ command += f" -c sources"
+ return _get_raw_data(command)
+ else:
+ command += f" sources -v"
+ return cmd(command)
+
+def show_tracking(raw: bool):
+ _is_configured()
+ command = f'chronyc'
+
+ if raw:
+ command += f" -c tracking"
+ return _get_raw_data(command)
+ else:
+ command += f" tracking"
+ return cmd(command)
+
+def show_sourcestats(raw: bool):
+ _is_configured()
+ command = f'chronyc'
+
+ if raw:
+ command += f" -c sourcestats"
+ return _get_raw_data(command)
+ else:
+ command += f" sourcestats -v"
+ return cmd(command)
+
+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/version.py b/src/op_mode/version.py
index ad0293aca..09d69ad1d 100755
--- a/src/op_mode/version.py
+++ b/src/op_mode/version.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2016-2022 VyOS maintainers and contributors
+# Copyright (C) 2016-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
@@ -30,11 +30,15 @@ from jinja2 import Template
version_output_tmpl = """
Version: VyOS {{version}}
Release train: {{release_train}}
+Release flavor: {{flavor}}
Built by: {{built_by}}
Built on: {{built_on}}
Build UUID: {{build_uuid}}
Build commit ID: {{build_git}}
+{%- if build_comment %}
+Build comment: {{build_comment}}
+{% endif %}
Architecture: {{system_arch}}
Boot via: {{boot_via}}