summaryrefslogtreecommitdiff
path: root/src/op_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/op_mode')
-rwxr-xr-xsrc/op_mode/accelppp.py2
-rw-r--r--src/op_mode/activation.py144
-rwxr-xr-xsrc/op_mode/bgp.py25
-rwxr-xr-xsrc/op_mode/bonding.py2
-rwxr-xr-xsrc/op_mode/bridge.py4
-rwxr-xr-xsrc/op_mode/cgnat.py2
-rwxr-xr-xsrc/op_mode/clear_conntrack.py2
-rwxr-xr-xsrc/op_mode/config_mgmt.py2
-rw-r--r--src/op_mode/config_sync.py193
-rwxr-xr-xsrc/op_mode/connect_disconnect.py29
-rwxr-xr-xsrc/op_mode/conntrack.py30
-rwxr-xr-xsrc/op_mode/conntrack_sync.py2
-rwxr-xr-xsrc/op_mode/container.py134
-rwxr-xr-xsrc/op_mode/cpu.py4
-rwxr-xr-xsrc/op_mode/dhcp.py157
-rwxr-xr-xsrc/op_mode/dns.py2
-rw-r--r--src/op_mode/evpn.py2
-rwxr-xr-xsrc/op_mode/execute_bandwidth_test.sh2
-rw-r--r--src/op_mode/execute_port-scan.py2
-rwxr-xr-xsrc/op_mode/file.py5
-rwxr-xr-xsrc/op_mode/firewall.py135
-rwxr-xr-xsrc/op_mode/flow_accounting_op.py229
-rwxr-xr-xsrc/op_mode/force_mtu_host.sh2
-rwxr-xr-xsrc/op_mode/force_root-partition-auto-resize.sh2
-rwxr-xr-xsrc/op_mode/format_disk.py6
-rwxr-xr-xsrc/op_mode/generate_interfaces_debug_archive.py4
-rwxr-xr-xsrc/op_mode/generate_ipsec_debug_archive.py2
-rwxr-xr-xsrc/op_mode/generate_openconnect_otp_key.py2
-rwxr-xr-xsrc/op_mode/generate_ovpn_client_file.py8
-rw-r--r--src/op_mode/generate_psk.py2
-rwxr-xr-xsrc/op_mode/generate_public_key_command.py2
-rwxr-xr-xsrc/op_mode/generate_service_rule-resequence.py2
-rwxr-xr-xsrc/op_mode/generate_ssh_server_key.py8
-rwxr-xr-xsrc/op_mode/generate_system_login_user.py2
-rwxr-xr-xsrc/op_mode/generate_tech-support_archive.py243
-rwxr-xr-xsrc/op_mode/igmp-proxy.py2
-rwxr-xr-xsrc/op_mode/ikev2_profile_generator.py2
-rwxr-xr-xsrc/op_mode/image_info.py10
-rwxr-xr-xsrc/op_mode/image_installer.py394
-rwxr-xr-xsrc/op_mode/image_manager.py4
-rwxr-xr-xsrc/op_mode/install_mok.sh7
-rwxr-xr-xsrc/op_mode/interfaces.py278
-rw-r--r--src/op_mode/interfaces_wireguard.py24
-rwxr-xr-xsrc/op_mode/interfaces_wireless.py23
-rwxr-xr-xsrc/op_mode/ipoe-control.py4
-rwxr-xr-xsrc/op_mode/ipsec.py69
-rwxr-xr-xsrc/op_mode/kernel_modules.py2
-rwxr-xr-xsrc/op_mode/lldp.py2
-rwxr-xr-xsrc/op_mode/load-balancing_haproxy.py2
-rwxr-xr-xsrc/op_mode/load-balancing_wan.py6
-rwxr-xr-xsrc/op_mode/log.py2
-rwxr-xr-xsrc/op_mode/maya_date.py6
-rwxr-xr-xsrc/op_mode/memory.py2
-rw-r--r--src/op_mode/mtr.py2
-rw-r--r--src/op_mode/mtr_execute.py2
-rwxr-xr-xsrc/op_mode/multicast.py2
-rwxr-xr-xsrc/op_mode/nat.py23
-rwxr-xr-xsrc/op_mode/neighbor.py5
-rw-r--r--src/op_mode/ntp.py2
-rwxr-xr-xsrc/op_mode/openconnect-control.py2
-rwxr-xr-xsrc/op_mode/openconnect.py2
-rwxr-xr-xsrc/op_mode/openvpn.py2
-rwxr-xr-xsrc/op_mode/otp.py4
-rwxr-xr-xsrc/op_mode/ping.py4
-rwxr-xr-xsrc/op_mode/pki.py50
-rwxr-xr-xsrc/op_mode/policy_route.py2
-rwxr-xr-xsrc/op_mode/powerctrl.py2
-rwxr-xr-xsrc/op_mode/ppp-server-ctrl.py2
-rwxr-xr-xsrc/op_mode/qos.py2
-rwxr-xr-xsrc/op_mode/raid.py2
-rwxr-xr-xsrc/op_mode/reset_openvpn.py2
-rwxr-xr-xsrc/op_mode/reset_vpn.py2
-rwxr-xr-xsrc/op_mode/reset_wireguard.py25
-rwxr-xr-xsrc/op_mode/restart.py6
-rwxr-xr-xsrc/op_mode/restart_dhcp_relay.py2
-rwxr-xr-xsrc/op_mode/restart_frr.py2
-rwxr-xr-xsrc/op_mode/route.py2
-rwxr-xr-xsrc/op_mode/secure_boot.py2
-rw-r--r--src/op_mode/serial.py2
-rwxr-xr-xsrc/op_mode/sflow.py2
-rwxr-xr-xsrc/op_mode/show-bond.py6
-rwxr-xr-xsrc/op_mode/show_acceleration.py10
-rwxr-xr-xsrc/op_mode/show_bonding_detail.sh7
-rwxr-xr-xsrc/op_mode/show_configuration_json.py2
-rwxr-xr-xsrc/op_mode/show_openconnect_otp.py4
-rwxr-xr-xsrc/op_mode/show_openvpn.py2
-rwxr-xr-xsrc/op_mode/show_openvpn_mfa.py2
-rwxr-xr-xsrc/op_mode/show_ppp_stats.sh5
-rwxr-xr-xsrc/op_mode/show_sensors.py17
-rw-r--r--src/op_mode/show_techsupport_report.py721
-rwxr-xr-xsrc/op_mode/show_usb_serial.py2
-rwxr-xr-xsrc/op_mode/show_users.py10
-rwxr-xr-xsrc/op_mode/show_virtual_server.py2
-rwxr-xr-xsrc/op_mode/show_vpp_interfaces.py263
-rwxr-xr-xsrc/op_mode/show_wwan.py2
-rwxr-xr-xsrc/op_mode/snmp.py4
-rwxr-xr-xsrc/op_mode/snmp_ifmib.py4
-rwxr-xr-xsrc/op_mode/snmp_v3.py2
-rwxr-xr-xsrc/op_mode/ssh.py2
-rwxr-xr-xsrc/op_mode/storage.py10
-rwxr-xr-xsrc/op_mode/stp.py185
-rwxr-xr-xsrc/op_mode/system.py2
-rw-r--r--src/op_mode/tcpdump.py14
-rw-r--r--src/op_mode/tech_support.py10
-rwxr-xr-xsrc/op_mode/toggle_help_binding.sh2
-rwxr-xr-xsrc/op_mode/traceroute.py22
-rwxr-xr-xsrc/op_mode/update_suricata.sh8
-rwxr-xr-xsrc/op_mode/uptime.py2
-rwxr-xr-xsrc/op_mode/version.py2
-rwxr-xr-xsrc/op_mode/vpn_ike_sa.py2
-rwxr-xr-xsrc/op_mode/vpn_ipsec.py9
-rwxr-xr-xsrc/op_mode/vpp.py525
-rw-r--r--src/op_mode/vpp_acl.py342
-rw-r--r--src/op_mode/vpp_nat_cgnat.py178
-rw-r--r--src/op_mode/vpp_nat_nat44.py251
-rwxr-xr-xsrc/op_mode/vrf.py2
-rwxr-xr-xsrc/op_mode/vrrp.py6
-rwxr-xr-xsrc/op_mode/webproxy_update_blacklist.sh2
-rwxr-xr-xsrc/op_mode/wireguard_client.py4
-rw-r--r--src/op_mode/zone.py2
120 files changed, 4250 insertions, 787 deletions
diff --git a/src/op_mode/accelppp.py b/src/op_mode/accelppp.py
index 67ce786d0..6f6fd4858 100755
--- a/src/op_mode/accelppp.py
+++ b/src/op_mode/accelppp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022-2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/activation.py b/src/op_mode/activation.py
new file mode 100644
index 000000000..16192f3eb
--- /dev/null
+++ b/src/op_mode/activation.py
@@ -0,0 +1,144 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# 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 sys
+import re
+import typing
+import tabulate
+
+import vyos.opmode
+from vyos.utils.activate import get_activation_scripts
+from vyos.utils.activate import set_activation as util_activate
+from vyos.utils.activate import get_activation
+from vyos.utils.activate import ActiveOpt
+from vyos.utils.io import ask_yes_no
+from vyos.base import Warning as Warn
+
+
+def _get_raw_data() -> dict:
+ return get_activation_scripts()
+
+
+def _split_name(name: str) -> tuple[str, str]:
+ # script names are guaranteed to have this format by construction:
+ # cf. scripts/generate-activation-scripts-json.py
+ match = re.match(r'(\d+)\-(.+)', name)
+ if match is None:
+ return '0', '_'
+ prio, base_name = match.groups()
+ return prio, base_name
+
+
+def _find_full_name(name: str) -> typing.Optional[str]:
+ script_names = list(_get_raw_data())
+ result = list(filter(lambda s: s.endswith(name), script_names))
+
+ return result[0] if result else None
+
+
+def show_list(raw: bool) -> typing.Optional[list]:
+ scripts = _get_raw_data()
+ data = []
+ for key in scripts.keys():
+ _, name = _split_name(key)
+ data.append(name)
+
+ if raw:
+ return data
+
+ print(*data)
+ return None
+
+
+def show_opts(raw: bool) -> typing.Optional[list]:
+ opts = list(typing.get_args(ActiveOpt))
+
+ if raw:
+ return opts
+
+ print(*opts)
+ return None
+
+
+def _format_scripts(scripts: dict):
+ headers = ['name', 'activate on reboot', 'priority']
+ data = []
+ for key in scripts.keys():
+ prio, name = _split_name(key)
+ value = scripts[key]
+ data.append([name, value, prio])
+
+ print('Activation units:')
+ print(tabulate.tabulate(data, headers))
+
+
+def show(raw: bool):
+ activation_dict = _get_raw_data()
+ if raw:
+ return activation_dict
+ return _format_scripts(activation_dict)
+
+
+def set_active(name: str, value: ActiveOpt, no_prompt: bool = False):
+ PROMPT_ENABLED = f'This will set {name} active on subsequent reboots. Proceed ?'
+ PROMPT_ONCE = f'This will set {name} active only for the next reboot. Proceed ?'
+ PROMPT_OFF = f'This will set {name} inactive. Proceed ?'
+ UNCHANGED = f'{name} is already set to {value}'
+ UNKNOWN = 'None such'
+
+ full_name = _find_full_name(name)
+ if not full_name:
+ Warn(f'No activation unit {name}')
+ return
+
+ state = get_activation(full_name)
+
+ if state == 'never':
+ Warn(f'{name} has been set to \'never\' and should not be reset')
+ return
+
+ if value == state:
+ print(UNCHANGED)
+ return
+
+ if value not in list(typing.get_args(ActiveOpt)):
+ Warn(f'No such value {value}')
+ return
+
+ match value:
+ case 'enabled':
+ message = PROMPT_ENABLED
+ case 'once':
+ message = PROMPT_ONCE
+ case 'off':
+ message = PROMPT_OFF
+ case _:
+ # not reached
+ message = UNKNOWN
+
+ if no_prompt or ask_yes_no(message, default=True):
+ util_activate(full_name, value)
+
+
+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/bgp.py b/src/op_mode/bgp.py
index 096113cb4..7f0815433 100755
--- a/src/op_mode/bgp.py
+++ b/src/op_mode/bgp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -80,6 +80,29 @@ show bgp
ArgFamily = typing.Literal['inet', 'inet6', 'l2vpn']
ArgFamilyModifier = typing.Literal['unicast', 'labeled_unicast', 'multicast', 'vpn', 'flowspec']
+def reset(command: str):
+ from vyos.utils.process import cmd
+
+ tokens = command.split()
+
+ # reset -> clear (only if it's the first token)
+ if tokens and tokens[0] == "reset":
+ tokens[0] = "clear"
+
+ # peer-group and vrf may have 'all' in their names; don't replace 'all' with '*'
+ skip_indexes = []
+ for index, word in enumerate(tokens[:-1]):
+ if word in ("peer-group", "vrf"):
+ skip_indexes.append(index + 1)
+
+ # replace standalone "all" with "*" unless it's in the skip list
+ for index, word in enumerate(tokens):
+ if word == "all" and index not in skip_indexes:
+ tokens[index] = "*"
+
+ command = " ".join(tokens)
+ cmd(f'vtysh -c "{command}"')
+
def show_summary(raw: bool):
from vyos.utils.process import cmd
diff --git a/src/op_mode/bonding.py b/src/op_mode/bonding.py
index 07bccbd4b..0ceb65cff 100755
--- a/src/op_mode/bonding.py
+++ b/src/op_mode/bonding.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2016-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/bridge.py b/src/op_mode/bridge.py
index c4293a77c..9056e16d4 100755
--- a/src/op_mode/bridge.py
+++ b/src/op_mode/bridge.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -83,7 +83,7 @@ def _get_raw_data_fdb(bridge):
def _get_raw_data_mdb(bridge):
- """Get MAC-address multicast gorup for the bridge brX
+ """Get MAC-address multicast group for the bridge brX
:return list
"""
json_data = cmd(f'bridge --json mdb show br {bridge}')
diff --git a/src/op_mode/cgnat.py b/src/op_mode/cgnat.py
index 9ad8f92f9..d53f6158b 100755
--- a/src/op_mode/cgnat.py
+++ b/src/op_mode/cgnat.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/clear_conntrack.py b/src/op_mode/clear_conntrack.py
index fec7cf144..2a4f19607 100755
--- a/src/op_mode/clear_conntrack.py
+++ b/src/op_mode/clear_conntrack.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/config_mgmt.py b/src/op_mode/config_mgmt.py
index 66de26d1f..fa2abec0e 100755
--- a/src/op_mode/config_mgmt.py
+++ b/src/op_mode/config_mgmt.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/config_sync.py b/src/op_mode/config_sync.py
new file mode 100644
index 000000000..d6eff7cd2
--- /dev/null
+++ b/src/op_mode/config_sync.py
@@ -0,0 +1,193 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# 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 sys
+import typing
+from pathlib import Path
+
+from vyos import opmode
+from vyos import http_api_client as http
+from vyos.utils.file import read_json
+from vyos.utils.dict import dict_to_paths
+from vyos.utils.list import list_contains_sublist
+from vyos.configtree import ConfigTree
+from vyos.configtree import ConfigTreeError
+from vyos.config_mgmt import ConfigMgmt
+from vyos.config_mgmt import ConfigMgmtError
+
+CONFIG_FILE = Path('/run/config_sync_conf.conf')
+
+
+def _normalize_section(section: typing.Optional[str]) -> list:
+ """Convert optional CLI section argument to config tree path list"""
+
+ if not section:
+ return []
+
+ # Section can be passed as a single string token ('interfaces ethernet')
+ return list(section.split())
+
+
+def _read_json_config() -> dict:
+ """Read config-sync service runtime JSON file"""
+
+ if not CONFIG_FILE.exists():
+ raise opmode.UnconfiguredObject('Config-sync service is not configured')
+
+ return read_json(CONFIG_FILE, defaultonfailure={})
+
+
+def _load_config_sync_sections() -> list:
+ """Load sections from config-sync service runtime JSON file"""
+
+ cfg = _read_json_config()
+ sections = cfg.get('section', {})
+
+ return list(dict_to_paths(sections)) if sections else []
+
+
+def _load_config_sync_settings() -> dict:
+ """Load remote API settings from config-sync service runtime JSON file"""
+
+ cfg = _read_json_config()
+ secondary = cfg.get('secondary', {})
+ address = secondary.get('address')
+ key = secondary.get('key')
+ port = int(secondary.get('port', 443))
+ timeout = int(secondary.get('timeout')) if secondary.get('timeout') else None
+
+ if not address or not key:
+ raise opmode.UnconfiguredObject(
+ 'Config-sync is not fully configured: missing secondary address/key'
+ )
+
+ return dict(host=address, key=key, port=port, timeout=timeout)
+
+
+class ConfigSyncDiffManager:
+ def __init__(self):
+ api_settings = _load_config_sync_settings()
+ self._client = http.ApiClient(http.ApiClientConfig(**api_settings))
+
+ self._config_mgmt = ConfigMgmt()
+
+ def _get_remote_config_tree(self, section_path: list = None) -> ConfigTree:
+ """
+ Retrieve remote config (or subtree) as ConfigTree via HTTPS API.
+
+ Note: Endpoint name is expected to be available on remote VyOS instance.
+ """
+
+ payload = {
+ 'configFormat': 'raw',
+ 'op': 'showConfig',
+ 'path': section_path or [],
+ }
+
+ try:
+ resp_data = self._client.post('retrieve', payload, raise_on_error=False)
+ except http.ApiError as e:
+ raise opmode.InternalError(f'Remote API failed: {e}') from e
+
+ error = (resp_data.get('error') or resp_data.get('detail') or '').strip()
+ if error:
+ ignored_errors = ('configuration under specified path is empty',)
+ if error.lower() not in ignored_errors:
+ raise opmode.InternalError(
+ f'Remote API responded with an error: {error}'
+ )
+
+ config_raw = resp_data.get('data') or ''
+ try:
+ return ConfigTree(config_raw)
+ except ConfigTreeError as e:
+ raise opmode.InternalError(f'Unable to build remote ConfigTree: {e}') from e
+
+ def get_sync_diff(
+ self,
+ source: str,
+ sections: list,
+ commands: typing.Optional[bool] = False,
+ ) -> str:
+ """Returns differences between local config and remote config for a given sections"""
+
+ results = []
+ remote_tree = self._get_remote_config_tree()
+ for section_path in sections:
+ try:
+ result = self._config_mgmt.remote_compare(
+ source,
+ remote_tree,
+ path=section_path,
+ commands=commands,
+ )
+ except ConfigMgmtError as e:
+ raise opmode.InternalError(str(e)) from e
+
+ result = result.strip()
+ if result:
+ results.append(result)
+
+ return '\n'.join(results)
+
+
+def show_sync_diff(
+ raw: bool,
+ source: typing.Optional[str],
+ section: typing.Optional[str],
+ commands: typing.Optional[bool],
+) -> str:
+ """Show differences between local config and remote config for a given section.
+
+ Args:
+ raw: unused (op-mode convention); output is always text.
+ source: local source config: running/candidate/saved.
+ section: optional top-level section to diff (e.g. "nat", "system time-zone").
+ commands: flag which indicates format of output.
+
+ Returns:
+ Diff output (string). Empty diff is rendered as "no changes".
+ """
+ _ = raw # op-mode framework passes it; keep signature consistent
+
+ source = source or 'running'
+ selected_section = _normalize_section(section)
+
+ configured_sections = _load_config_sync_sections()
+ if selected_section:
+ if not list_contains_sublist(configured_sections, selected_section):
+ raise opmode.UnconfiguredObject(
+ f"Config-sync is not configured for '{section}' section. "
+ f"Use 'set service config-sync section {section}' for this."
+ )
+ sections = [selected_section]
+ else:
+ sections = configured_sections
+
+ manager = ConfigSyncDiffManager()
+ output = manager.get_sync_diff(source, sections, commands=commands)
+
+ return output if output else 'No changes between local and remote configuration'
+
+
+if __name__ == '__main__':
+ try:
+ res = opmode.run(sys.modules[__name__])
+ if res:
+ print(res)
+ except (ValueError, opmode.Error) as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/op_mode/connect_disconnect.py b/src/op_mode/connect_disconnect.py
index 8903f916a..d5db85d25 100755
--- a/src/op_mode/connect_disconnect.py
+++ b/src/op_mode/connect_disconnect.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -18,6 +18,7 @@ import os
import argparse
from psutil import process_iter
+from time import sleep
from vyos.configquery import ConfigTreeQuery
from vyos.utils.process import call
@@ -68,7 +69,7 @@ def connect(interface):
if ( count % 60 == 0 ):
print(f'Commit still in progress after {count}s - waiting')
count += 1
- time.sleep(1)
+ sleep(1)
call('/usr/libexec/vyos/conf_mode/qos.py')
def disconnect(interface):
@@ -97,19 +98,23 @@ def main():
group = parser.add_mutually_exclusive_group()
group.add_argument("--connect", help="Bring up a connection-oriented network interface", action="store_true")
group.add_argument("--disconnect", help="Take down connection-oriented network interface", action="store_true")
+ group.add_argument("--reconnect", help="Reconnect connection-oriented network interface", action="store_true")
parser.add_argument("--interface", help="Interface name", action="store", required=True)
args = parser.parse_args()
- if args.connect or args.disconnect:
- if args.disconnect:
- disconnect(args.interface)
-
- if args.connect:
- if commit_in_progress():
- print('Cannot connect while a commit is in progress')
- exit(1)
- connect(args.interface)
-
+ # Disallow connecting interfaces while their configuration might be changing
+ if args.connect or args.reconnect:
+ if commit_in_progress():
+ print('Cannot connect while a commit is in progress')
+ exit(1)
+
+ if args.connect:
+ connect(args.interface)
+ elif args.disconnect:
+ disconnect(args.interface)
+ elif args.reconnect:
+ disconnect(args.interface)
+ connect(args.interface)
else:
parser.print_help()
diff --git a/src/op_mode/conntrack.py b/src/op_mode/conntrack.py
index c379c3e60..f39012b2b 100755
--- a/src/op_mode/conntrack.py
+++ b/src/op_mode/conntrack.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -127,7 +127,6 @@ def get_formatted_output(dict_data):
reply_dport = meta['layer4']['dport']
proto = meta['layer4']['protoname']
if direction == 'independent':
- conn_id = meta['id']
# T6138 flowtable offload conntrack entries without 'timeout'
timeout = meta.get('timeout', 'n/a')
orig_src = f'{orig_src}:{orig_sport}' if orig_sport else orig_src
@@ -137,10 +136,29 @@ def get_formatted_output(dict_data):
state = meta['state'] if 'state' in meta else ''
mark = meta['mark'] if 'mark' in meta else ''
zone = meta['zone'] if 'zone' in meta else ''
- data_entries.append(
- [conn_id, orig_src, orig_dst, reply_src, reply_dst, proto, state, timeout, mark, zone])
- headers = ["Id", "Original src", "Original dst", "Reply src", "Reply dst", "Protocol", "State", "Timeout", "Mark",
- "Zone"]
+ data_entry = [
+ orig_src,
+ orig_dst,
+ reply_src,
+ reply_dst,
+ proto,
+ state,
+ timeout,
+ mark,
+ zone,
+ ]
+ data_entries.append(data_entry)
+ headers = [
+ "Original src",
+ "Original dst",
+ "Reply src",
+ "Reply dst",
+ "Protocol",
+ "State",
+ "Timeout",
+ "Mark",
+ "Zone",
+ ]
output = tabulate(data_entries, headers, numalign="left")
return output
diff --git a/src/op_mode/conntrack_sync.py b/src/op_mode/conntrack_sync.py
index f3b09b452..0da5b3b0b 100755
--- a/src/op_mode/conntrack_sync.py
+++ b/src/op_mode/conntrack_sync.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/container.py b/src/op_mode/container.py
index 05f65df1f..e0753f1be 100755
--- a/src/op_mode/container.py
+++ b/src/op_mode/container.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -14,13 +14,53 @@
# 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 typing
import json
+import shutil
import sys
+import subprocess
+from pathlib import Path
+from vyos.defaults import directories
from vyos.utils.process import cmd
from vyos.utils.process import rc_cmd
+from vyos.utils.process import run
import vyos.opmode
+def clean_layer(name: str) -> int:
+ def layer_id_from_containers(name: str) -> str | None:
+ if containers.is_file():
+ try:
+ index = json.loads(containers.read_text())
+ except Exception:
+ return None
+ for item in index:
+ if name in item.get("names", []):
+ return item.get("layer")
+ return None
+
+ def purge_layer_by_id(layer_id: str):
+ layer_dir = overlay_root / layer_id
+
+ # Remove the overlay ID directory
+ shutil.rmtree(layer_dir, ignore_errors=True)
+ storage_dir = Path(directories['podman_storage'])
+ overlay_root = storage_dir / "overlay"
+ containers = storage_dir / "overlay-containers/containers.json"
+ unit = f"vyos-container-{name}.service"
+ layer_id = layer_id_from_containers(name)
+ if not layer_id:
+ # No mapping found; nothing to do
+ return 2
+
+ purge_layer_by_id(layer_id)
+
+ # Reinitiate the container's overlay layer
+ cmd(f"rm -f /run/{unit}.cid /run/{unit}.pid")
+ cmd(f"systemctl reset-failed {unit}")
+ result = run(f"systemctl start {unit}")
+ return result
+
def _get_json_data(command: str) -> list:
"""
Get container command format JSON
@@ -34,7 +74,7 @@ def _get_raw_data(command: str) -> list:
def add_image(name: str):
""" Pull image from container registry. If registry authentication
- is defined within VyOS CLI, credentials are used to login befroe pull """
+ is defined within VyOS CLI, credentials are used to login before pull """
from vyos.configquery import ConfigTreeQuery
conf = ConfigTreeQuery()
@@ -54,14 +94,14 @@ def add_image(name: str):
rc, out = rc_cmd(cmd)
if rc != 0: raise vyos.opmode.InternalError(out)
- rc, output = rc_cmd(f'podman image pull {name}')
+ rc, output = rc_cmd(f'podman image pull {name}', buffered=False)
if rc != 0:
raise vyos.opmode.InternalError(output)
if do_logout:
rc_cmd('podman logout --all')
-def delete_image(name: str):
+def delete_image(name: str, force: typing.Optional[bool] = False):
from vyos.utils.process import rc_cmd
if name == 'all':
@@ -71,9 +111,33 @@ def delete_image(name: str):
if not name: return
# replace newline with whitespace
name = name.replace('\n', ' ')
- rc, output = rc_cmd(f'podman image rm {name}')
- if rc != 0:
- raise vyos.opmode.InternalError(output)
+ # convert to list
+ name = name.split()
+ else:
+ # convert str -> list for further processing down the line
+ name = [name]
+
+ for image in name:
+ # convert the truncated image ID to a full image ID
+ rc, ancestor = rc_cmd(f'podman inspect {image} --format "{{{{.Id}}}}"', stderr=None)
+ if rc != 0:
+ raise vyos.opmode.InternalError(ancestor)
+ # check if the image ID is an ancestor of any running container
+ rc, in_use = rc_cmd(f'podman ps --filter ancestor={ancestor} -q', stderr=None)
+ if rc != 0:
+ raise vyos.opmode.InternalError(in_use)
+
+ if bool(in_use):
+ error = f'Cannot delete image "{image}" because it is currently '\
+ f'being used by container "{in_use}"!'
+ raise vyos.opmode.InternalError(error)
+
+ tmp = f'podman image rm {image}'
+ if force: tmp += ' --force'
+
+ rc, output = rc_cmd(tmp)
+ if rc != 0:
+ raise vyos.opmode.InternalError(output)
def show_container(raw: bool):
command = 'podman ps --all'
@@ -101,14 +165,66 @@ def show_network(raw: bool):
def restart(name: str):
from vyos.utils.process import rc_cmd
+ from vyos.config import Config
+ from vyos.container import restart_network
rc, output = rc_cmd(f'systemctl restart vyos-container-{name}.service')
if rc != 0:
- print(output)
- return None
+ rc2 = clean_layer(name)
+ if rc2 != 0:
+ print(output)
+ return None
+ if rc == 0:
+ conf = Config()
+ container = conf.get_config_dict(['container'], key_mangling=('-', '_'),
+ no_tag_node_value_mangle=True,
+ get_first_key=True,
+ with_recursive_defaults=True)
+ restart_network(container)
print(f'Container "{name}" restarted!')
return output
+def show_log(name: str, follow: bool = False, raw: bool = False):
+ """
+ Show or monitor logs for a specific container.
+ Use --follow to continuously stream logs.
+ """
+ from vyos.configquery import ConfigTreeQuery
+ conf = ConfigTreeQuery()
+ container = conf.get_config_dict(['container', 'name', name], get_first_key=True, with_recursive_defaults=True)
+ log_type = container.get('log-driver')
+ if log_type == 'k8s-file':
+ if follow:
+ log_command_list = ['sudo', 'podman', 'logs', '--follow', '--names', name]
+ else:
+ log_command_list = ['sudo', 'podman', 'logs', '--names', name]
+ elif log_type == 'journald':
+ if follow:
+ log_command_list = ['journalctl', '--follow', '--unit', f'vyos-container-{name}.service']
+ else:
+ log_command_list = ['journalctl', '-e', '--no-pager', '--unit', f'vyos-container-{name}.service']
+ elif log_type == 'none':
+ print(f'Container "{name}" has disabled logs.')
+ return None
+ else:
+ raise vyos.opmode.InternalError(f'Unknown log type "{log_type}" for container "{name}".')
+
+ process = None
+ try:
+ process = subprocess.Popen(log_command_list,
+ stdout=sys.stdout,
+ stderr=sys.stderr)
+ process.wait()
+ except KeyboardInterrupt:
+ if process:
+ process.terminate()
+ process.wait()
+ return None
+ except Exception as e:
+ raise vyos.opmode.InternalError(f"Error starting logging command: {e} ")
+ return None
+
+
if __name__ == '__main__':
try:
res = vyos.opmode.run(sys.modules[__name__])
diff --git a/src/op_mode/cpu.py b/src/op_mode/cpu.py
index 1a0f7392f..07cb90187 100755
--- a/src/op_mode/cpu.py
+++ b/src/op_mode/cpu.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2016-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -47,7 +47,7 @@ def _format_cpus(cpu_data):
def _get_summary_data():
count = get_core_count()
cpu_data = get_cpus()
- models = [c['model name'] for c in cpu_data]
+ models = [c.get('model name', 'unknown') for c in cpu_data]
env = {'count': count, "models": models}
return env
diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py
index 725bfc75b..9cb5a84ae 100755
--- a/src/op_mode/dhcp.py
+++ b/src/op_mode/dhcp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022-2025 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -83,12 +83,12 @@ ArgOrigin = typing.Literal['local', 'remote']
def _get_raw_server_leases(
- config, family='inet', pool=None, sorted=None, state=[], origin=None
+ config, family='inet', vrf='', pool=None, sorted=None, state=[], origin=None
) -> list:
inet_suffix = '6' if family == 'inet6' else '4'
pools = [pool] if pool else kea_get_dhcp_pools(config, inet_suffix)
- mappings = kea_get_server_leases(config, inet_suffix, pools, state, origin)
+ mappings = kea_get_server_leases(config, inet_suffix, vrf, pools, state, origin)
if sorted:
if sorted == 'ip':
@@ -134,6 +134,7 @@ def _get_formatted_server_leases(raw_data, family='inet'):
if family == 'inet6':
for lease in raw_data:
ipaddr = lease.get('ip')
+ hw_addr = lease.get('mac')
state = lease.get('state')
start = datetime.fromtimestamp(
lease.get('last_communication'), timezone.utc
@@ -146,19 +147,22 @@ def _get_formatted_server_leases(raw_data, family='inet'):
remain = lease.get('remaining')
lease_type = lease.get('type')
pool = lease.get('pool')
+ hostname = lease.get('hostname')
host_identifier = lease.get('duid')
data_entries.append(
- [ipaddr, state, start, end, remain, lease_type, pool, host_identifier]
+ [ipaddr, hw_addr, state, start, end, remain, pool, hostname, lease_type, host_identifier]
)
headers = [
'IPv6 address',
+ 'MAC address',
'State',
'Last communication',
'Lease expiration',
'Remaining',
- 'Type',
'Pool',
+ 'Hostname',
+ 'Type',
'DUID',
]
@@ -166,9 +170,13 @@ def _get_formatted_server_leases(raw_data, family='inet'):
return output
-def _get_pool_size(pool, family='inet'):
+def _get_pool_size(pool, family='inet', vrf=''):
v = 'v6' if family == 'inet6' else ''
- base = f'service dhcp{v}-server shared-network-name {pool}'
+ # if vrf is set get the correct base for config
+ if vrf:
+ base = f'vrf name {vrf} service dhcp{v}-server shared-network-name {pool}'
+ else:
+ base = f'service dhcp{v}-server shared-network-name {pool}'
size = 0
subnets = config.list_nodes(f'{base} subnet')
for subnet in subnets:
@@ -185,14 +193,14 @@ def _get_pool_size(pool, family='inet'):
return size
-def _get_raw_server_pool_statistics(config, family='inet', pool=None):
+def _get_raw_server_pool_statistics(config, family='inet', vrf='', pool=None):
inet_suffix = '6' if family == 'inet6' else '4'
pools = [pool] if pool else kea_get_dhcp_pools(config, inet_suffix)
stats = []
for p in pools:
- size = _get_pool_size(family=family, pool=p)
- leases = len(_get_raw_server_leases(config, family=family, pool=p))
+ size = _get_pool_size(family=family, vrf=vrf, pool=p)
+ leases = len(_get_raw_server_leases(config, family=family, vrf=vrf, pool=p))
use_percentage = round(leases / size * 100) if size != 0 else 0
pool_stats = {
'pool': p,
@@ -269,11 +277,20 @@ def _verify_server(func):
def _wrapper(*args, **kwargs):
config = ConfigTreeQuery()
family = kwargs.get('family')
+ vrf = kwargs.get('vrf')
v = 'v6' if family == 'inet6' else ''
- unconf_message = f'DHCP{v} server is not configured'
+
# Check if config does not exist
- if not config.exists(f'service dhcp{v}-server'):
- raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
+ if vrf:
+ unconf_message = f'DHCP{v} server is not configured for VRF {vrf}'
+ if not config.exists(f'vrf name {vrf} service dhcp{v}-server'):
+ raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
+ else:
+ unconf_message = f'DHCP{v} server is not configured'
+ if not config.exists(f'service dhcp{v}-server'):
+ raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
+
+ # return
return func(*args, **kwargs)
return _wrapper
@@ -291,11 +308,31 @@ def _verify_client(func):
v = 'v6' if family == 'inet6' else ''
interface = kwargs.get('interface')
interface_path = Section.get_config_path(interface)
+ path_elems = interface_path.split()
+ base_path = ['interfaces'] + path_elems
+
unconf_message = f'DHCP{v} client not configured on interface {interface}!'
- # Check if config does not exist
- if not config.exists(f'interfaces {interface_path} address dhcp{v}'):
+ iface_conf = config.get_config_dict(
+ base_path, key_mangling=('-', '_'), get_first_key=True
+ )
+
+ if family == 'inet6':
+ addrs = iface_conf.get('address', [])
+ has_dhcpv6_addr = 'dhcpv6' in addrs
+
+ dhcpv6_opts = iface_conf.get('dhcpv6_options', {})
+ has_parameters_only = 'parameters_only' in dhcpv6_opts
+ has_pd = 'pd' in dhcpv6_opts
+
+ config_exists = has_dhcpv6_addr or has_parameters_only or has_pd
+ else:
+ addrs = iface_conf.get('address', [])
+ config_exists = 'dhcp' in addrs
+
+ if not config_exists:
raise vyos.opmode.UnconfiguredObject(unconf_message)
+
return func(*args, **kwargs)
return _wrapper
@@ -303,25 +340,42 @@ def _verify_client(func):
@_verify_server
def show_server_pool_statistics(
- raw: bool, family: ArgFamily, pool: typing.Optional[str]
+ raw: bool, family: ArgFamily, vrf: typing.Optional[str], pool: typing.Optional[str]
):
v = 'v6' if family == 'inet6' else ''
inet_suffix = '6' if family == 'inet6' else '4'
- if not is_systemd_service_running(f'kea-dhcp{inet_suffix}-server.service'):
+ if vrf:
+ service = f'isc-kea-dhcp{inet_suffix}-server@{vrf}.service'
+ else:
+ service = f'isc-kea-dhcp{inet_suffix}-server.service'
+
+ if not is_systemd_service_running(service):
Warning(stale_warn_msg)
try:
- active_config = kea_get_active_config(inet_suffix)
+ active_config = kea_get_active_config(inet_suffix, vrf)
except Exception:
- raise vyos.opmode.DataUnavailable('Cannot fetch DHCP server configuration')
+ if vrf:
+ raise vyos.opmode.DataUnavailable(
+ f'Cannot fetch DHCP server configuration for VRF {vrf}'
+ )
+ else:
+ raise vyos.opmode.DataUnavailable('Cannot fetch DHCP server configuration')
active_pools = kea_get_dhcp_pools(active_config, inet_suffix)
if pool and active_pools and pool not in active_pools:
- raise vyos.opmode.IncorrectValue(f'DHCP{v} pool "{pool}" does not exist!')
+ if vrf:
+ raise vyos.opmode.IncorrectValue(
+ f'DHCP{v} pool "{pool}" does not exist for VRF {vrf}!'
+ )
+ else:
+ raise vyos.opmode.IncorrectValue(f'DHCP{v} pool "{pool}" does not exist!')
- pool_data = _get_raw_server_pool_statistics(active_config, family=family, pool=pool)
+ pool_data = _get_raw_server_pool_statistics(
+ active_config, family=family, vrf=vrf, pool=pool
+ )
if raw:
return pool_data
else:
@@ -332,6 +386,7 @@ def show_server_pool_statistics(
def show_server_leases(
raw: bool,
family: ArgFamily,
+ vrf: typing.Optional[str],
pool: typing.Optional[str],
sorted: typing.Optional[str],
state: typing.Optional[ArgState],
@@ -340,18 +395,33 @@ def show_server_leases(
v = 'v6' if family == 'inet6' else ''
inet_suffix = '6' if family == 'inet6' else '4'
- if not is_systemd_service_running(f'kea-dhcp{inet_suffix}-server.service'):
+ if vrf:
+ service = f'isc-kea-dhcp{inet_suffix}-server@{vrf}.service'
+ else:
+ service = f'isc-kea-dhcp{inet_suffix}-server.service'
+
+ if not is_systemd_service_running(service):
Warning(stale_warn_msg)
try:
- active_config = kea_get_active_config(inet_suffix)
+ active_config = kea_get_active_config(inet_suffix, vrf)
except Exception:
- raise vyos.opmode.DataUnavailable('Cannot fetch DHCP server configuration')
+ if vrf:
+ raise vyos.opmode.DataUnavailable(
+ f'Cannot fetch DHCP server configuration for VRF {vrf}'
+ )
+ else:
+ raise vyos.opmode.DataUnavailable('Cannot fetch DHCP server configuration')
active_pools = kea_get_dhcp_pools(active_config, inet_suffix)
if pool and active_pools and pool not in active_pools:
- raise vyos.opmode.IncorrectValue(f'DHCP{v} pool "{pool}" does not exist!')
+ if vrf:
+ raise vyos.opmode.IncorrectValue(
+ f'DHCP{v} pool "{pool}" does not exist for VRF {vrf}!'
+ )
+ else:
+ raise vyos.opmode.IncorrectValue(f'DHCP{v} pool "{pool}" does not exist!')
sort_valid = sort_valid_inet6 if family == 'inet6' else sort_valid_inet
if sorted and sorted not in sort_valid:
@@ -363,6 +433,7 @@ def show_server_leases(
lease_data = _get_raw_server_leases(
config=active_config,
family=family,
+ vrf=vrf,
pool=pool,
sorted=sorted,
state=state,
@@ -378,24 +449,40 @@ def show_server_leases(
def show_server_static_mappings(
raw: bool,
family: ArgFamily,
+ vrf: typing.Optional[str],
pool: typing.Optional[str],
sorted: typing.Optional[str],
):
v = 'v6' if family == 'inet6' else ''
inet_suffix = '6' if family == 'inet6' else '4'
- if not is_systemd_service_running(f'kea-dhcp{inet_suffix}-server.service'):
+ if vrf:
+ service = f'isc-kea-dhcp{inet_suffix}-server@{vrf}.service'
+ else:
+ service = f'isc-kea-dhcp{inet_suffix}-server.service'
+
+ if not is_systemd_service_running(service):
Warning(stale_warn_msg)
try:
- active_config = kea_get_active_config(inet_suffix)
+ active_config = kea_get_active_config(inet_suffix, vrf)
except Exception:
- raise vyos.opmode.DataUnavailable('Cannot fetch DHCP server configuration')
+ if vrf:
+ raise vyos.opmode.DataUnavailable(
+ f'Cannot fetch DHCP server configuration for VRF {vrf}'
+ )
+ else:
+ raise vyos.opmode.DataUnavailable('Cannot fetch DHCP server configuration')
active_pools = kea_get_dhcp_pools(active_config, inet_suffix)
if pool and active_pools and pool not in active_pools:
- raise vyos.opmode.IncorrectValue(f'DHCP{v} pool "{pool}" does not exist!')
+ if vrf:
+ raise vyos.opmode.IncorrectValue(
+ f'DHCP{v} pool "{pool}" does not exist for VRF {vrf}!'
+ )
+ else:
+ raise vyos.opmode.IncorrectValue(f'DHCP{v} pool "{pool}" does not exist!')
if sorted and sorted not in mapping_sort_valid:
raise vyos.opmode.IncorrectValue(f'DHCP{v} sort "{sorted}" is invalid!')
@@ -409,21 +496,21 @@ def show_server_static_mappings(
return _get_formatted_server_static_mappings(static_mappings)
-def _lease_valid(inet, address):
- leases = kea_get_leases(inet)
+def _lease_valid(inet, vrf, address):
+ leases = kea_get_leases(inet, vrf)
return any(lease['ip-address'] == address for lease in leases)
@_verify_server
-def clear_dhcp_server_lease(family: ArgFamily, address: str):
+def clear_dhcp_server_lease(family: ArgFamily, address: str, vrf: typing.Optional[str]):
v = 'v6' if family == 'inet6' else ''
inet = '6' if family == 'inet6' else '4'
- if not _lease_valid(inet, address):
+ if not _lease_valid(inet, vrf, address):
print(f'Lease not found on DHCP{v} server')
return None
- if not kea_delete_lease(inet, address):
+ if not kea_delete_lease(inet, vrf, address):
print(f'Failed to clear lease for "{address}"')
return None
@@ -509,7 +596,7 @@ def _get_formatted_client_leases(lease_data):
if 'new_dhcp_server_identifier' in lease:
data_entries.append(['DHCP Server', lease['new_dhcp_server_identifier']])
if 'new_dhcp_lease_time' in lease:
- data_entries.append(['DHCP Server', lease['new_dhcp_lease_time']])
+ data_entries.append(['Lease Time', lease['new_dhcp_lease_time']])
if 'vrf' in lease:
data_entries.append(['VRF', lease['vrf']])
if 'last_update' in lease:
diff --git a/src/op_mode/dns.py b/src/op_mode/dns.py
index 16c462f23..7c9f769f1 100755
--- a/src/op_mode/dns.py
+++ b/src/op_mode/dns.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/evpn.py b/src/op_mode/evpn.py
index cae4ab9f5..a6dee0b34 100644
--- a/src/op_mode/evpn.py
+++ b/src/op_mode/evpn.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2016-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/execute_bandwidth_test.sh b/src/op_mode/execute_bandwidth_test.sh
index a6ad0b42c..a7c7484d2 100755
--- a/src/op_mode/execute_bandwidth_test.sh
+++ b/src/op_mode/execute_bandwidth_test.sh
@@ -1,6 +1,6 @@
#!/bin/sh
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/execute_port-scan.py b/src/op_mode/execute_port-scan.py
index bf17d0379..47cd2f7c4 100644
--- a/src/op_mode/execute_port-scan.py
+++ b/src/op_mode/execute_port-scan.py
@@ -1,6 +1,6 @@
#! /usr/bin/env python3
#
-# Copyright (C) 2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/file.py b/src/op_mode/file.py
index bf13bed6f..8420c5355 100755
--- a/src/op_mode/file.py
+++ b/src/op_mode/file.py
@@ -1,6 +1,6 @@
#!/usr/bin/python3
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -133,9 +133,6 @@ def print_file_data(path: str) -> None:
with open(path, 'r') as f:
for line in f:
print(line, end='')
- # tcpdump files go to TShark.
- elif 'pcap' in file_type or os.path.splitext(path)[1] == '.pcap':
- print(cmd(['sudo', 'tshark', '-r', path]))
# All other binaries get hexdumped.
else:
print(cmd(['hexdump', '-C', path]))
diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py
index 7a3ab921d..d5cd088e6 100755
--- a/src/op_mode/firewall.py
+++ b/src/op_mode/firewall.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -18,6 +18,7 @@ import argparse
import ipaddress
import json
import re
+from signal import signal, SIGPIPE, SIG_DFL
import tabulate
import textwrap
@@ -25,6 +26,9 @@ from vyos.config import Config
from vyos.utils.process import cmd
from vyos.utils.dict import dict_search_args
+signal(SIGPIPE, SIG_DFL)
+
+
def get_config_node(conf, node=None, family=None, hook=None, priority=None):
if node == 'nat':
if family == 'ipv6':
@@ -148,6 +152,38 @@ def get_nftables_group_members(family, table, name):
return out
+def get_nftables_remote_group_members(family, table, name):
+ prefix = 'ip6' if family == 'ipv6' else 'ip'
+ out = []
+
+ try:
+ results_str = cmd(f'nft -j list set {prefix} {table} {name}')
+ results = json.loads(results_str)
+ except:
+ return out
+
+ if 'nftables' not in results:
+ return out
+
+ for obj in results['nftables']:
+ if 'set' not in obj:
+ continue
+
+ set_obj = obj['set']
+ if 'elem' in set_obj:
+ for elem in set_obj['elem']:
+ # search for single IP elements
+ if isinstance(elem, str):
+ out.append(elem)
+ # search for prefix elements
+ elif isinstance(elem, dict) and 'prefix' in elem:
+ out.append(f"{elem['prefix']['addr']}/{elem['prefix']['len']}")
+ # search for IP range elements
+ elif isinstance(elem, dict) and 'range' in elem:
+ out.append(f"{elem['range'][0]}-{elem['range'][1]}")
+
+ return out
+
def output_firewall_vertical(rules, headers, adjust=True):
for rule in rules:
adjusted_rule = rule + [""] * (len(headers) - len(rule)) if adjust else rule # account for different header length, like default-action
@@ -178,7 +214,7 @@ def output_firewall_name(family, hook, priority, firewall_conf, single_rule_id=N
row.append(rule_details['conditions'])
rows.append(row)
- if hook in ['input', 'forward', 'output']:
+ if hook in ['input', 'forward', 'output', 'prerouting']:
def_action = firewall_conf['default_action'] if 'default_action' in firewall_conf else 'accept'
else:
def_action = firewall_conf['default_action'] if 'default_action' in firewall_conf else 'drop'
@@ -316,7 +352,7 @@ def output_firewall_name_statistics(family, hook, prior, prior_conf, single_rule
rows.append(row)
- if hook in ['input', 'forward', 'output']:
+ if hook in ['input', 'forward', 'output', 'prerouting']:
row = ['default', '']
rule_details = details['default-action']
row.append(rule_details.get('packets', 0))
@@ -556,32 +592,8 @@ def show_firewall_group(name=None):
header_tail = []
for group_type, group_type_conf in firewall['group'].items():
- ##
- if group_type != 'dynamic_group':
-
- for group_name, group_conf in group_type_conf.items():
- if name and name != group_name:
- continue
-
- references = find_references(group_type, group_name)
- row = [group_name, textwrap.fill(group_conf.get('description') or '', 50), group_type, '\n'.join(references) or 'N/D']
- if 'address' in group_conf:
- row.append("\n".join(sorted(group_conf['address'])))
- elif 'network' in group_conf:
- row.append("\n".join(sorted(group_conf['network'], key=ipaddress.ip_network)))
- elif 'mac_address' in group_conf:
- row.append("\n".join(sorted(group_conf['mac_address'])))
- elif 'port' in group_conf:
- row.append("\n".join(sorted(group_conf['port'])))
- elif 'interface' in group_conf:
- row.append("\n".join(sorted(group_conf['interface'])))
- elif 'url' in group_conf:
- row.append(group_conf['url'])
- else:
- row.append('N/D')
- rows.append(row)
-
- else:
+ # iterate over dynamic-groups
+ if group_type == 'dynamic_group':
if not args.detail:
header_tail = ['Timeout', 'Expires']
@@ -590,6 +602,9 @@ def show_firewall_group(name=None):
prefix = 'DA_' if dynamic_type == 'address_group' else 'DA6_'
if dynamic_type in firewall['group']['dynamic_group']:
for dynamic_name, dynamic_conf in firewall['group']['dynamic_group'][dynamic_type].items():
+ if name and name != dynamic_name:
+ continue
+
references = find_references(dynamic_type, dynamic_name)
row = [dynamic_name, textwrap.fill(dynamic_conf.get('description') or '', 50), dynamic_type + '(dynamic)', '\n'.join(references) or 'N/D']
@@ -628,6 +643,68 @@ def show_firewall_group(name=None):
header_tail += [""] * (len(members) - 1)
rows.append(row)
+ # iterate over remote-groups
+ elif group_type == 'remote_group':
+ for remote_name, remote_conf in group_type_conf.items():
+ if name and name != remote_name:
+ continue
+
+ references = find_references(group_type, remote_name)
+ row = [remote_name, textwrap.fill(remote_conf.get('description') or '', 50), group_type, '\n'.join(references) or 'N/D']
+ members = get_nftables_remote_group_members("ipv4", 'vyos_filter', f'R_{remote_name}')
+ members6 = get_nftables_remote_group_members("ipv6", 'vyos_filter', f'R6_{remote_name}')
+
+ if 'url' in remote_conf:
+ # display only the url if no members are found for both views
+ if not members and not members6:
+ if args.detail:
+ header_tail = ['IPv6 Members', 'Remote URL']
+ row.append('N/D')
+ row.append('N/D')
+ row.append(remote_conf['url'])
+ else:
+ row.append(remote_conf['url'])
+ rows.append(row)
+ else:
+ # display all table elements in detail view
+ if args.detail:
+ header_tail = ['IPv6 Members', 'Remote URL']
+ if members:
+ row.append(' '.join(members))
+ else:
+ row.append('N/D')
+ if members6:
+ row.append(' '.join(members6))
+ else:
+ row.append('N/D')
+ row.append(remote_conf['url'])
+ rows.append(row)
+ else:
+ row.append(remote_conf['url'])
+ rows.append(row)
+
+ # catch the rest of the group types
+ else:
+ for group_name, group_conf in group_type_conf.items():
+ if name and name != group_name:
+ continue
+
+ references = find_references(group_type, group_name)
+ row = [group_name, textwrap.fill(group_conf.get('description') or '', 50), group_type, '\n'.join(references) or 'N/D']
+ if 'address' in group_conf:
+ row.append("\n".join(sorted(group_conf['address'])))
+ elif 'network' in group_conf:
+ row.append("\n".join(sorted(group_conf['network'], key=ipaddress.ip_network)))
+ elif 'mac_address' in group_conf:
+ row.append("\n".join(sorted(group_conf['mac_address'])))
+ elif 'port' in group_conf:
+ row.append("\n".join(sorted(group_conf['port'])))
+ elif 'interface' in group_conf:
+ row.append("\n".join(sorted(group_conf['interface'])))
+ else:
+ row.append('N/D')
+ rows.append(row)
+
if rows:
print('Firewall Groups\n')
if args.detail:
diff --git a/src/op_mode/flow_accounting_op.py b/src/op_mode/flow_accounting_op.py
index 497ccafdf..078634610 100755
--- a/src/op_mode/flow_accounting_op.py
+++ b/src/op_mode/flow_accounting_op.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -18,18 +18,16 @@ import sys
import argparse
import re
import ipaddress
-import os.path
from tabulate import tabulate
-from json import loads
-from vyos.utils.commit import commit_in_progress
+from vyos.utils.kernel import is_module_loaded
from vyos.utils.process import cmd
-from vyos.utils.process import run
from vyos.logger import syslog
+from vyos.configquery import ConfigTreeQuery
+from vyos import ipt_netflow
# some default values
-uacctd_pidfile = '/var/run/uacctd.pid'
-uacctd_pipefile = '/tmp/uacctd.pipe'
+flows_dump_path = '/proc/net/stat/ipt_netflow_flows'
def parse_port(port):
try:
@@ -45,7 +43,7 @@ def parse_ports(arg):
if re.match(r'^\d+$', arg):
# Single port
port = parse_port(arg)
- return {"type": "single", "value": port}
+ return {"type": "single", "values": (port,)}
elif re.match(r'^\d+\-\d+$', arg):
# Port range
ports = arg.split("-")
@@ -53,12 +51,12 @@ def parse_ports(arg):
if ports[0] > ports[1]:
raise ValueError("Malformed port range \'{0}\': lower end is greater than the higher".format(arg))
else:
- return {"type": "range", "value": (ports[0], ports[1])}
+ return {"type": "range", "values": range(ports[0], ports[1] + 1)}
elif re.match(r'^\d+,.*\d$', arg):
# Port list
- ports = re.split(r',+', arg) # This allows duplicate commad like '1,,2,3,4'
+ ports = re.split(r',+', arg) # This allows duplicate commas like '1,,2,3,4'
ports = list(map(parse_port, ports))
- return {"type": "list", "value": ports}
+ return {"type": "list", "values": ports}
else:
raise ValueError("Malformed port spec \'{0}\'".format(arg))
@@ -69,9 +67,8 @@ def check_host(host):
raise ValueError("Invalid host \'{}\', must be a valid IP or IPv6 address".format(host))
# check if flow-accounting running
-def _uacctd_running():
- command = 'systemctl status uacctd.service > /dev/null'
- return run(command) == 0
+def _netflow_running():
+ return is_module_loaded(ipt_netflow.module_name)
# get list of interfaces
@@ -89,26 +86,62 @@ def _get_ifaces_dict():
if regex_filter.search(iface_line):
ifaces_dict[int(regex_filter.search(iface_line).group('iface_index'))] = regex_filter.search(iface_line).group('iface_name')
- # return dictioanry
+ # return dictionary
return ifaces_dict
# get list of flows
def _get_flows_list():
- # run command to get flows list
- out = cmd(f'/usr/bin/pmacct -s -O json -T flows -p {uacctd_pipefile}',
- message='Failed to get flows list')
+ # File format:
+ # When MAC disabled:
+ # # hash a dev:i,o proto src:ip,port dst:ip,port nexthop tos,tcpflags,options,tcpoptions packets bytes ts:first,last
+ # 1 c06c 0 4,-1 1 10.2.0.7,0 10.1.0.5,0 0.0.0.0 0,0,0,0 186 15624 92261,131
+ # 2 1e3ca 0 3,-1 1 10.1.0.5,0 10.2.0.7,2048 0.0.0.0 0,0,0,0 186 15624 92261,132
+
+ # When MAC enabled + VLAN fix:
+ # hash a dev:i,o mac:src,dst vlan type proto src:ip,port dst:ip,port nexthop tos,tcpflags,options,tcpoptions packets bytes ts:first,last
+ # 1 11a41 0 4,-1 0c:27:1f:55:00:00,0c:e8:b1:71:00:02 - 0800 1 10.2.0.7,0 10.1.0.5,0 0.0.0.0 0,0,0,0 1182 99288 591502,529
+ # 2 13bc5 0 4,-1 0c:27:1f:55:00:00,0c:e8:b1:71:00:02 - 0800 1 10.2.0.7,0 10.2.0.1,2048 0.0.0.0 0,0,0,0 577 48468 590831,1006
+ # 3 166dd 0 3,-1 0c:f1:0a:d5:00:00,0c:e8:b1:71:00:01 - 0800 1 10.1.0.5,0 10.2.0.7,2048 0.0.0.0 0,0,0,0 1182 99288 591502,529
- # read output
- flows_out = out.splitlines()
- # make a list with flows
flows_list = []
- for flow_line in flows_out:
- try:
- flows_list.append(loads(flow_line))
- except Exception as err:
- syslog.error('Unable to read flow info: {}'.format(err))
+ with open(flows_dump_path) as f:
+ headers = f.readline()
+ headers = headers.split()
+ for i, h in enumerate(headers):
+
+ if ',' in h and ':' not in h:
+ h = 'extra:' + h
+
+ if ':' in h:
+ key, subkeys = h.split(':', 1)
+ headers[i] = {'key': key, 'subkeys': subkeys.split(',')}
+
+ linenum = 1
+ for flow_line in f:
+ linenum += 1
+ flow_dict = {}
+ flow_line = flow_line.split()
+ if len(flow_line) != len(headers):
+ syslog.error(
+ f'Unexpected number of elements in {flows_dump_path}, line {linenum}'
+ )
+ continue
+ for i, val in enumerate(flow_line):
+ if isinstance(headers[i], str):
+ flow_dict[headers[i]] = val
+ elif isinstance(headers[i], dict):
+ val = val.split(',')
+ if len(val) != len(headers[i]['subkeys']):
+ syslog.error(
+ f"Unexpected number of elements in {flows_dump_path} in column {headers[i]['key']} in line {linenum}"
+ )
+ continue
+ flow_dict[headers[i]['key']] = dict(zip(headers[i]['subkeys'], val))
+ else:
+ assert False, "Unexpected type of header"
+ flows_list.append(flow_dict)
# return list of flows
return flows_list
@@ -119,12 +152,15 @@ def _flows_filter(flows, ifaces):
# predefine filtered flows list
flows_filtered = []
+ def _iface_to_str(iface):
+ if int(iface) in ifaces:
+ return ifaces[int(iface)]
+ return 'unknown'
+
# add interface names to flows
for flow in flows:
- if flow['iface_in'] in ifaces:
- flow['iface_in_name'] = ifaces[flow['iface_in']]
- else:
- flow['iface_in_name'] = 'unknown'
+ flow['iface_in_name'] = _iface_to_str(flow['dev']['i'])
+ flow['iface_out_name'] = _iface_to_str(flow['dev']['o'])
# iterate through flows list
for flow in flows:
@@ -134,16 +170,19 @@ def _flows_filter(flows, ifaces):
continue
# filter by host
if cmd_args.host:
- if flow['ip_src'] != cmd_args.host and flow['ip_dst'] != cmd_args.host:
+ if (
+ flow['src']['ip'] != cmd_args.host
+ and flow['dst']['ip'] != cmd_args.host
+ ):
continue
# filter by ports
if cmd_args.ports:
- if cmd_args.ports['type'] == 'single':
- if flow['port_src'] != cmd_args.ports['value'] and flow['port_dst'] != cmd_args.ports['value']:
- continue
- else:
- if flow['port_src'] not in cmd_args.ports['value'] and flow['port_dst'] not in cmd_args.ports['value']:
- continue
+ # for 'single' it is a tuple with one value, for 'list' - list of ports, for range - range of ports
+ if (
+ int(flow['src']['port']) not in cmd_args.ports['values']
+ and int(flow['dst']['port']) not in cmd_args.ports['values']
+ ):
+ continue
# add filtered flows to new list
flows_filtered.append(flow)
@@ -159,23 +198,36 @@ def _flows_filter(flows, ifaces):
# print flow table
def _flows_table_print(flows):
# define headers and body
- table_headers = ['IN_IFACE', 'SRC_MAC', 'DST_MAC', 'SRC_IP', 'DST_IP', 'SRC_PORT', 'DST_PORT', 'PROTOCOL', 'TOS', 'PACKETS', 'FLOWS', 'BYTES']
+ table_headers = [
+ 'IN_IFACE',
+ 'SRC_MAC',
+ 'DST_MAC',
+ 'SRC_IP',
+ 'DST_IP',
+ 'SRC_PORT',
+ 'DST_PORT',
+ 'PROTOCOL',
+ 'TOS',
+ 'PACKETS',
+ # 'FLOWS', # What was here in pmacct?
+ 'BYTES',
+ ]
table_body = []
# convert flows to list
for flow in flows:
table_line = [
flow.get('iface_in_name'),
- flow.get('mac_src'),
- flow.get('mac_dst'),
- flow.get('ip_src'),
- flow.get('ip_dst'),
- flow.get('port_src'),
- flow.get('port_dst'),
- flow.get('ip_proto'),
- flow.get('tos'),
+ flow.get('mac', {}).get('src'),
+ flow.get('mac', {}).get('dst'),
+ flow.get('src', {}).get('ip'),
+ flow.get('dst', {}).get('ip'),
+ flow.get('src', {}).get('port'),
+ flow.get('dst', {}).get('port'),
+ flow.get('proto'),
+ flow.get('extra', {}).get('tos'),
flow.get('packets'),
- flow.get('flows'),
- flow.get('bytes')
+ # flow.get('flows'),
+ flow.get('bytes'),
]
table_body.append(table_line)
# configure and fill table
@@ -190,21 +242,37 @@ def _flows_table_print(flows):
sys.exit(0)
-# check if in-memory table is active
-def _check_imt():
- if not os.path.exists(uacctd_pipefile):
- print("In-memory table is not available")
- sys.exit(1)
-
-
# define program arguments
cmd_args_parser = argparse.ArgumentParser(description='show flow-accounting')
-cmd_args_parser.add_argument('--action', choices=['show', 'clear', 'restart'], required=True, help='command to flow-accounting daemon')
-cmd_args_parser.add_argument('--filter', choices=['interface', 'host', 'ports', 'top'], required=False, nargs='*', help='filter flows to display')
-cmd_args_parser.add_argument('--interface', required=False, help='interface name for output filtration')
-cmd_args_parser.add_argument('--host', type=str, required=False, help='host address for output filtering')
-cmd_args_parser.add_argument('--ports', type=str, required=False, help='port number, range or list for output filtering')
-cmd_args_parser.add_argument('--top', type=int, required=False, help='top records for output filtering')
+# 'clear' and 'restart' are not implemented
+cmd_args_parser.add_argument(
+ '--action',
+ choices=['show', 'restart'],
+ default='show',
+ help='show stat or restart module',
+)
+cmd_args_parser.add_argument(
+ '--filter',
+ choices=['interface', 'host', 'ports', 'top'],
+ required=False,
+ nargs='*',
+ help='filter flows to display',
+)
+cmd_args_parser.add_argument(
+ '--interface', required=False, help='interface name for output filtration'
+)
+cmd_args_parser.add_argument(
+ '--host', type=str, required=False, help='host address for output filtering'
+)
+cmd_args_parser.add_argument(
+ '--ports',
+ type=str,
+ required=False,
+ help='port number, range or list for output filtering',
+)
+cmd_args_parser.add_argument(
+ '--top', type=int, required=False, help='top records for output filtering'
+)
# parse arguments
cmd_args = cmd_args_parser.parse_args()
@@ -219,30 +287,13 @@ except ValueError as e:
sys.exit(1)
# main logic
-# do nothing if uacctd daemon is not running
-if not _uacctd_running():
+# do nothing if ipt_NETFLOW is not active
+if not _netflow_running():
print("flow-accounting is not active")
sys.exit(1)
-# restart pmacct daemon
-if cmd_args.action == 'restart':
- if commit_in_progress():
- print('Cannot restart flow-accounting while a commit is in progress')
- exit(1)
- # run command to restart flow-accounting
- cmd('systemctl restart uacctd.service',
- message='Failed to restart flow-accounting')
-
-# clear in-memory collected flows
-if cmd_args.action == 'clear':
- _check_imt()
- # run command to clear flows
- cmd(f'/usr/bin/pmacct -e -p {uacctd_pipefile}',
- message='Failed to clear flows')
-
# show table with flows
if cmd_args.action == 'show':
- _check_imt()
# get interfaces index and names
ifaces_dict = _get_ifaces_dict()
# get flows
@@ -254,4 +305,22 @@ if cmd_args.action == 'show':
# print flows
_flows_table_print(tabledata)
+if cmd_args.action == 'restart':
+ ipt_netflow.stop()
+
+ # get needed interfaces
+ conf = ConfigTreeQuery()
+ config_path = ['system', 'flow-accounting']
+ if not conf.exists(config_path + ['netflow', 'interface']):
+ print("Flow accounting not configured, exiting")
+ sys.exit(1)
+
+ ingress_interfaces = conf.values(config_path + ['netflow', 'interface'])
+ if conf.exists(config_path + ['enable-egress']):
+ egress_interfaces = ingress_interfaces
+ else:
+ egress_interfaces = []
+
+ ipt_netflow.start(ingress_interfaces, egress_interfaces)
+
sys.exit(0)
diff --git a/src/op_mode/force_mtu_host.sh b/src/op_mode/force_mtu_host.sh
index c72fc243f..e3e24b57b 100755
--- a/src/op_mode/force_mtu_host.sh
+++ b/src/op_mode/force_mtu_host.sh
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
diff --git a/src/op_mode/force_root-partition-auto-resize.sh b/src/op_mode/force_root-partition-auto-resize.sh
index b39e87560..e17f63a88 100755
--- a/src/op_mode/force_root-partition-auto-resize.sh
+++ b/src/op_mode/force_root-partition-auto-resize.sh
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
diff --git a/src/op_mode/format_disk.py b/src/op_mode/format_disk.py
index dc3c96322..56cb51a02 100755
--- a/src/op_mode/format_disk.py
+++ b/src/op_mode/format_disk.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2021 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -123,10 +123,10 @@ if __name__ == '__main__':
f'\ndata on {target_disk}.\n')
if not ask_yes_no('Do you wish to proceed?'):
- print(f'Disk drive {target_disk} will not be re-formated')
+ print(f'Disk drive {target_disk} will not be re-formatted')
exit(0)
- print(f'Re-formating disk drive {target_disk}...')
+ print(f'Re-formatting disk drive {target_disk}...')
print('Making backup copy of partitions...')
backup_partitions(target_disk)
diff --git a/src/op_mode/generate_interfaces_debug_archive.py b/src/op_mode/generate_interfaces_debug_archive.py
index 3059aad23..cf1f23c42 100755
--- a/src/op_mode/generate_interfaces_debug_archive.py
+++ b/src/op_mode/generate_interfaces_debug_archive.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -54,7 +54,7 @@ CMD_INTERFACES_LIST: list[str] = [
"ethtool --phy-statistics "
]
-# get intefaces info
+# get interfaces info
interfaces_list = os.popen('ls /sys/class/net/').read().split()
# modify CMD_INTERFACES_LIST for all interfaces
diff --git a/src/op_mode/generate_ipsec_debug_archive.py b/src/op_mode/generate_ipsec_debug_archive.py
index ca2eeb511..de0f25bfd 100755
--- a/src/op_mode/generate_ipsec_debug_archive.py
+++ b/src/op_mode/generate_ipsec_debug_archive.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/generate_openconnect_otp_key.py b/src/op_mode/generate_openconnect_otp_key.py
index 99b67d261..5cb1c6edf 100755
--- a/src/op_mode/generate_openconnect_otp_key.py
+++ b/src/op_mode/generate_openconnect_otp_key.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022-2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/generate_ovpn_client_file.py b/src/op_mode/generate_ovpn_client_file.py
index 1d2f1067a..a55f2e3c5 100755
--- a/src/op_mode/generate_ovpn_client_file.py
+++ b/src/op_mode/generate_ovpn_client_file.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -108,13 +108,13 @@ if __name__ == '__main__':
required=True,
)
parser.add_argument(
- "-a", "--ca", type=str, help='OpenVPN CA cerificate', required=True
+ "-a", "--ca", type=str, help='OpenVPN CA certificate', required=True
)
parser.add_argument(
- "-c", "--cert", type=str, help='OpenVPN client cerificate', required=True
+ "-c", "--cert", type=str, help='OpenVPN client certificate', required=True
)
parser.add_argument(
- "-k", "--key", type=str, help='OpenVPN client cerificate key', action="store"
+ "-k", "--key", type=str, help='OpenVPN client certificate key', action="store"
)
args = parser.parse_args()
diff --git a/src/op_mode/generate_psk.py b/src/op_mode/generate_psk.py
index d51293712..816150dbf 100644
--- a/src/op_mode/generate_psk.py
+++ b/src/op_mode/generate_psk.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/generate_public_key_command.py b/src/op_mode/generate_public_key_command.py
index 8ba55c901..1c1246742 100755
--- a/src/op_mode/generate_public_key_command.py
+++ b/src/op_mode/generate_public_key_command.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022-2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/generate_service_rule-resequence.py b/src/op_mode/generate_service_rule-resequence.py
index 9333d6353..fd6354110 100755
--- a/src/op_mode/generate_service_rule-resequence.py
+++ b/src/op_mode/generate_service_rule-resequence.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/generate_ssh_server_key.py b/src/op_mode/generate_ssh_server_key.py
index d6063c43c..459157afd 100755
--- a/src/op_mode/generate_ssh_server_key.py
+++ b/src/op_mode/generate_ssh_server_key.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -15,6 +15,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from sys import exit
+
+from vyos.defaults import directories
from vyos.utils.io import ask_yes_no
from vyos.utils.process import cmd
from vyos.utils.commit import commit_in_progress
@@ -26,6 +28,8 @@ if commit_in_progress():
print('Cannot restart SSH while a commit is in progress')
exit(1)
+conf_mode_dir = directories['conf_mode']
+
cmd('rm -v /etc/ssh/ssh_host_*')
cmd('dpkg-reconfigure openssh-server')
-cmd('systemctl restart ssh.service')
+cmd(f'{conf_mode_dir}/service_ssh.py')
diff --git a/src/op_mode/generate_system_login_user.py b/src/op_mode/generate_system_login_user.py
index 1b328eae0..c0cb69708 100755
--- a/src/op_mode/generate_system_login_user.py
+++ b/src/op_mode/generate_system_login_user.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022-2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/generate_tech-support_archive.py b/src/op_mode/generate_tech-support_archive.py
index 41b53cd15..d005d78ee 100755
--- a/src/op_mode/generate_tech-support_archive.py
+++ b/src/op_mode/generate_tech-support_archive.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -13,34 +13,32 @@
#
# 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 argparse
import glob
from datetime import datetime
from pathlib import Path
from shutil import rmtree
-
from socket import gethostname
from sys import exit
from tarfile import open as tar_open
-from vyos.utils.process import rc_cmd
+
+from vyos.defaults import directories
+from vyos.utils.process import call
+from vyos.utils.process import cmd
+from vyos.utils.file import get_name_from_path
from vyos.remote import upload
-def op(cmd: str) -> str:
- """Returns a command with the VyOS operational mode wrapper."""
- return f'/opt/vyatta/bin/vyatta-op-cmd-wrapper {cmd}'
-
-def save_stdout(command: str, file: Path) -> None:
- rc, stdout = rc_cmd(command)
- body: str = f'''### {command} ###
-Command: {command}
-Exit code: {rc}
-Stdout:
-{stdout}
-
-'''
- with file.open(mode='a') as f:
- f.write(body)
+
+# Example: bdbdd9a4807f_tech-support-archive_2026-02-02T13-53-18
+ARCHIVE_PATTERN = '_tech-support-archive_'
+# Example: drops-debug_2026-02-02T13-53-18
+ARCHIVE_TMP_DIR_PATTERN = 'drops-debug_'
+DEFAULT_TMP_DIR = '/tmp'
+EXCLUDED_ARCHIVE_EXT = ('.iso', '.gz', '.tar', '.zip')
+
+
def __rotate_logs(path: str, log_pattern:str):
files_list = glob.glob(f'{path}/{log_pattern}')
if len(files_list) > 5:
@@ -48,12 +46,59 @@ def __rotate_logs(path: str, log_pattern:str):
os.remove(oldest_file)
+def __save_show_report_files(reports_dir: Path):
+ """
+ Save result of execution `show tech-support report` command
+ :param reports_dir: path to the result directory
+ :type reports_dir: pathlib.Path
+ """
+
+ vyos_op_scripts_dir = directories['op_mode']
+ script_path = f'{vyos_op_scripts_dir}/show_techsupport_report.py'
+ arguments = [
+ '--launched-from-generate-archive',
+ '--outdir',
+ str(reports_dir),
+ ]
+ output = cmd([script_path] + arguments)
+
+ if output.strip():
+ print(output)
+
+
def __generate_archived_files(location_path: str) -> None:
"""
- Generate arhives of main directories
+ Generate archives of main directories
:param location_path: path to temporary directory
:type location_path: str
"""
+
+ # sync/flush journald before archiving /var/log/journal
+ cmd(['journalctl', '--sync'])
+ cmd(['journalctl', '--flush'])
+
+ def __tar_filter(tarinfo):
+ # path inside tar, because we set arcname=... below
+ name = tarinfo.name
+ basename = os.path.basename(name)
+
+ # /var/log: exclude /var/log/messages and /var/log/messages.*
+ if name.startswith('var/log/messages'):
+ if basename == 'messages' or basename.startswith('messages.'):
+ return None
+
+ # /tmp, /home: exclude previous tech-support archives and temporary archive directories
+ if name.startswith(('tmp/', 'home/')):
+ if ARCHIVE_PATTERN in name or basename.startswith(ARCHIVE_TMP_DIR_PATTERN):
+ return None
+
+ # /home, /opt/vyatta/etc/config, /tmp: exclude general archives
+ if name.startswith(('home/', 'opt/vyatta/etc/config/', 'tmp/')):
+ if basename.lower().endswith(EXCLUDED_ARCHIVE_EXT):
+ return None
+
+ return tarinfo
+
# Dictionary arhive_name:directory_to_arhive
archive_dict = {
'etc': '/etc',
@@ -62,87 +107,149 @@ def __generate_archived_files(location_path: str) -> None:
'root': '/root',
'tmp': '/tmp',
'core-dump': '/var/core',
- 'config': '/opt/vyatta/etc/config'
- }
- # Dictionary arhive_name:excluding pattern
- archive_excludes = {
- # Old location of archives
- 'config': 'tech-support-archive',
- # New locations of arhives
- 'tmp': 'tech-support-archive'
+ 'config': '/opt/vyatta/etc/config',
+ 'run': '/run',
}
+
for archive_name, path in archive_dict.items():
- archive_file: str = f'{location_path}/{archive_name}.tar.gz'
+ if not os.path.exists(path):
+ continue
+
+ arcname = str(path).lstrip('/') # e.g. /etc -> 'etc'
+
+ archive_file = f'{location_path}/{archive_name}.tar.gz'
with tar_open(name=archive_file, mode='x:gz') as tar_file:
- if archive_name in archive_excludes:
- tar_file.add(path, filter=lambda x: None if str(archive_excludes[archive_name]) in str(x.name) else x)
- else:
- tar_file.add(path)
+ try:
+ tar_file.add(path, arcname=arcname, filter=__tar_filter)
+ except (PermissionError, OSError) as e:
+ print(f'Unable to read `{path}` to archive files:', e)
+ continue # skip paths we can't read
def __generate_main_archive_file(archive_file: str, tmp_dir_path: str) -> None:
"""
- Generate main arhive file
- :param archive_file: name of arhive file
+ Generate main archive file
+ :param archive_file: name of archive file
:type archive_file: str
- :param tmp_dir_path: path to arhive memeber
+ :param tmp_dir_path: path to archive member
:type tmp_dir_path: str
"""
+
+ arcname = get_name_from_path(archive_file)
with tar_open(name=archive_file, mode='x:gz') as tar_file:
- tar_file.add(tmp_dir_path, arcname=os.path.basename(tmp_dir_path))
+ tar_file.add(tmp_dir_path, arcname=arcname)
+
+def __generate_topology_snapshots(output_dir: Path) -> None:
+ """
+ Generates physical and logical topology PNG files using `lstopo`
+
+ :param output_dir: directory where topology PNGs will be stored
+ """
+
+ physical_topo = output_dir / 'topology.png'
+ logical_topo = output_dir / 'topology-logical.png'
+
+ # Capture physical topology
+ call(['lstopo', '--output-format', 'png', str(physical_topo)])
+
+ # Capture logical topology
+ call(['lstopo', '--logical', '--output-format', 'png', str(logical_topo)])
+
+
+def __resolve_main_archive_path(input_path: str, default_archive_name: str) -> Path:
+ """
+ Normalize path for saving a .tar.gz file based on rules:
+
+ Rules:
+ - file -> file.tar.gz
+ - file.tar -> file.tar.gz
+ - file.tgz -> file.tgz
+ - dir/ -> dir/{default_archive_name}
+ - ../dir/file.tar.gz -> (unchanged)
+ - file.zip -> file.tar.gz
+
+ :param input_path: user's provided path to the archive
+ :param default_archive_name: name of archive if user didn't provide it
+ """
+
+ path = Path(input_path)
+
+ # Case 1: default temporary directory -> extend by default name of file
+ if input_path == DEFAULT_TMP_DIR:
+ return path / default_archive_name
+
+ # Case 2: already .tar.gz -> return unchanged
+ if path.name.endswith(('.tar.gz', '.tgz')):
+ return path
+
+ # Case 3: already .tar -> .tar.gz
+ if path.name.endswith('.tar'):
+ return path.with_suffix('.tar.gz')
+
+ # Case 4: directory (explicit trailing slash OR existing directory)
+ if input_path.endswith(('/', '\\')) or path.is_dir():
+ dir_path = path
+ return dir_path / default_archive_name
+
+ # Default behavior for any other extension
+ return path.with_suffix('.tar.gz')
if __name__ == '__main__':
- defualt_tmp_dir = '/tmp'
parser = argparse.ArgumentParser()
- parser.add_argument("path", nargs='?', default=defualt_tmp_dir)
+ parser.add_argument('path', nargs='?', default=DEFAULT_TMP_DIR)
args = parser.parse_args()
- location_path = args.path[:-1] if args.path[-1] == '/' else args.path
hostname: str = gethostname()
- time_now: str = datetime.now().isoformat(timespec='seconds').replace(":", "-")
-
- remote = False
- tmp_path = ''
- tmp_dir_path = ''
- if 'ftp://' in args.path or 'scp://' in args.path:
- remote = True
- tmp_path = defualt_tmp_dir
+ time_now: str = datetime.now().isoformat(timespec='seconds').replace(':', '-')
+ default_archive_inner_dir = f'{hostname}{ARCHIVE_PATTERN}{time_now}'
+ default_archive_name = f'{default_archive_inner_dir}.tar.gz'
+
+ is_remote = args.path.startswith(('ftp://', 'scp://'))
+ if is_remote:
+ base_tmp_path = DEFAULT_TMP_DIR
+ archive_dest_path = Path(f'{base_tmp_path}/{default_archive_name}')
else:
- tmp_path = location_path
- archive_pattern = f'_tech-support-archive_'
- archive_file_name = f'{hostname}{archive_pattern}{time_now}.tar.gz'
+ # Define destination path to the main archive file based on a rules
+ archive_dest_path = __resolve_main_archive_path(args.path, default_archive_name)
+ base_tmp_path = str(archive_dest_path.parent)
+ default_archive_name = archive_dest_path.name
+ default_archive_inner_dir = get_name_from_path(archive_dest_path.name)
# Log rotation in tmp directory
- if tmp_path == defualt_tmp_dir:
- __rotate_logs(tmp_path, f'*{archive_pattern}*')
+ if base_tmp_path == DEFAULT_TMP_DIR:
+ __rotate_logs(base_tmp_path, f'*{ARCHIVE_PATTERN}*')
# Temporary directory creation
- tmp_dir_path = f'{tmp_path}/drops-debug_{time_now}'
- tmp_dir: Path = Path(tmp_dir_path)
+ tmp_dir: Path = Path(f'{base_tmp_path}/{ARCHIVE_TMP_DIR_PATTERN}{time_now}')
tmp_dir.mkdir(parents=True)
- report_file: Path = Path(f'{tmp_dir_path}/show_tech-support_report.txt')
- report_file.touch()
+ # Directory which contains list of 'tech-support' reports
+ reports_dir: Path = Path(f'{tmp_dir}/show_tech-support_report')
+
+ # Call the topology snapshot function here
+ __generate_topology_snapshots(tmp_dir)
+
try:
+ # Generate files using `show tech-support report` command
+ __save_show_report_files(reports_dir)
- save_stdout(op('show tech-support report'), report_file)
# Generate included archives
- __generate_archived_files(tmp_dir_path)
+ __generate_archived_files(tmp_dir)
# Generate main archive
- __generate_main_archive_file(f'{tmp_path}/{archive_file_name}', tmp_dir_path)
- # Delete temporary directory
- rmtree(tmp_dir)
+ __generate_main_archive_file(archive_dest_path, tmp_dir)
+
# Upload to remote site if it is scpecified
- if remote:
- upload(f'{tmp_path}/{archive_file_name}', args.path)
- print(f'Debug file is generated and located in {location_path}/{archive_file_name}')
+ if is_remote:
+ upload_uri = args.path
+ upload(str(archive_dest_path), upload_uri)
except Exception as err:
print(f'Error during generating a debug file: {err}')
- # cleanup
- if tmp_dir.exists():
- rmtree(tmp_dir)
+ else:
+ print(f'Debug file is generated and located in {archive_dest_path}')
finally:
- # cleanup
+ # Delete temporary directory
+ if tmp_dir.exists():
+ rmtree(tmp_dir, ignore_errors=True)
exit()
diff --git a/src/op_mode/igmp-proxy.py b/src/op_mode/igmp-proxy.py
index 709e25915..d197e11f6 100755
--- a/src/op_mode/igmp-proxy.py
+++ b/src/op_mode/igmp-proxy.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/ikev2_profile_generator.py b/src/op_mode/ikev2_profile_generator.py
index cf2bc6d5c..0db9ef545 100755
--- a/src/op_mode/ikev2_profile_generator.py
+++ b/src/op_mode/ikev2_profile_generator.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/image_info.py b/src/op_mode/image_info.py
index 56aefcd6e..119960a6f 100755
--- a/src/op_mode/image_info.py
+++ b/src/op_mode/image_info.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# This file is part of VyOS.
#
@@ -72,6 +72,14 @@ def _format_show_images_details(
return tabulated
+def show_images_current(raw: bool) -> Union[image.BootDetails, str]:
+
+ images_summary = show_images_summary(raw=True)
+ if raw:
+ return {'image_running' : images_summary['image_running']}
+ else:
+ return images_summary['image_running']
+
def show_images_summary(raw: bool) -> Union[image.BootDetails, str]:
images_available: list[str] = grub.version_list()
diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py
index 9c17d0229..07f69ccd5 100755
--- a/src/op_mode/image_installer.py
+++ b/src/op_mode/image_installer.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright 2023-2025 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# This file is part of VyOS.
#
@@ -17,14 +17,23 @@
# You should have received a copy of the GNU General Public License along with
# VyOS. If not, see <https://www.gnu.org/licenses/>.
-from argparse import ArgumentParser, Namespace
+from argparse import ArgumentParser
+from argparse import Namespace
from pathlib import Path
-from shutil import copy, chown, rmtree, copytree
+from shutil import copy
+from shutil import chown
+from shutil import rmtree
+from shutil import copytree
+from shutil import disk_usage
from glob import glob
from sys import exit
from os import environ
from os import readlink
-from os import getpid, getppid
+from os import getpid
+from os import getppid
+from os import sync
+from json import loads
+from json import dumps
from typing import Union
from urllib.parse import urlparse
from passlib.hosts import linux_context
@@ -34,22 +43,42 @@ from psutil import disk_partitions
from vyos.base import Warning
from vyos.configtree import ConfigTree
+from vyos.config_mgmt import unsaved_commits
+from vyos.defaults import base_dir
+from vyos.defaults import directories
+from vyos.defaults import activation_hint
+from vyos.flavor import get_image_serial_console
from vyos.remote import download
-from vyos.system import disk, grub, image, compat, raid, SYSTEM_CFG_VER
+from vyos.system import disk
+from vyos.system import grub
+from vyos.system import image
+from vyos.system import compat
+from vyos.system import raid
+from vyos.system import SYSTEM_CFG_VER
+from vyos.system import grub_util
from vyos.template import render
-from vyos.utils.auth import (
- DEFAULT_PASSWORD,
- EPasswdStrength,
- evaluate_strength
-)
-from vyos.utils.io import ask_input, ask_yes_no, select_entry
+from vyos.utils.auth import DEFAULT_PASSWORD
+from vyos.utils.auth import EPasswdStrength
+from vyos.utils.auth import evaluate_strength
+from vyos.utils.auth import get_local_users
+from vyos.utils.auth import get_user_home_dir
+from vyos.utils.dict import dict_search
+from vyos.utils.io import ask_input
+from vyos.utils.io import ask_yes_no
+from vyos.utils.io import select_entry
from vyos.utils.file import chmod_2775
-from vyos.utils.process import cmd, run, rc_cmd
+from vyos.utils.file import read_file
+from vyos.utils.file import write_file
+from vyos.utils.process import cmd
+from vyos.utils.process import run
+from vyos.utils.process import rc_cmd
from vyos.version import get_version_data
# define text messages
MSG_ERR_NOT_LIVE: str = 'The system is already installed. Please use "add system image" instead.'
MSG_ERR_LIVE: str = 'The system is in live-boot mode. Please use "install image" instead.'
+MSG_ERR_NOT_ENOUGH_SPACE: str = 'Image upgrade requires at least 2GB of free drive space.'
+MSG_ERR_UNSAVED_COMMITS: str = 'There are unsaved changes to the configuration. Either save or revert before upgrade.'
MSG_ERR_NO_DISK: str = 'No suitable disk was found. There must be at least one disk of 2GB or greater size.'
MSG_ERR_IMPROPER_IMAGE: str = 'Missing sha256sum.txt.\nEither this image is corrupted, or of era 1.2.x (md5sum) and would downgrade image tools;\ndisallowed in either case.'
MSG_ERR_INCOMPATIBLE_IMAGE: str = 'Image compatibility check failed, aborting installation.'
@@ -73,7 +102,9 @@ MSG_INFO_INSTALL_PARTITONING: str = 'Creating partition table...'
MSG_INPUT_CONFIG_FOUND: str = 'An active configuration was found. Would you like to copy it to the new image?'
MSG_INPUT_CONFIG_CHOICE: str = 'The following config files are available for boot:'
MSG_INPUT_CONFIG_CHOOSE: str = 'Which file would you like as boot config?'
+MSG_INPUT_UNSAVED_COMMITS: str = 'There are unsaved changes to the configuration. They will not be copied to the new image. Continue without saving?'
MSG_INPUT_IMAGE_NAME: str = 'What would you like to name this image?'
+MSG_INPUT_IMAGE_NAME_TAKEN: str = 'There is already an installed image by that name; please choose again'
MSG_INPUT_IMAGE_DEFAULT: str = 'Would you like to set the new image as the default one for boot?'
MSG_INPUT_PASSWORD: str = 'Please enter a password for the "vyos" user:'
MSG_INPUT_PASSWORD_CONFIRM: str = 'Please confirm password for the "vyos" user:'
@@ -102,6 +133,8 @@ CONST_MIN_ROOT_SIZE: int = 1610612736 # 1.5 GB
CONST_RESERVED_SPACE: int = (2 + 1 + 256) * 1024**2
# define directories and paths
+DIR_CONFIG: str = directories['config']
+DIR_DATA: str = directories['data']
DIR_INSTALLATION: str = '/mnt/installation'
DIR_ROOTFS_SRC: str = f'{DIR_INSTALLATION}/root_src'
DIR_ROOTFS_DST: str = f'{DIR_INSTALLATION}/root_dst'
@@ -111,19 +144,20 @@ DIR_KERNEL_SRC: str = '/boot/'
FILE_ROOTFS_SRC: str = '/usr/lib/live/mount/medium/live/filesystem.squashfs'
ISO_DOWNLOAD_PATH: str = ''
-external_download_script = '/usr/libexec/vyos/simple-download.py'
-external_latest_image_url_script = '/usr/libexec/vyos/latest-image-url.py'
+external_download_script: str = f'{base_dir}/simple-download.py'
+external_latest_image_url_script: str = f'{base_dir}/latest-image-url.py'
+
+(flavor_sercon_type, flavor_sercon_num, flavor_sercon_speed) = get_image_serial_console()
# default boot variables
DEFAULT_BOOT_VARS: dict[str, str] = {
'timeout': '5',
'console_type': 'tty',
- 'console_num': '0',
- 'console_speed': '115200',
+ 'console_num': flavor_sercon_num,
+ 'console_speed': flavor_sercon_speed,
'bootmode': 'normal'
}
-
def bytes_to_gb(size: int) -> float:
"""Convert Bytes to GBytes, rounded to 1 decimal number
@@ -249,12 +283,18 @@ def search_previous_installation(disks: list[str]) -> None:
print('Searching for data from previous installations')
image_data = []
encrypted_configs = []
+ legacy_bind_mount = False
for disk_name in disks:
for partition in disk.partition_list(disk_name):
if disk.partition_mount(partition, mnt_tmp):
if Path(mnt_tmp + '/boot').exists():
for path in Path(mnt_tmp + '/boot').iterdir():
if path.joinpath('rw/config/.vyatta_config').exists():
+ legacy_bind_mount = True
+ image_data.append((path.name, partition))
+ elif path.joinpath(
+ 'rw/opt/vyatta/etc/config/.vyatta_config'
+ ).exists():
image_data.append((path.name, partition))
if Path(mnt_tmp + '/luks').exists():
for path in Path(mnt_tmp + '/luks').iterdir():
@@ -307,7 +347,12 @@ def search_previous_installation(disks: list[str]) -> None:
disk.partition_mount(image_drive, mnt_tmp)
if not encrypted:
- copytree(f'{mnt_tmp}/boot/{image_name}/rw/config', mnt_config)
+ if legacy_bind_mount:
+ copytree(f'{mnt_tmp}/boot/{image_name}/rw/config', mnt_config)
+ else:
+ copytree(
+ f'{mnt_tmp}/boot/{image_name}/rw/opt/vyatta/etc/config', mnt_config
+ )
else:
copy(f'{mnt_tmp}/luks/{image_name}', mnt_encrypted_config)
@@ -330,7 +375,7 @@ def copy_preserve_owner(src: str, dst: str, *, follow_symlinks=True):
def copy_previous_installation_data(target_dir: str) -> None:
if Path('/mnt/config').exists():
- copytree('/mnt/config', f'{target_dir}/opt/vyatta/etc/config',
+ copytree('/mnt/config', f'{target_dir}{DIR_CONFIG}',
dirs_exist_ok=True)
if Path('/mnt/ssh').exists():
copytree('/mnt/ssh', f'{target_dir}/etc/ssh',
@@ -476,6 +521,77 @@ def setup_grub(root_dir: str) -> None:
render(grub_cfg_menu, grub.TMPL_GRUB_MENU, {})
render(grub_cfg_options, grub.TMPL_GRUB_OPTS, {})
+def get_cli_kernel_options(config_file: str) -> list:
+ config = ConfigTree(read_file(config_file))
+ config_dict = loads(config.to_json())
+ cmdline_options = []
+ kernel_options = dict_search('system.option.kernel', config_dict)
+ if kernel_options is None:
+ return cmdline_options
+
+ k_cpu_opts = kernel_options.get('cpu', {})
+ k_memory_opts = kernel_options.get('memory', {})
+
+ # XXX: This code path and if statements must be kept in sync with the Kernel
+ # option handling in system_options.py:generate(). This occurrence is used
+ # for having the appropriate options passed to GRUB after an image upgrade!
+ if 'disable-mitigations' in kernel_options:
+ cmdline_options.append('mitigations=off')
+ if 'disable-power-saving' in kernel_options:
+ cmdline_options.append('intel_idle.max_cstate=0 processor.max_cstate=1')
+ if 'amd-pstate-driver' in kernel_options:
+ mode = kernel_options['amd-pstate-driver']
+ cmdline_options.append(
+ f'initcall_blacklist=acpi_cpufreq_init amd_pstate={mode}')
+ if 'quiet' in kernel_options:
+ cmdline_options.append('quiet')
+
+ # Early reboot on kernel panic via kernel cmdline (must match system_option.py)
+ if dict_search('system.option.reboot-on-panic', config_dict) is not None:
+ cmdline_options.append('panic=60')
+
+ if 'disable-hpet' in kernel_options:
+ cmdline_options.append('hpet=disable')
+
+ if 'disable-mce' in kernel_options:
+ cmdline_options.append('mce=off')
+
+ if 'disable-softlockup' in kernel_options:
+ cmdline_options.append('nosoftlockup')
+
+ # CPU options
+ isol_cpus = k_cpu_opts.get('isolate-cpus')
+ if isol_cpus:
+ cmdline_options.append(f'isolcpus={isol_cpus}')
+
+ nohz_full = k_cpu_opts.get('nohz-full')
+ if nohz_full:
+ cmdline_options.append(f'nohz_full={nohz_full}')
+
+ rcu_nocbs = k_cpu_opts.get('rcu-no-cbs')
+ if rcu_nocbs:
+ cmdline_options.append(f'rcu_nocbs={rcu_nocbs}')
+
+ if 'disable-nmi-watchdog' in k_cpu_opts:
+ cmdline_options.append('nmi_watchdog=0')
+
+ # Memory options
+ if 'disable-numa-balancing' in k_memory_opts:
+ cmdline_options.append('numa_balancing=disable')
+
+ default_hp_size = k_memory_opts.get('default-hugepage-size')
+ if default_hp_size:
+ cmdline_options.append(f'default_hugepagesz={default_hp_size}')
+
+ hp_sizes = k_memory_opts.get('hugepage-size')
+ if hp_sizes:
+ for size, settings in hp_sizes.items():
+ cmdline_options.append(f'hugepagesz={size}')
+ count = settings.get('hugepage-count')
+ if count:
+ cmdline_options.append(f'hugepages={count}')
+
+ return cmdline_options
def configure_authentication(config_file: str, password: str) -> None:
"""Write encrypted password to config file
@@ -490,10 +606,7 @@ def configure_authentication(config_file: str, password: str) -> None:
plaintext exposed
"""
encrypted_password = linux_context.hash(password)
-
- with open(config_file) as f:
- config_string = f.read()
-
+ config_string = read_file(config_file)
config = ConfigTree(config_string)
config.set([
'system', 'login', 'user', 'vyos', 'authentication',
@@ -506,6 +619,49 @@ def configure_authentication(config_file: str, password: str) -> None:
with open(config_file, 'w') as f:
f.write(config.to_string())
+def configure_serial_console(config_file: str, console_type: str) -> None:
+ """Apply serial console settings to config.boot from kernel cmdline.
+
+ This overlaps with 05-serial_console.py activation logic, but that script
+ only runs during live boot. During installation, the user may pick a
+ different source config, so serial console settings must be written to
+ the final target config explicitly.
+
+ Behavior:
+ - Reads the kernel serial console device/speed from the current boot cmdline.
+ - If the detected device is a valid tty, writes:
+ system console device <TTY> speed <rate>
+ - If "console_type == 'S'", also writes:
+ system console device <TTY> kernel
+
+ Args:
+ config_file (str): path of target config file
+ console_type (str): 'K' (KVM/tty) or 'S' (serial)
+ """
+ from vyos.utils.serial import is_tty
+ from vyos.utils.kernel import get_kernel_serial_console
+
+ # Parse current kernel cmdline and continue only for valid serial console
+ # data. Prevent writing incomplete/invalid console settings to config.boot.
+ k_console_type, k_console_num, k_console_speed = get_kernel_serial_console()
+ device = f'{k_console_type}{k_console_num}'
+ if not is_tty(device) or not k_console_speed:
+ return
+
+ base = ['system', 'console', 'device']
+ config_string = read_file(config_file)
+ config = ConfigTree(config_string)
+ config.set(base + [device, 'speed'], value=k_console_speed)
+ config.set_tag(base)
+
+ # Only mark this device as kernel boot console when console_type 'S' for
+ # serial was defined by user.
+ if console_type == 'S':
+ config.set(base + [device, 'kernel'])
+
+ with open(config_file, 'w') as f:
+ f.write(config.to_string())
+
def validate_signature(file_path: str, sign_type: str) -> None:
"""Validate a file by signature and delete a signature file
@@ -534,21 +690,18 @@ def validate_signature(file_path: str, sign_type: str) -> None:
print('Signature is valid')
def download_file(local_file: str, remote_path: str, vrf: str,
- username: str, password: str,
progressbar: bool = False, check_space: bool = False):
- environ['REMOTE_USERNAME'] = username
- environ['REMOTE_PASSWORD'] = password
+ # Server credentials are implicitly passed in environment variables
+ # that are set by add_image
if vrf is None:
download(local_file, remote_path, progressbar=progressbar,
check_space=check_space, raise_error=True)
else:
- remote_auth = f'REMOTE_USERNAME={username} REMOTE_PASSWORD={password}'
vrf_cmd = f'ip vrf exec {vrf} {external_download_script} \
--local-file {local_file} --remote-path {remote_path}'
- cmd(vrf_cmd, auth=remote_auth)
+ cmd(vrf_cmd, env=environ)
def image_fetch(image_path: str, vrf: str = None,
- username: str = '', password: str = '',
no_prompt: bool = False) -> Path:
"""Fetch an ISO image
@@ -567,9 +720,8 @@ def image_fetch(image_path: str, vrf: str = None,
if image_path == 'latest':
command = external_latest_image_url_script
if vrf:
- command = f'REMOTE_USERNAME={username} REMOTE_PASSWORD={password} \
- ip vrf exec {vrf} ' + command
- code, output = rc_cmd(command)
+ command = f'ip vrf exec {vrf} {command}'
+ code, output = rc_cmd(command, env=environ)
if code:
print(output)
exit(MSG_INFO_INSTALL_EXIT)
@@ -581,7 +733,6 @@ def image_fetch(image_path: str, vrf: str = None,
# Download the image file
ISO_DOWNLOAD_PATH = os.path.join(os.path.expanduser("~"), '{0}.iso'.format(uuid4()))
download_file(ISO_DOWNLOAD_PATH, image_path, vrf,
- username, password,
progressbar=True, check_space=True)
# Download the image signature
@@ -592,8 +743,7 @@ def image_fetch(image_path: str, vrf: str = None,
for sign_type in ['minisig']:
try:
download_file(f'{ISO_DOWNLOAD_PATH}.{sign_type}',
- f'{image_path}.{sign_type}', vrf,
- username, password)
+ f'{image_path}.{sign_type}', vrf)
sign_file = (True, sign_type)
break
except Exception:
@@ -625,7 +775,7 @@ def migrate_config() -> bool:
Returns:
bool: user's decision
"""
- active_config_path: Path = Path('/opt/vyatta/etc/config/config.boot')
+ active_config_path: Path = Path(f'{DIR_CONFIG}/config.boot')
if active_config_path.exists():
if ask_yes_no(MSG_INPUT_CONFIG_FOUND, default=True):
return True
@@ -643,6 +793,20 @@ def copy_ssh_host_keys() -> bool:
return False
+def copy_ssh_known_hosts() -> bool:
+ """Ask user to copy SSH `known_hosts` files
+
+ Returns:
+ bool: user's decision
+ """
+ known_hosts_files = get_known_hosts_files()
+ msg = (
+ 'Would you like to save the SSH known hosts (fingerprints) '
+ 'from your current configuration?'
+ )
+ return known_hosts_files and ask_yes_no(msg, default=True)
+
+
def console_hint() -> str:
pid = getppid() if 'SUDO_USER' in environ else getpid()
try:
@@ -651,12 +815,11 @@ def console_hint() -> str:
path = '/dev/tty'
name = Path(path).name
- if name == 'ttyS0':
+ if name.startswith(('ttyS', 'ttyAMA')):
return 'S'
else:
return 'K'
-
def cleanup(mounts: list[str] = [], remove_items: list[str] = []) -> None:
"""Clean up after installation
@@ -805,12 +968,12 @@ def install_image() -> None:
print(MSG_WARN_PASSWORD_CONFIRM)
# ask for default console
+ console_dict: dict[str, str] = {'K': 'tty', 'S': flavor_sercon_type}
console_type: str = ask_input(MSG_INPUT_CONSOLE_TYPE,
default=console_hint(),
- valid_responses=['K', 'S'])
- console_dict: dict[str, str] = {'K': 'tty', 'S': 'ttyS'}
+ valid_responses=console_dict.keys())
- config_boot_list = ['/opt/vyatta/etc/config/config.boot',
+ config_boot_list = [f'{DIR_CONFIG}/config.boot',
'/opt/vyatta/etc/config.boot.default']
default_config = config_boot_list[0]
@@ -843,10 +1006,10 @@ def install_image() -> None:
Path(f'{DIR_DST_ROOT}/boot/efi').mkdir(parents=True)
disk.partition_mount(install_target.partition['efi'], f'{DIR_DST_ROOT}/boot/efi')
- # a config dir. It is the deepest one, so the comand will
+ # a config dir. It is the deepest one, so the command will
# create all the rest in a single step
print('Creating a configuration file')
- target_config_dir: str = f'{DIR_DST_ROOT}/boot/{image_name}/rw/opt/vyatta/etc/config/'
+ target_config_dir: str = f'{DIR_DST_ROOT}/boot/{image_name}/rw{DIR_CONFIG}/'
Path(target_config_dir).mkdir(parents=True)
chown(target_config_dir, group='vyattacfg')
chmod_2775(target_config_dir)
@@ -854,6 +1017,8 @@ def install_image() -> None:
copy(default_config, f'{target_config_dir}/config.boot')
configure_authentication(f'{target_config_dir}/config.boot',
user_password)
+ configure_serial_console(f'{target_config_dir}/config.boot',
+ console_type)
Path(f'{target_config_dir}/.vyatta_config').touch()
# create a persistence.conf
@@ -878,6 +1043,14 @@ def install_image() -> None:
write_dir: str = f'{DIR_DST_ROOT}/boot/{image_name}/rw'
raid.update_default(write_dir)
+ # set activation hint
+ target_data_dir: str = f'{DIR_DST_ROOT}/boot/{image_name}/rw{DIR_DATA}/'
+ data_path = Path(target_data_dir)
+ data_path.mkdir(parents=True)
+ data_path.chmod(0o755)
+ init_hint = data_path.joinpath(Path(activation_hint).name)
+ init_hint.touch()
+
setup_grub(DIR_DST_ROOT)
# add information about version
grub.create_structure()
@@ -897,8 +1070,7 @@ def install_image() -> None:
for disk_target in l:
disk.partition_mount(disk_target.partition['efi'], f'{DIR_DST_ROOT}/boot/efi')
grub.install(disk_target.name, f'{DIR_DST_ROOT}/boot/',
- f'{DIR_DST_ROOT}/boot/efi',
- id=f'VyOS (RAID disk {l.index(disk_target) + 1})')
+ f'{DIR_DST_ROOT}/boot/efi')
disk.partition_umount(disk_target.partition['efi'])
else:
print('Installing GRUB to the drive')
@@ -924,7 +1096,7 @@ def install_image() -> None:
except Exception as err:
print(f'Unable to install VyOS: {err}')
- # unmount filesystems and clenup
+ # unmount filesystems and cleanup
try:
if install_target is not None:
if is_raid_install(install_target):
@@ -939,6 +1111,55 @@ def install_image() -> None:
exit(1)
+def get_known_hosts_files(for_root=True, for_users=True) -> list:
+ """Collect all existing `known_hosts` files for root and/or users under /home"""
+
+ files = []
+
+ if for_root:
+ base_files = ('/root/.ssh/known_hosts', '/etc/ssh/ssh_known_hosts')
+ for file_path in base_files:
+ root_known_hosts = Path(file_path)
+ if root_known_hosts.exists():
+ files.append(root_known_hosts)
+
+ if for_users: # for each non-system user
+ for user in get_local_users():
+ home_dir = Path(get_user_home_dir(user))
+ if home_dir.exists():
+ known_hosts = home_dir / '.ssh' / 'known_hosts'
+ if known_hosts.exists():
+ files.append(known_hosts)
+
+ return files
+
+
+def migrate_known_hosts(target_dir: str):
+ """Copy `known_hosts` for root and all users to the new image directory"""
+
+ def _mkdir_and_copy_file(known_hosts_file, target_known_hosts):
+ target_known_hosts.parent.mkdir(parents=True, exist_ok=True)
+ copy(known_hosts_file, target_known_hosts)
+
+ # Copy root only files using default path
+ known_hosts_files = get_known_hosts_files(for_root=True, for_users=False)
+ for known_hosts_file in known_hosts_files:
+ target_known_hosts = Path(f'{target_dir}{known_hosts_file}')
+ _mkdir_and_copy_file(known_hosts_file, target_known_hosts)
+
+ # During image installation, backup critical user-specific files (e.g., known_hosts)
+ # from each user's home directory into /var/.users_backups/{user}. This ensures that their
+ # SSH configuration and trust relationships are preserved across system re-installations
+ # or provisioning.
+ # More details: https://github.com/vyos/vyos-1x/pull/4678#pullrequestreview-3169648265
+ known_hosts_files = get_known_hosts_files(for_root=False, for_users=True)
+ for known_hosts_file in known_hosts_files:
+ username = known_hosts_file.parent.parent.name
+ base_dir = Path(f'{target_dir}/var/.users_backups/{username}')
+ target_known_hosts = base_dir / '.ssh' / 'known_hosts'
+ _mkdir_and_copy_file(known_hosts_file, target_known_hosts)
+
+
@compat.grub_cfg_update
def add_image(image_path: str, vrf: str = None, username: str = '',
password: str = '', no_prompt: bool = False, force: bool = False) -> None:
@@ -950,8 +1171,26 @@ def add_image(image_path: str, vrf: str = None, username: str = '',
if image.is_live_boot():
exit(MSG_ERR_LIVE)
+ # Trying to upgrade with insufficient space can break the system.
+ # It's better to be on the safe side:
+ # our images are a bit below 1G,
+ # so one gigabyte to download the image plus one more to install it
+ # sounds like a sensible estimate.
+ if disk_usage('/').free < (2 * 1024**3):
+ exit(MSG_ERR_NOT_ENOUGH_SPACE)
+
+ if unsaved_commits():
+ if not no_prompt:
+ if not ask_yes_no(MSG_INPUT_UNSAVED_COMMITS, default=False):
+ exit()
+ else:
+ exit(MSG_ERR_UNSAVED_COMMITS)
+
+ environ['REMOTE_USERNAME'] = username
+ environ['REMOTE_PASSWORD'] = password
+
# fetch an image
- iso_path: Path = image_fetch(image_path, vrf, username, password, no_prompt)
+ iso_path: Path = image_fetch(image_path, vrf, no_prompt)
try:
# mount an ISO
Path(DIR_ISO_MOUNT).mkdir(mode=0o755, parents=True)
@@ -984,8 +1223,12 @@ def add_image(image_path: str, vrf: str = None, username: str = '',
f'Adding image would downgrade image tools to v.{cfg_ver}; disallowed')
if not no_prompt:
+ versions = grub.version_list()
while True:
image_name: str = ask_input(MSG_INPUT_IMAGE_NAME, version_name)
+ if image_name in versions:
+ print(MSG_INPUT_IMAGE_NAME_TAKEN)
+ continue
if image.validate_name(image_name):
break
print(MSG_WARN_IMAGE_NAME_WRONG)
@@ -997,18 +1240,45 @@ def add_image(image_path: str, vrf: str = None, username: str = '',
# find target directory
root_dir: str = disk.find_persistence()
- # a config dir. It is the deepest one, so the comand will
+ cmdline_options = []
+
+ # a config dir. It is the deepest one, so the command will
# create all the rest in a single step
- target_config_dir: str = f'{root_dir}/boot/{image_name}/rw/opt/vyatta/etc/config/'
+ target_config_dir: str = f'{root_dir}/boot/{image_name}/rw{DIR_CONFIG}/'
# copy config
if no_prompt or migrate_config():
- print('Copying configuration directory')
- # copytree preserves perms but not ownership:
- Path(target_config_dir).mkdir(parents=True)
- chown(target_config_dir, group='vyattacfg')
- chmod_2775(target_config_dir)
- copytree('/opt/vyatta/etc/config/', target_config_dir, symlinks=True,
- copy_function=copy_preserve_owner, dirs_exist_ok=True)
+ if Path('/dev/mapper/vyos_config').exists():
+ print('Copying encrypted configuration volume')
+
+ # Record information from which image we upgraded to the new one.
+ # This can be used for a future automatic rollback into the old image.
+ #
+ # For encrypted config, we need to copy, sync filesystems and remove from current image
+ tmp = {'previous_image' : image.get_running_image()}
+ write_file('/opt/vyatta/etc/config/first_boot', dumps(tmp))
+ sync()
+
+ # Copy encrypted volumes
+ current_name = image.get_running_image()
+ current_config_path = f'{root_dir}/luks/{current_name}'
+ target_config_path = f'{root_dir}/luks/{image_name}'
+ copy(current_config_path, target_config_path)
+
+ # Now remove from current image
+ Path('/opt/vyatta/etc/config/first_boot').unlink()
+ else:
+ print('Copying configuration directory')
+ # copytree preserves perms but not ownership:
+ Path(target_config_dir).mkdir(parents=True)
+ chown(target_config_dir, group='vyattacfg')
+ chmod_2775(target_config_dir)
+ copytree(f'{DIR_CONFIG}/', target_config_dir, symlinks=True,
+ copy_function=copy_preserve_owner, dirs_exist_ok=True)
+
+ # Record information from which image we upgraded to the new one.
+ # This can be used for a future automatic rollback into the old image.
+ tmp = {'previous_image' : image.get_running_image()}
+ write_file(f'{target_config_dir}/first_boot', dumps(tmp))
else:
Path(target_config_dir).mkdir(parents=True)
chown(target_config_dir, group='vyattacfg')
@@ -1023,6 +1293,11 @@ def add_image(image_path: str, vrf: str = None, username: str = '',
for host_key in host_keys:
copy(host_key, target_ssh_dir)
+ target_ssh_known_hosts_dir: str = f'{root_dir}/boot/{image_name}/rw'
+ if no_prompt or copy_ssh_known_hosts():
+ print('Copying SSH known_hosts files')
+ migrate_known_hosts(target_ssh_known_hosts_dir)
+
# copy system image and kernel files
print('Copying system image files')
for file in Path(f'{DIR_ISO_MOUNT}/live').iterdir():
@@ -1040,6 +1315,13 @@ def add_image(image_path: str, vrf: str = None, username: str = '',
if set_as_default:
grub.set_default(image_name, root_dir)
+ if Path(f'{target_config_dir}/config.boot').exists():
+ cmdline_options = get_cli_kernel_options(
+ f'{target_config_dir}/config.boot')
+ grub_util.update_kernel_cmdline_options(' '.join(cmdline_options),
+ root_dir=root_dir,
+ version=image_name)
+
except OSError as e:
# if no space error, remove image dir and cleanup
if e.errno == ENOSPC:
diff --git a/src/op_mode/image_manager.py b/src/op_mode/image_manager.py
index fb4286dbc..985db96dd 100755
--- a/src/op_mode/image_manager.py
+++ b/src/op_mode/image_manager.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# This file is part of VyOS.
#
@@ -33,7 +33,7 @@ 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']
+ConsoleType: TypeAlias = Literal['tty', 'ttyS', 'ttyAMA']
def annotate_list(images_list: list[str]) -> list[str]:
"""Annotate list of images with additional info
diff --git a/src/op_mode/install_mok.sh b/src/op_mode/install_mok.sh
new file mode 100755
index 000000000..29f78cd1f
--- /dev/null
+++ b/src/op_mode/install_mok.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+if test -f /var/lib/shim-signed/mok/vyos-dev-2025-shim.der; then
+ mokutil --ignore-keyring --import /var/lib/shim-signed/mok/vyos-dev-2025-shim.der;
+else
+ echo "Secure Boot Machine Owner Key not found";
+fi
diff --git a/src/op_mode/interfaces.py b/src/op_mode/interfaces.py
index e7afc4caa..ecc4e73d2 100755
--- a/src/op_mode/interfaces.py
+++ b/src/op_mode/interfaces.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022-2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -13,14 +13,13 @@
#
# 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 re
import sys
import glob
import json
import typing
+import textwrap
from datetime import datetime
from tabulate import tabulate
@@ -28,18 +27,14 @@ import vyos.opmode
from vyos.ifconfig import Section
from vyos.ifconfig import Interface
from vyos.ifconfig import VRRP
+from vyos.utils.dict import dict_set_nested
+from vyos.utils.io import catch_broken_pipe
+from vyos.utils.network import get_interface_vrf
+from vyos.utils.network import interface_exists
from vyos.utils.process import cmd
from vyos.utils.process import rc_cmd
from vyos.utils.process import call
-
-def catch_broken_pipe(func):
- def wrapped(*args, **kwargs):
- try:
- func(*args, **kwargs)
- except (BrokenPipeError, KeyboardInterrupt):
- # Flush output to /dev/null and bail out.
- os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())
- return wrapped
+from vyos.configquery import op_mode_config_dict
# The original implementation of filtered_interfaces has signature:
# (ifnames: list, iftypes: typing.Union[str, list], vif: bool, vrrp: bool) -> intf: Interface:
@@ -84,6 +79,18 @@ def filtered_interfaces(ifnames: typing.Union[str, list],
yield interface
+def is_interface_has_mac(interface_name):
+ interface_no_mac = ('tun', 'wg')
+ return not any(interface_name.startswith(prefix) for prefix in interface_no_mac)
+
+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 _split_text(text, used=0):
"""
take a string and attempt to split it to fit with the width of the screen
@@ -109,6 +116,7 @@ def _split_text(text, used=0):
continue
if line:
yield line[1:]
+ line = f' {word}'
else:
line = f'{line} {word}'
@@ -236,10 +244,6 @@ def _get_summary_data(ifname: typing.Optional[str],
iftype = ''
ret = []
- def is_interface_has_mac(interface_name):
- interface_no_mac = ('tun', 'wg')
- return not any(interface_name.startswith(prefix) for prefix in interface_no_mac)
-
for interface in filtered_interfaces(ifname, iftype, vif, vrrp):
res_intf = {}
@@ -296,6 +300,140 @@ def _get_counter_data(ifname: typing.Optional[str],
return ret
+def _get_kernel_data(raw, ifname = None, detail = False,
+ statistics = False):
+ if ifname:
+ # Check if the interface exists
+ if not interface_exists(ifname):
+ raise vyos.opmode.IncorrectValue(f"{ifname} does not exist!")
+ int_name = f'dev {ifname}'
+ else:
+ int_name = ''
+
+ kernel_interface = json.loads(cmd(f'ip -j -d -s address show {int_name}'))
+
+ # Return early if raw
+ if raw:
+ return kernel_interface, None
+
+ # Format the kernel data
+ kernel_interface_out = _format_kernel_data(kernel_interface, detail, statistics)
+
+ return kernel_interface, kernel_interface_out
+
+def _format_kernel_data(data, detail, statistics):
+ output_list = []
+ podman_vrf = {}
+ tmpInfo = {}
+
+ # Sort interfaces by name
+ for interface in sorted(data, key=lambda x: x.get('ifname', '')):
+ interface_name = interface.get('ifname', '')
+
+ # Skip VRF interfaces
+ if interface.get('linkinfo', {}).get('info_kind') == 'vrf':
+ continue
+ # Skip spawned interfaces
+ elif interface_name.startswith(('tunl', 'gre', 'erspan', 'pim6reg')):
+ continue
+
+ master = interface.get('master', 'default')
+ vrf = get_interface_vrf(interface)
+
+ # Get the device model; ex. Intel Corporation Ethernet Controller I225-V
+ dev_model = interface.get('parentdev', '')
+ if 'parentdev' in interface:
+ parentdev = interface['parentdev']
+ if re.match(r'^[0-9a-fA-F]{4}:', parentdev):
+ dev_model = cmd(f'lspci -nn -s {parentdev}').split(']:')[1].strip()
+
+ # Get the IP addresses on interface
+ ip_list = []
+ has_global = False
+
+ for ip in interface['addr_info']:
+ if ip.get('scope') in ('global', 'host'):
+ has_global = True
+ local = ip.get('local', '-')
+ prefixlen = ip.get('prefixlen', '')
+ ip_list.append(f"{local}/{prefixlen}")
+
+ # If no global IP address, add '-'; indicates no IP address on interface
+ if not has_global:
+ ip_list.append('-')
+
+ # Generate a mapping of podman interfaces to their VRF
+ if interface_name.startswith('pod-'):
+ dict_set_nested(f'{interface_name}.vrf', master, podman_vrf)
+
+ # If the veth interface's master is a podman interface, the VRF is the VRF of the podman interface
+ if master.startswith('pod-'):
+ vrf = podman_vrf.get(master).get('vrf', 'default')
+
+ rx_stats = interface.get('stats64', {}).get('rx')
+ tx_stats = interface.get('stats64', {}).get('tx')
+
+ sl_status = ('A' if not 'UP' in interface['flags'] else 'u') + '/' + ('D' if interface['operstate'] == 'DOWN' else 'u')
+
+ # Generate temporary dict to hold data
+ tmpInfo['ifname'] = interface_name
+ tmpInfo['ip'] = ip_list
+ tmpInfo['mac'] = interface.get('address', 'n/a') if is_interface_has_mac(interface_name) else 'n/a'
+ tmpInfo['mtu'] = interface.get('mtu', '')
+ tmpInfo['vrf'] = vrf
+ tmpInfo['status'] = sl_status
+ tmpInfo['description'] = "\n".join(textwrap.wrap(interface.get('ifalias', ''), width=50))
+ tmpInfo['device'] = dev_model
+ tmpInfo['alternate_names'] = interface.get('altnames', '')
+ tmpInfo['minimum_mtu'] = interface.get('min_mtu', '')
+ tmpInfo['maximum_mtu'] = interface.get('max_mtu', '')
+ tmpInfo['rx_packets'] = rx_stats.get('packets', "")
+ tmpInfo['rx_bytes'] = rx_stats.get('bytes', "")
+ tmpInfo['rx_errors'] = rx_stats.get('errors', "")
+ tmpInfo['rx_dropped'] = rx_stats.get('dropped', "")
+ tmpInfo['rx_over_errors'] = rx_stats.get('over_errors', '')
+ tmpInfo['multicast'] = rx_stats.get('multicast', "")
+ tmpInfo['tx_packets'] = tx_stats.get('packets', "")
+ tmpInfo['tx_bytes'] = tx_stats.get('bytes', "")
+ tmpInfo['tx_errors'] = tx_stats.get('errors', "")
+ tmpInfo['tx_dropped'] = tx_stats.get('dropped', "")
+ tmpInfo['tx_carrier_errors'] = tx_stats.get('carrier_errors', "")
+ tmpInfo['tx_collisions'] = tx_stats.get('collisions', "")
+
+ # Order the stats based on 'detail' or 'statistics'
+ if detail:
+ stat_keys = [
+ "rx_packets", "rx_bytes", "rx_errors", "rx_dropped",
+ "rx_over_errors", "multicast",
+ "tx_packets", "tx_bytes", "tx_errors", "tx_dropped",
+ "tx_carrier_errors", "tx_collisions",
+ ]
+ elif statistics:
+ stat_keys = [
+ "rx_packets", "rx_bytes", "tx_packets", "tx_bytes",
+ "rx_dropped", "tx_dropped", "rx_errors", "tx_errors",
+ ]
+ else:
+ stat_keys = []
+
+ stat_list = [tmpInfo.get(k, "") for k in stat_keys]
+
+ # Generate output list; detail adds more fields
+ output_list.append([tmpInfo['ifname'],
+ *(['\n'.join(tmpInfo['ip'])] if not statistics else []),
+ *([tmpInfo['mac']] if not statistics else []),
+ *([tmpInfo['vrf']] if not statistics else []),
+ *([tmpInfo['mtu']] if not statistics else []),
+ *([tmpInfo['status']] if not statistics else []),
+ *([tmpInfo['description']] if not statistics else []),
+ *([tmpInfo['device']] if detail else []),
+ *(['\n'.join(tmpInfo['alternate_names'])] if detail else []),
+ *([tmpInfo['minimum_mtu']] if detail else []),
+ *([tmpInfo['maximum_mtu']] if detail else []),
+ *(stat_list if any([detail, statistics]) else [])])
+
+ return output_list
+
@catch_broken_pipe
def _format_show_data(data: list):
unhandled = []
@@ -445,6 +583,34 @@ def _format_show_counters(data: list):
print (output)
return output
+def show_kernel(raw: bool, intf_name: typing.Optional[str],
+ detail: bool, statistics: bool):
+ raw_data, data = _get_kernel_data(raw, intf_name, detail, statistics)
+
+ # Return early if raw
+ if raw:
+ return raw_data
+
+ if detail:
+ # Detail headers; ex. show interfaces kernel detail; show interfaces kernel eth0 detail
+ detail_header = ['Interface', 'IP Address', 'MAC', 'VRF', 'MTU', 'S/L', 'Description',
+ 'Device', 'Alternate Names','Minimum MTU', 'Maximum MTU', 'RX_Packets',
+ 'RX_Bytes', 'RX_Errors', 'RX_Dropped', 'Receive Overrun Errors', 'Received Multicast',
+ 'TX_Packets', 'TX_Bytes', 'TX_Errors', 'TX_Dropped', 'Transmit Carrier Errors',
+ 'Transmit Collisions']
+ elif statistics:
+ # Statistics headers; ex. show interfaces kernel statistics; show interfaces kernel eth0 statistics
+ headers = ['Interface', 'Rx Packets', 'Rx Bytes', 'Tx Packets', 'Tx Bytes', 'Rx Dropped', 'Tx Dropped', 'Rx Errors', 'Tx Errors']
+ else:
+ # Normal headers; ex. show interfaces kernel; show interfaces kernel eth0
+ print('Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down')
+ headers = ['Interface', 'IP Address', 'MAC', 'VRF', 'MTU', 'S/L', 'Description']
+
+
+ if detail:
+ detailed_output(data, detail_header)
+ else:
+ print(tabulate(data, headers))
def _show_raw(data: list, intf_name: str):
if intf_name is not None and len(data) <= 1:
@@ -489,6 +655,86 @@ def show_counters(raw: bool, intf_name: typing.Optional[str],
return _show_raw(data, intf_name)
return _format_show_counters(data)
+def show_vlan_to_vni(raw: bool, intf_name: typing.Optional[str],
+ vid: typing.Optional[str], detail: bool,
+ statistics: bool):
+ if not interface_exists(intf_name):
+ raise vyos.opmode.UnconfiguredObject(f"Interface {intf_name} does not exist\n")
+
+ if not vid:
+ vid = "all"
+
+ tunnel_data = json.loads(cmd(f"bridge -j vlan tunnelshow dev {intf_name} vid {vid}"))
+
+ if not tunnel_data:
+ if vid == "all":
+ raise vyos.opmode.UnconfiguredObject(f"No VLAN-to-VNI mapping found for interface {intf_name}\n")
+ else:
+ raise vyos.opmode.UnconfiguredObject(f"No VLAN-to-VNI mapping found for VLAN {vid}\n")
+
+ statistics_data = json.loads(cmd(f"bridge -j -s vlan tunnelshow dev {intf_name} vid {vid}"))[0]
+
+ mapping_config = op_mode_config_dict(['interfaces', 'vxlan', intf_name, 'vlan-to-vni'],
+ get_first_key=True)
+
+ raw_data = {intf_name: {}}
+ output_list = []
+
+ for tunnel in tunnel_data[0].get("tunnels", []):
+ tunnel_id = tunnel.get("tunid")
+ tunnel_dict = raw_data[intf_name][tunnel_id] = {}
+
+ for vlan in statistics_data.get("vlans", []):
+ if vlan.get("vid") == tunnel.get("vlan"):
+ vlan_id = str(vlan.get("vid"))
+ description = mapping_config.get(vlan_id, {}).get("description", "")
+
+ # detail allows for longer descriptions; each output wraps to 80 characters
+ if detail:
+ description = "\n".join(textwrap.wrap(description, width=65))
+ elif raw:
+ pass
+ else:
+ description = "\n".join(textwrap.wrap(description, width=48))
+
+ if raw:
+ tunnel_dict["vlan"] = vlan_id
+ tunnel_dict["rx_bytes"] = vlan.get("rx_bytes")
+ tunnel_dict["tx_bytes"] = vlan.get("tx_bytes")
+ tunnel_dict["rx_packets"] = vlan.get("rx_packets")
+ tunnel_dict["tx_packets"] = vlan.get("tx_packets")
+ tunnel_dict["description"] = description
+ else:
+ #Generate output list; detail adds more fields
+ output_list.append([
+ *([intf_name] if not detail else []),
+ vlan_id,
+ tunnel_id,
+ *([description] if not statistics else []),
+ *([vlan.get("rx_packets")] if any([detail, statistics]) else []),
+ *([vlan.get("rx_bytes")] if any([detail, statistics]) else []),
+ *([vlan.get("tx_packets")] if any([detail, statistics]) else []),
+ *([vlan.get("tx_bytes")] if any([detail, statistics]) else [])
+ ])
+
+ if raw:
+ return raw_data
+
+ if detail:
+ # Detail headers; ex. show interfaces vxlan vxlan1 vlan-to-vni detail
+ detail_header = ['VLAN', 'VNI', 'Description', 'Rx Packets', 'Rx Bytes', 'Tx Packets', 'Tx Bytes']
+ print('-' * 35)
+ print(f"Interface: {intf_name}\n")
+ detailed_output(output_list, detail_header)
+ elif statistics:
+ # Statistics headers; ex. show interfaces vxlan vxlan1 vlan-to-vni statistics
+ headers = ['Interface', 'VLAN', 'VNI', 'Rx Packets', 'Rx Bytes', 'Tx Packets', 'Tx Bytes']
+ print(tabulate(output_list, headers))
+ else:
+ # Normal headers; ex. show interfaces vxlan vxlan1 vlan-to-vni
+ headers = ['Interface', 'VLAN', 'VNI', 'Description']
+ print(tabulate(output_list, headers))
+
def clear_counters(intf_name: typing.Optional[str],
intf_type: typing.Optional[str],
vif: bool, vrrp: bool):
diff --git a/src/op_mode/interfaces_wireguard.py b/src/op_mode/interfaces_wireguard.py
index 627af0579..b600bd3a4 100644
--- a/src/op_mode/interfaces_wireguard.py
+++ b/src/op_mode/interfaces_wireguard.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -18,31 +18,13 @@ import sys
import vyos.opmode
from vyos.ifconfig import WireGuardIf
-from vyos.configquery import ConfigTreeQuery
-
-def _verify(func):
- """Decorator checks if WireGuard interface config exists"""
- from functools import wraps
-
- @wraps(func)
- def _wrapper(*args, **kwargs):
- config = ConfigTreeQuery()
- interface = kwargs.get('intf_name')
- if not config.exists(['interfaces', 'wireguard', interface]):
- unconf_message = f'WireGuard interface {interface} is not configured'
- raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
- return func(*args, **kwargs)
-
- return _wrapper
-
-
-@_verify
+@vyos.opmode.verify_cli_exists(['interfaces', 'wireguard'],
+ 'WireGuard interface {interface} is not configured!')
def show_summary(raw: bool, intf_name: str):
intf = WireGuardIf(intf_name, create=False, debug=False)
return intf.operational.show_interface()
-
if __name__ == '__main__':
try:
res = vyos.opmode.run(sys.modules[__name__])
diff --git a/src/op_mode/interfaces_wireless.py b/src/op_mode/interfaces_wireless.py
index bf6e462f3..d1070e95f 100755
--- a/src/op_mode/interfaces_wireless.py
+++ b/src/op_mode/interfaces_wireless.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -23,18 +23,9 @@ from tabulate import tabulate
from vyos.utils.process import popen
from vyos.configquery import ConfigTreeQuery
-def _verify(func):
- """Decorator checks if Wireless LAN config exists"""
- from functools import wraps
-
- @wraps(func)
- def _wrapper(*args, **kwargs):
- config = ConfigTreeQuery()
- if not config.exists(['interfaces', 'wireless']):
- unconf_message = 'No Wireless interfaces configured'
- raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
- return func(*args, **kwargs)
- return _wrapper
+verify_path = ['interfaces', 'wireless']
+verify_error = 'Wireless/WiFi subsystem unconfigured!'
+verify_interface_error = 'Wireless interface {interface} is not configured!'
def _get_raw_info_data():
output_data = []
@@ -93,7 +84,7 @@ def _get_raw_scan_data(intf_name):
ssid['ssid'] = line.lstrip().split(':')[-1].lstrip()
elif line.lstrip().startswith('signal: '):
- # Siganl can be " signal: -67.00 dBm", thus strip all leading whitespaces
+ # Signal can be " signal: -67.00 dBm", thus strip all leading whitespaces
ssid['signal'] = line.lstrip().split(':')[-1].split()[0]
elif line.lstrip().startswith('DS Parameter set: channel'):
@@ -156,7 +147,7 @@ def _format_station_data(raw_data):
headers = ["Station", "Signal", "RX bytes", "RX packets", "TX bytes", "TX packets"]
return tabulate(output, headers, numalign="left")
-@_verify
+@vyos.opmode.verify_cli_exists(verify_path, verify_error)
def show_info(raw: bool):
info_data = _get_raw_info_data()
if raw:
@@ -169,7 +160,7 @@ def show_scan(raw: bool, intf_name: str):
return data
return _format_scan_data(data)
-@_verify
+@vyos.opmode.verify_cli_exists(verify_path, verify_interface_error)
def show_stations(raw: bool, intf_name: str):
data = _get_raw_station_data(intf_name)
if raw:
diff --git a/src/op_mode/ipoe-control.py b/src/op_mode/ipoe-control.py
index b7d6a0c43..632cc93cc 100755
--- a/src/op_mode/ipoe-control.py
+++ b/src/op_mode/ipoe-control.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -27,7 +27,7 @@ cmd_dict = {
'actions' : {
'show_sessions' : 'show sessions',
'show_stat' : 'show stat',
- 'terminate' : 'teminate'
+ 'terminate' : 'terminate'
}
}
diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py
index 1ab50b105..3061089ee 100755
--- a/src/op_mode/ipsec.py
+++ b/src/op_mode/ipsec.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022-2025 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -229,18 +229,48 @@ def _get_parent_sa_state(connection_name: str, data: list) -> str:
ike_state = 'up'
return ike_state
+def _get_parent_ppk_state(connection_name: str, data: list) -> str:
+ """Get paren PPK state by connection name
-def _get_child_sa_state(connection_name: str, tunnel_name: str, data: list) -> str:
+ Args:
+ connection_name (str): Connection name
+ data (list): List of current SAs from vici
+
+ Returns:
+ Parent PPK state
+ """
+ ppk_state = 'no'
+ if not data:
+ return ppk_state
+ for sa in data:
+ # check if parent PPK exists
+ for connection, connection_conf in sa.items():
+ if connection_name != connection:
+ continue
+ if 'ppk' in connection_conf and connection_conf['ppk'].lower() == 'yes':
+ ppk_state = 'yes'
+ return ppk_state
+
+
+def _get_child_sa_state(
+ connection_name: str, tunnel_name: str, data: list, mode: str
+) -> str:
"""Get child SA state by connection and tunnel name
Args:
connection_name (str): Connection name
tunnel_name (str): Tunnel name
data (list): List of current SAs from vici
+ mode (str): Mode of child from vici list_connections
Returns:
- str: `up` if child SA state is 'installed' otherwise `down`
+ str: `up` if child SA state is 'installed' or child is passthrough
+ otherwise `down`
"""
+ # passthrough child (trap mode) has 'PASS' mode and is always up,
+ # but has no sa, so is not present in list_sas (data)
+ if mode == 'PASS':
+ return 'up'
child_sa = 'down'
if not data:
return child_sa
@@ -327,10 +357,22 @@ def _get_raw_data_connections(list_connections: list, list_sas: list) -> list:
base_list['local_id'] = conn_conf.get('local-1', '').get('id')
base_list['remote_id'] = conn_conf.get('remote-1', '').get('id')
base_list['version'] = conn_conf.get('version', 'IKE')
+ if conn_conf.get('ppk_id'):
+ if conn_conf.get('ppk_required') == 'yes':
+ base_list['ppk'] = 'req/' + _get_parent_ppk_state(
+ connection, list_sas
+ )
+ else:
+ base_list['ppk'] = 'opt/' + _get_parent_ppk_state(
+ connection, list_sas
+ )
+ else:
+ base_list['ppk'] = 'none/' + _get_parent_ppk_state(connection, list_sas)
base_list['children'] = []
children = conn_conf['children']
for tunnel, tun_options in children.items():
- state = _get_child_sa_state(connection, tunnel, list_sas)
+ mode = tun_options.get('mode')
+ state = _get_child_sa_state(connection, tunnel, list_sas, mode)
local_ts = tun_options.get('local-ts')
remote_ts = tun_options.get('remote-ts')
dpd_action = tun_options.get('dpd_action')
@@ -391,6 +433,8 @@ def _get_formatted_output_conections(data):
f'{entry["ike_proposal"]["hash"]}/'
f'{entry["ike_proposal"]["dh"]}'
)
+ ppk = entry['ppk']
+
connections.append(
[
ike_name,
@@ -402,6 +446,7 @@ def _get_formatted_output_conections(data):
local_id,
remote_id,
proposal,
+ ppk,
]
)
for tun in entry['children']:
@@ -419,6 +464,7 @@ def _get_formatted_output_conections(data):
f'{tun["esp_proposal"]["hash"]}/'
f'{tun["esp_proposal"]["dh"]}'
)
+ ppk = '-'
connections.append(
[
tun_name,
@@ -430,6 +476,7 @@ def _get_formatted_output_conections(data):
local_id,
remote_id,
proposal,
+ ppk,
]
)
connection_headers = [
@@ -442,8 +489,12 @@ def _get_formatted_output_conections(data):
'Local id',
'Remote id',
'Proposal',
+ 'PPK',
]
- output = tabulate(connections, connection_headers, numalign='left')
+ output = (
+ 'PPK Codes: none - Not Configured, opt - PPK is Optional, req - PPK is required, no - PPK not negotiated, yes - PPK negotiated\n'
+ + tabulate(connections, connection_headers, numalign='left')
+ )
return output
@@ -453,7 +504,7 @@ def _get_formatted_output_conections(data):
def _get_childsa_id_list(ike_sas: list) -> list:
"""
Generate list of CHILD SA ids based on list of OrderingDict
- wich is returned by vici
+ which is returned by vici
:param ike_sas: list of IKE SAs generated by vici
:type ike_sas: list
:return: list of IKE SAs ids
@@ -472,7 +523,7 @@ def _get_con_childsa_name_list(
) -> list:
"""
Generate list of CHILD SA ids based on list of OrderingDict
- wich is returned by vici
+ which is returned by vici
:param ike_sas: list of IKE SAs connections generated by vici
:type ike_sas: list
:param filter_dict: dict of filter options
@@ -739,7 +790,7 @@ def show_sa(raw: bool):
def _get_output_sas_detail(ra_output_list: list) -> str:
"""
- Formate all IKE SAs detail output
+ Format all IKE SAs detail output
:param ra_output_list: IKE SAs list
:type ra_output_list: list
:return: formatted RA IKE SAs detail output
@@ -870,7 +921,7 @@ def _get_formatted_ipsec_proposal(sa: dict) -> str:
def _get_output_ra_sas_detail(ra_output_list: list) -> str:
"""
- Formate RA IKE SAs detail output
+ Format RA IKE SAs detail output
:param ra_output_list: IKE SAs list
:type ra_output_list: list
:return: formatted RA IKE SAs detail output
diff --git a/src/op_mode/kernel_modules.py b/src/op_mode/kernel_modules.py
index e381a1df7..5475158aa 100755
--- a/src/op_mode/kernel_modules.py
+++ b/src/op_mode/kernel_modules.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/lldp.py b/src/op_mode/lldp.py
index fac622b81..6d77db5bc 100755
--- a/src/op_mode/lldp.py
+++ b/src/op_mode/lldp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/load-balancing_haproxy.py b/src/op_mode/load-balancing_haproxy.py
index ae6734e16..3ea016677 100755
--- a/src/op_mode/load-balancing_haproxy.py
+++ b/src/op_mode/load-balancing_haproxy.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/load-balancing_wan.py b/src/op_mode/load-balancing_wan.py
index 9fa473802..6f1d00dcd 100755
--- a/src/op_mode/load-balancing_wan.py
+++ b/src/op_mode/load-balancing_wan.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -56,13 +56,15 @@ def _get_raw_data():
return data
def _get_formatted_output(raw_data):
+ from time import time
+
for ifname, if_data in raw_data.items():
latest_change = if_data['last_success'] if if_data['last_success'] > if_data['last_failure'] else if_data['last_failure']
change_dt = datetime.fromtimestamp(latest_change) if latest_change > 0 else None
success_dt = datetime.fromtimestamp(if_data['last_success']) if if_data['last_success'] > 0 else None
failure_dt = datetime.fromtimestamp(if_data['last_failure']) if if_data['last_failure'] > 0 else None
- now = datetime.utcnow()
+ now = datetime.fromtimestamp(time())
fmt_data = {
'ifname': ifname,
diff --git a/src/op_mode/log.py b/src/op_mode/log.py
index 797ba5a88..0bf44cf6a 100755
--- a/src/op_mode/log.py
+++ b/src/op_mode/log.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/maya_date.py b/src/op_mode/maya_date.py
index 847b543e0..2d5a13ab9 100755
--- a/src/op_mode/maya_date.py
+++ b/src/op_mode/maya_date.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (c) 2013, 2018 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -168,7 +168,7 @@ class MayaDate(object):
""" The start date is not the beginning of both cycles,
it's 4 Ajaw. So we need to add 4 to the 13 days cycle day,
- and substract 1 from the 20 day cycle to get correct result.
+ and subtract 1 from the 20 day cycle to get correct result.
"""
tzolkin_13 = (days + 4) % 13
tzolkin_20 = (days - 1) % 20
@@ -181,7 +181,7 @@ class MayaDate(object):
""" Returns haab date string.
The time start on 8 Kumk'u rather than 0 Pop, which is
- 17 days before the new haab, so we need to substract 17
+ 17 days before the new haab, so we need to subtract 17
from the current date to get correct result.
"""
days = self.days
diff --git a/src/op_mode/memory.py b/src/op_mode/memory.py
index eb530035b..20d937243 100755
--- a/src/op_mode/memory.py
+++ b/src/op_mode/memory.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2022 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/mtr.py b/src/op_mode/mtr.py
index 522cbe008..646d95e7b 100644
--- a/src/op_mode/mtr.py
+++ b/src/op_mode/mtr.py
@@ -1,6 +1,6 @@
#! /usr/bin/env python3
-# Copyright (C) 2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/mtr_execute.py b/src/op_mode/mtr_execute.py
index 2585a7ee4..b97e46a1f 100644
--- a/src/op_mode/mtr_execute.py
+++ b/src/op_mode/mtr_execute.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/multicast.py b/src/op_mode/multicast.py
index 0666f8af3..096d01665 100755
--- a/src/op_mode/multicast.py
+++ b/src/op_mode/multicast.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py
index c6cf4770a..f97d7dc0f 100755
--- a/src/op_mode/nat.py
+++ b/src/op_mode/nat.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -144,9 +144,11 @@ def _get_formatted_output_rules(data, direction, family):
if 'expr' in rule['rule']:
interface = rule.get('rule').get('expr')[0].get('match').get('right') \
if jmespath.search('rule.expr[*].match.left.meta', rule) else 'any'
+ if interface[0] == '@':
+ interface = interface[3:]
for index, match in enumerate(jmespath.search('rule.expr[*].match', rule)):
if 'payload' in match['left']:
- # Handle NAT rule containing comma-seperated list of ports
+ # Handle NAT rule containing comma-separated list of ports
if (isinstance(match['right'], dict) and
('prefix' in match['right'] or 'set' in match['right'] or
'range' in match['right'])):
@@ -154,7 +156,10 @@ def _get_formatted_output_rules(data, direction, family):
my_dict = {**match['left']['payload'], **match['right']}
my_dict['op'] = match['op']
op = '!' if my_dict.get('op') == '!=' else ''
- proto = my_dict.get('protocol').upper()
+ if my_dict['field'] in ['sport', 'dport']:
+ proto = my_dict.get('protocol').upper()
+ if proto == 'TH':
+ proto = 'TCP, UDP'
if my_dict['field'] == 'saddr':
saddr = f'{op}{my_dict["prefix"]["addr"]}/{my_dict["prefix"]["len"]}'
elif my_dict['field'] == 'daddr':
@@ -166,6 +171,10 @@ def _get_formatted_output_rules(data, direction, family):
# Handle NAT rule containing a single port
else:
field = jmespath.search('left.payload.field', match)
+ if field in ['sport', 'dport']:
+ proto = jmespath.search('left.payload.protocol', match).upper()
+ if proto == 'TH':
+ proto = 'TCP, UDP'
if field == 'saddr':
saddr = match.get('right')
elif field == 'daddr':
@@ -186,8 +195,12 @@ sport {sport}'''
destination = f'''{daddr}
dport {dport}'''
- if jmespath.search('left.payload.field', match) == 'protocol':
- field_proto = match.get('right').upper()
+ if jmespath.search('left.meta.key', match) == 'l4proto':
+ right = match.get('right')
+ if isinstance(right, dict) and 'set' in right:
+ proto = ', '.join(right['set'])
+ elif isinstance(right, str):
+ proto = right.upper()
for expr in rule.get('rule').get('expr'):
if 'snat' in expr:
diff --git a/src/op_mode/neighbor.py b/src/op_mode/neighbor.py
index 8b3c45c7c..9bbad94e1 100755
--- a/src/op_mode/neighbor.py
+++ b/src/op_mode/neighbor.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022-2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -109,7 +109,7 @@ def reset(family: ArgFamily, interface: typing.Optional[str], address: typing.Op
run(f"""ip --family {family} neighbor flush dev {interface}""")
else:
# Flush an entire neighbor table
- run(f"""ip --family {family} neighbor flush""")
+ run(f"""ip --family {family} neighbor flush all""")
if __name__ == '__main__':
try:
@@ -119,4 +119,3 @@ if __name__ == '__main__':
except (ValueError, vyos.opmode.Error) as e:
print(e)
sys.exit(1)
-
diff --git a/src/op_mode/ntp.py b/src/op_mode/ntp.py
index 6ec0fedcb..42d0c1b00 100644
--- a/src/op_mode/ntp.py
+++ b/src/op_mode/ntp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/openconnect-control.py b/src/op_mode/openconnect-control.py
index b70d4fa16..dec5b9482 100755
--- a/src/op_mode/openconnect-control.py
+++ b/src/op_mode/openconnect-control.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/openconnect.py b/src/op_mode/openconnect.py
index 62c683ebb..35df25856 100755
--- a/src/op_mode/openconnect.py
+++ b/src/op_mode/openconnect.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/openvpn.py b/src/op_mode/openvpn.py
index 092873909..7347ac757 100755
--- a/src/op_mode/openvpn.py
+++ b/src/op_mode/openvpn.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022-2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/otp.py b/src/op_mode/otp.py
index a4ab9b22b..aceb75660 100755
--- a/src/op_mode/otp.py
+++ b/src/op_mode/otp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
-
-# Copyright 2017, 2022 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
diff --git a/src/op_mode/ping.py b/src/op_mode/ping.py
index 583d8792c..f52dfa7de 100755
--- a/src/op_mode/ping.py
+++ b/src/op_mode/ping.py
@@ -1,6 +1,6 @@
#! /usr/bin/env python3
-# Copyright (C) 2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -97,7 +97,7 @@ options = {
'no-loopback': {
'ping': '{command} -L',
'type': 'noarg',
- 'help': 'Supress loopback of multicast pings'
+ 'help': 'Suppress loopback of multicast pings'
},
'pattern': {
'ping': '{command} -p {value}',
diff --git a/src/op_mode/pki.py b/src/op_mode/pki.py
index 49a461e9e..bf8bba657 100755
--- a/src/op_mode/pki.py
+++ b/src/op_mode/pki.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -26,6 +26,7 @@ from cryptography.x509.oid import ExtendedKeyUsageOID
import vyos.opmode
+from vyos.base import Warning
from vyos.config import Config
from vyos.config import config_dict_mangle_acme
from vyos.pki import encode_certificate
@@ -417,7 +418,7 @@ def parse_san_string(san_string):
output.append(ipaddress.IPv6Address(value))
elif tag == 'dns' or tag == 'rfc822':
output.append(value)
- return
+ return output
def generate_certificate_request(
@@ -1251,6 +1252,7 @@ def show_certificate_authority(
def show_certificate(
raw: bool,
name: typing.Optional[str] = None,
+ private: typing.Optional[bool] = False,
pem: typing.Optional[bool] = False,
fingerprint: typing.Optional[ArgsFingerprint] = None,
):
@@ -1281,12 +1283,31 @@ def show_certificate(
if not cert:
continue
- if name and pem:
+ if name and pem and not (private or fingerprint):
print(encode_certificate(cert))
return
- elif name and fingerprint:
+ elif name and fingerprint and not private:
print(get_certificate_fingerprint(cert, fingerprint))
return
+ elif name and private:
+ if 'private' in cert_dict and 'key' in cert_dict['private']:
+ protected = 'password_protected' in cert_dict['private']
+ private_key = load_private_key(
+ cert_dict['private']['key'],
+ passphrase=None,
+ wrap_tags=True,
+ )
+ if private_key:
+ print(encode_private_key(private_key, passphrase=None))
+ else:
+ if protected:
+ print(f'Private key for certificate "{cert_name}" is '
+ 'password-protected and cannot be displayed')
+ else:
+ print(f'Failed to load private key for certificate "{cert_name}"')
+ else:
+ print(f'No private key found for certificate "{cert_name}"')
+ return
ca_name = get_certificate_ca(cert, ca_certs)
cert_subject_cn = cert.subject.rfc4514_string().split(',')[0]
@@ -1373,6 +1394,27 @@ def show_all(raw: bool):
print('\n')
show_crl(raw)
+def renew_certbot(raw: bool, force: typing.Optional[bool] = False):
+ from vyos.defaults import directories
+
+ certbot_config = directories['certbot']
+ vyos_conf_scripts_dir = directories['conf_mode']
+
+ if force and not os.path.isdir(f'{certbot_config}'):
+ # Assume someone deleted the certbot_config folder, renew alone will not
+ # work as there are no configuration files left to know what to renew.
+ # Re-run CLI PKI helper to initially request certificates via ACME
+ # again. This should never be the case - but sometimes the universe has
+ # a bad time
+ Warning(f'Directory "{certbot_config}" missing. Reinitializing PKI ' \
+ 'subsystem...\n\n')
+ out = cmd(f'sudo sg vyattacfg -c "{vyos_conf_scripts_dir}/pki.py"')
+ elif force:
+ out = cmd(f'sudo sg vyattacfg -c "{vyos_conf_scripts_dir}/pki.py certbot_renew_force"')
+ else:
+ out = cmd(f'sudo sg vyattacfg -c "{vyos_conf_scripts_dir}/pki.py certbot_renew"')
+
+ print(out)
if __name__ == '__main__':
try:
diff --git a/src/op_mode/policy_route.py b/src/op_mode/policy_route.py
index d12465008..966dc80f9 100755
--- a/src/op_mode/policy_route.py
+++ b/src/op_mode/policy_route.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py
index c32a2be7d..a60c33dc6 100755
--- a/src/op_mode/powerctrl.py
+++ b/src/op_mode/powerctrl.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/ppp-server-ctrl.py b/src/op_mode/ppp-server-ctrl.py
index 2bae5b32a..96b497549 100755
--- a/src/op_mode/ppp-server-ctrl.py
+++ b/src/op_mode/ppp-server-ctrl.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/qos.py b/src/op_mode/qos.py
index 464b552ee..47a7a3d59 100755
--- a/src/op_mode/qos.py
+++ b/src/op_mode/qos.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/raid.py b/src/op_mode/raid.py
index fed8ae2c3..985ef730f 100755
--- a/src/op_mode/raid.py
+++ b/src/op_mode/raid.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/reset_openvpn.py b/src/op_mode/reset_openvpn.py
index cef5299da..e3345435a 100755
--- a/src/op_mode/reset_openvpn.py
+++ b/src/op_mode/reset_openvpn.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/reset_vpn.py b/src/op_mode/reset_vpn.py
index 61d7c8c81..4ff1740d2 100755
--- a/src/op_mode/reset_vpn.py
+++ b/src/op_mode/reset_vpn.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022-2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/reset_wireguard.py b/src/op_mode/reset_wireguard.py
index 1fcfb31b5..ad8ea0346 100755
--- a/src/op_mode/reset_wireguard.py
+++ b/src/op_mode/reset_wireguard.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2025 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -16,35 +16,16 @@
import sys
import typing
-
import vyos.opmode
from vyos.ifconfig import WireGuardIf
-from vyos.configquery import ConfigTreeQuery
-
-
-def _verify(func):
- """Decorator checks if WireGuard interface config exists"""
- from functools import wraps
-
- @wraps(func)
- def _wrapper(*args, **kwargs):
- config = ConfigTreeQuery()
- interface = kwargs.get('interface')
- if not config.exists(['interfaces', 'wireguard', interface]):
- unconf_message = f'WireGuard interface {interface} is not configured'
- raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
- return func(*args, **kwargs)
- return _wrapper
-
-
-@_verify
+@vyos.opmode.verify_cli_exists(['interfaces', 'wireguard'],
+ 'WireGuard interface {interface} is not configured!')
def reset_peer(interface: str, peer: typing.Optional[str] = None):
intf = WireGuardIf(interface, create=False, debug=False)
return intf.operational.reset_peer(peer)
-
if __name__ == '__main__':
try:
res = vyos.opmode.run(sys.modules[__name__])
diff --git a/src/op_mode/restart.py b/src/op_mode/restart.py
index efa835485..4f05d6eb8 100755
--- a/src/op_mode/restart.py
+++ b/src/op_mode/restart.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
@@ -26,11 +26,11 @@ config = ConfigTreeQuery()
service_map = {
'dhcp': {
- 'systemd_service': 'kea-dhcp4-server',
+ 'systemd_service': 'isc-kea-dhcp4-server',
'path': ['service', 'dhcp-server'],
},
'dhcpv6': {
- 'systemd_service': 'kea-dhcp6-server',
+ 'systemd_service': 'isc-kea-dhcp6-server',
'path': ['service', 'dhcpv6-server'],
},
'dns_dynamic': {
diff --git a/src/op_mode/restart_dhcp_relay.py b/src/op_mode/restart_dhcp_relay.py
index 42626cac4..99c2c20b6 100755
--- a/src/op_mode/restart_dhcp_relay.py
+++ b/src/op_mode/restart_dhcp_relay.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/restart_frr.py b/src/op_mode/restart_frr.py
index 83146f5ec..188d99037 100755
--- a/src/op_mode/restart_frr.py
+++ b/src/op_mode/restart_frr.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/route.py b/src/op_mode/route.py
index 4aa57dbf4..b11b0ccc2 100755
--- a/src/op_mode/route.py
+++ b/src/op_mode/route.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/secure_boot.py b/src/op_mode/secure_boot.py
index 5f6390a15..a2d4c9e72 100755
--- a/src/op_mode/secure_boot.py
+++ b/src/op_mode/secure_boot.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/serial.py b/src/op_mode/serial.py
index a5864872b..e9f9fc121 100644
--- a/src/op_mode/serial.py
+++ b/src/op_mode/serial.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/sflow.py b/src/op_mode/sflow.py
index 0f3feb35a..668d0b7b3 100755
--- a/src/op_mode/sflow.py
+++ b/src/op_mode/sflow.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/show-bond.py b/src/op_mode/show-bond.py
index f676e0841..19abc440a 100755
--- a/src/op_mode/show-bond.py
+++ b/src/op_mode/show-bond.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -60,7 +60,7 @@ elif args.slaves:
cfg_dict['mode'] = tmp.get_mode()
cfg_dict['admin_state'] = tmp.get_admin_state()
cfg_dict['oper_state'] = tmp.operational.get_state()
- cfg_dict['members'] = tmp.get_slaves()
+ cfg_dict['members'] = tmp.get_members()
data.append(cfg_dict)
elif args.interface:
@@ -74,7 +74,7 @@ elif args.interface:
# each bond member interface has its own statistics
data['members'] = []
- for member in BondIf(args.interface).get_slaves():
+ for member in BondIf(args.interface).get_members():
tmp = {}
tmp['ifname'] = member
tmp['rx_bytes'] = read_file(f'/sys/class/net/{member}/statistics/rx_bytes')
diff --git a/src/op_mode/show_acceleration.py b/src/op_mode/show_acceleration.py
index 1c4831f1d..05c591356 100755
--- a/src/op_mode/show_acceleration.py
+++ b/src/op_mode/show_acceleration.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -26,7 +26,7 @@ from vyos.utils.process import popen
def detect_qat_dev():
output, err = popen('lspci -nn', decode='utf-8')
if not err:
- data = re.findall('(8086:19e2)|(8086:37c8)|(8086:0435)|(8086:6f54)', output)
+ data = re.findall('(8086:19e2)|(8086:37c[8-9])|(8086:0435)|(8086:6f54)', output)
# QAT devices found
if data:
return
@@ -71,7 +71,7 @@ def get_qat_proc_path(qat_dev):
q_bsf = q_list[1]
return "/sys/kernel/debug/qat_"+q_type+"_"+q_bsf+"/"
-# Check if QAT service confgured
+# Check if QAT service configured
def check_qat_if_conf():
if not Config().exists_effective('system acceleration qat'):
print("\t system acceleration qat is not configured")
@@ -92,8 +92,8 @@ args = parser.parse_args()
if args.hw:
detect_qat_dev()
- # Show availible Intel QAT devices
- call('lspci -nn | egrep -e \'8086:37c8|8086:19e2|8086:0435|8086:6f54\'')
+ # Show available Intel QAT devices
+ call('lspci -nn | egrep -e \'8086:37c[8-9]|8086:19e2|8086:0435|8086:6f54\'')
elif args.flow and args.dev:
check_qat_if_conf()
call('cat '+get_qat_proc_path(args.dev)+"fw_counters")
diff --git a/src/op_mode/show_bonding_detail.sh b/src/op_mode/show_bonding_detail.sh
new file mode 100755
index 000000000..62265daa2
--- /dev/null
+++ b/src/op_mode/show_bonding_detail.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+if [ -f "/proc/net/bonding/$1" ]; then
+ cat "/proc/net/bonding/$1";
+else
+ echo "Interface $1 does not exist!";
+fi
diff --git a/src/op_mode/show_configuration_json.py b/src/op_mode/show_configuration_json.py
index fdece533b..4e4b4d386 100755
--- a/src/op_mode/show_configuration_json.py
+++ b/src/op_mode/show_configuration_json.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/show_openconnect_otp.py b/src/op_mode/show_openconnect_otp.py
index 3771fb385..61e52d01e 100755
--- a/src/op_mode/show_openconnect_otp.py
+++ b/src/op_mode/show_openconnect_otp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
-# Copyright 2017-2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -97,7 +97,7 @@ def display_otp_ocserv(username, params, info):
if __name__ == '__main__':
parser = argparse.ArgumentParser(add_help=False, description='Show OTP authentication information for selected user')
parser.add_argument('--user', action="store", type=str, default='', help='Username')
- parser.add_argument('--info', action="store", type=str, default='full', help='Wich information to display')
+ parser.add_argument('--info', action="store", type=str, default='full', help='Which information to display')
args = parser.parse_args()
if check_uname_otp(args.user):
diff --git a/src/op_mode/show_openvpn.py b/src/op_mode/show_openvpn.py
index 6abafc8b6..9f2708b69 100755
--- a/src/op_mode/show_openvpn.py
+++ b/src/op_mode/show_openvpn.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/show_openvpn_mfa.py b/src/op_mode/show_openvpn_mfa.py
index 100c42154..a08fd33ae 100755
--- a/src/op_mode/show_openvpn_mfa.py
+++ b/src/op_mode/show_openvpn_mfa.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright 2017-2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
diff --git a/src/op_mode/show_ppp_stats.sh b/src/op_mode/show_ppp_stats.sh
new file mode 100755
index 000000000..d9c17f966
--- /dev/null
+++ b/src/op_mode/show_ppp_stats.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+if [ -d "/sys/class/net/$1" ]; then
+ /usr/sbin/pppstats "$1";
+fi
diff --git a/src/op_mode/show_sensors.py b/src/op_mode/show_sensors.py
index 5e3084fe9..b7ff05178 100755
--- a/src/op_mode/show_sensors.py
+++ b/src/op_mode/show_sensors.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright 2017-2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -17,25 +17,26 @@
import re
import sys
+
from vyos.utils.process import popen
from vyos.utils.process import DEVNULL
-output,retcode = popen("sensors --no-adapter", stderr=DEVNULL)
+output, retcode = popen("sensors --no-adapter", stderr=DEVNULL)
if retcode == 0:
print (output)
sys.exit(0)
else:
- output,retcode = popen("sensors-detect --auto",stderr=DEVNULL)
- match = re.search(r'#----cut here----(.*)#----cut here----',output, re.DOTALL)
+ output, retcode = popen("sensors-detect --auto", stderr=DEVNULL)
+ match = re.search(r'#----cut here----(.*)#----cut here----', output,
+ re.DOTALL)
if match:
for module in match.group(0).split('\n'):
if not module.startswith("#"):
popen("modprobe {}".format(module.strip()))
- output,retcode = popen("sensors --no-adapter", stderr=DEVNULL)
+ output, retcode = popen("sensors --no-adapter", stderr=DEVNULL)
if retcode == 0:
- print (output)
+ print(output)
sys.exit(0)
-
-print ("No sensors found")
+print("No sensors found")
sys.exit(1)
diff --git a/src/op_mode/show_techsupport_report.py b/src/op_mode/show_techsupport_report.py
index 32cf67778..2434da29a 100644
--- a/src/op_mode/show_techsupport_report.py
+++ b/src/op_mode/show_techsupport_report.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -16,235 +16,321 @@
import os
import sys
-from typing import List
+import argparse
+from pathlib import Path
+from dataclasses import dataclass
+from dataclasses import field
+from typing import Callable
+from typing import Optional
+from typing import Sequence
+
from vyos.ifconfig import Section
from vyos.ifconfig import Interface
from vyos.utils.process import rc_cmd
+from vyos.utils.process import wrap_op as op
-def print_header(command: str) -> None:
- """Prints a command with headers '-'.
-
- Example:
+@dataclass(frozen=True)
+class BaseSpec:
+ # Available only for 'show tech-support report' (not 'generate tech-support archive')
+ report_only: Optional[bool] = field(kw_only=True, default=False)
- % print_header('Example command')
- ---------------
- Example command
- ---------------
- """
- header_length = len(command) * '-'
- print(f"\n{header_length}\n{command}\n{header_length}")
+@dataclass(frozen=True)
+class CommandSpec(BaseSpec):
+ header: str # Display header for a command section
+ command: str # Shell command to execute
-def execute_command(command: str, header_text: str) -> None:
- """Executes a command and prints the output with a header.
+@dataclass(frozen=True)
+class FuncSpec(BaseSpec):
+ name: str # Display name for a function section
+ fn: Callable[
+ ['Runner'], None
+ ] # Callable that receives a Runner and writes output via it
- Example:
- % execute_command('uptime', "Uptime of the system")
- --------------------
- Uptime of the system
- --------------------
- 20:21:57 up 9:04, 5 users, load average: 0.00, 0.00, 0.0
+class OutputSink:
+ """Writes output to stdout and/or a file, depending on parameters"""
- """
- print_header(header_text)
- try:
- rc, output = rc_cmd(command)
- # 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}")
+ def __init__(self, *, file_path: Optional[Path]):
+ # Target file path; None means stdout
+ self._file_path = file_path
+ self._fh = None
+ def __enter__(self) -> 'OutputSink':
+ # Open output file if not writing to stdout
+ if not self.is_stdout:
+ self._file_path.parent.mkdir(parents=True, exist_ok=True)
+ # Use text mode, overwrite per run
+ self._fh = self._file_path.open('w', encoding='utf-8', errors='replace')
+ return self
-def op(cmd: str) -> str:
- """Returns a command with the VyOS operational mode wrapper."""
- return f'/opt/vyatta/bin/vyatta-op-cmd-wrapper {cmd}'
+ def __exit__(self, exc_type, exc, tb):
+ if self._fh is not None:
+ self._fh.close()
+ self._fh = None
+ @property
+ def is_stdout(self):
+ # True when output is directed to stdout
+ return self._file_path is None
-def get_ethernet_interfaces() -> List[Interface]:
+ def write(self, text: str):
+ # Enable unbuffered print param to improve responsiveness of printed
+ # output to end user
+ if self.is_stdout:
+ print(text, end='', flush=True)
+ else:
+ if self._fh is not None:
+ self._fh.write(text)
+ self._fh.flush()
+
+
+class Runner:
+ """Executes commands and writes output to the provided sink"""
+
+ def __init__(self, sink: OutputSink):
+ self.sink = sink
+
+ def exec(self, command: str, header: str = None):
+ texts = []
+
+ if header:
+ texts.append(header)
+ texts.append(f'Command: {command}')
+
+ try:
+ # Start a new section for this command
+ self.section('\n'.join(texts))
+
+ rc, output = rc_cmd(command)
+ # Ensure output ends with newline when present
+ if output and not output.endswith('\n'):
+ output += '\n'
+
+ self.sink.write(output)
+ if rc not in (0, None):
+ self.sink.write(
+ f'Command `{command}` returned non-zero ({rc}) exit status\n'
+ )
+ except (BrokenPipeError, KeyboardInterrupt):
+ raise
+ except Exception as e:
+ self.sink.write(f'Error executing command: {command}\n')
+ self.sink.write(f'Error message: {e}\n')
+
+ def section(self, title: str):
+ """Just print a section header without running a command"""
+ self.sink.write(header_block(title))
+
+
+def header_block(title: str, delimiter='-') -> str:
+ """Create an underline/overline header block for multiline text."""
+ lines = title.splitlines()
+ max_len = max(len(line) for line in lines)
+ line = delimiter * max_len
+ title_block = '\n'.join(lines)
+ return f'\n{line}\n{title_block}\n{line}\n'
+
+
+def select_reports(args: argparse.Namespace) -> dict[str, tuple[BaseSpec]]:
+ reports = REPORTS.copy()
+ requested = args.reports
+
+ if requested:
+ missing = [r for r in requested if r not in reports]
+ if missing:
+ known = ', '.join(sorted(reports.keys()))
+ raise SystemExit(f'Unknown report(s): {", ".join(missing)}. Known: {known}')
+
+ reports = {name: reports[name] for name in requested}
+
+ if args.launched_from_generate_archive:
+ for key in reports.keys():
+ items = reports[key]
+ reports[key] = {item for item in items if not item.report_only}
+
+ return reports
+
+
+def execute_item(item: BaseSpec, runner: Runner) -> None:
+ if isinstance(item, CommandSpec):
+ runner.exec(item.command, item.header)
+ elif isinstance(item, FuncSpec):
+ runner.section(item.name)
+ item.fn(runner)
+ else:
+ assert False, f'Unsupported report type: {type(item)}'
+
+
+def parse_args(argv: Sequence[str]) -> argparse.Namespace:
+ p = argparse.ArgumentParser(description='VyOS tech-support command collector')
+ p.add_argument(
+ '--outdir',
+ type=Path,
+ default=None,
+ help=(
+ 'Directory to write report files into (one file per report group). '
+ 'If this option is omitted, files are written to stdout.'
+ ),
+ )
+ p.add_argument(
+ '--reports',
+ nargs='*',
+ default=[],
+ help=(
+ 'Which report groups to run (default: all). '
+ 'Example: --reports vyos-main-info'
+ ),
+ )
+ p.add_argument(
+ '--launched-from-generate-archive',
+ action='store_true',
+ default=False,
+ help=(
+ 'A boolean flag indicates that command executed for generating '
+ 'tech-support archive (`generate tech-support archive`). '
+ 'In this case some sections will be ignored because already available in archive.'
+ ),
+ )
+ return p.parse_args(argv)
+
+
+def main(argv: Sequence[str]):
+ args = parse_args(argv)
+
+ # Select which reports to execute
+ chosen = select_reports(args)
+
+ # Execute each report and write to its own sink
+ for report_name, commands in chosen.items():
+ # Use a per-report output file when `outdir` is provided
+ file_path = (args.outdir / report_name) if args.outdir else None
+
+ with OutputSink(file_path=file_path) as sink:
+ runner = Runner(sink)
+ try:
+ # Write top-level report header
+ sink.write(header_block(report_name, delimiter='='))
+
+ # Execute each item in the report
+ for item in commands:
+ execute_item(item, runner)
+ except (BrokenPipeError, KeyboardInterrupt):
+ if sink.is_stdout:
+ # Exit gracefully when user interrupts program output
+ # Flush standard streams; redirect remaining output to devnull
+ # Resolves T5633: Bug #1 and 3
+ os.dup2(
+ os.open(os.devnull, os.O_WRONLY),
+ sys.stdout.fileno(), # pylint: disable = no-member
+ )
+ sys.exit(1)
+
+
+def get_ethernet_interfaces() -> list[Interface]:
"""Returns a list of Ethernet interfaces."""
return Section.interfaces('ethernet')
-def show_version() -> None:
- """Prints the VyOS version and package changes."""
- execute_command(op('show version'), 'VyOS Version and Package Changes')
-
-
-def show_config_file() -> None:
- """Prints the contents of a configuration file with a header."""
- execute_command('cat /opt/vyatta/etc/config/config.boot', 'Configuration file')
-
-
-def show_running_config() -> None:
- """Prints the running configuration."""
- execute_command(op('show configuration'), 'Running configuration')
-
-
-def show_package_repository_config() -> None:
- """Prints the package repository configuration file."""
- execute_command('cat /etc/apt/sources.list', 'Package Repository Configuration File')
- execute_command('ls -l /etc/apt/sources.list.d/', 'Repositories')
-
-
-def show_user_startup_scripts() -> None:
- """Prints the user startup scripts."""
- execute_command('cat /config/scripts/vyos-preconfig-bootup.script', 'User Startup Scripts (Preconfig)')
- execute_command('cat /config/scripts/vyos-postconfig-bootup.script', 'User Startup Scripts (Postconfig)')
-
-
-def show_frr_config() -> None:
- """Prints the FRR configuration."""
- execute_command('vtysh -c "show run"', 'FRR configuration')
-
-
-def show_interfaces() -> None:
- """Prints the interfaces."""
- execute_command(op('show interfaces'), 'Interfaces')
-
-
-def show_interface_statistics() -> None:
- """Prints the interface statistics."""
- execute_command('ip -s link show', 'Interface statistics')
-
-
-def show_physical_interface_statistics() -> None:
+def show_physical_interface_statistics(r: Runner):
"""Prints the physical interface statistics."""
- execute_command('/usr/bin/true', 'Physical Interface statistics')
+
for iface in get_ethernet_interfaces():
- # Exclude vlans
- if '.' in iface:
+ if '.' in iface: # exclude VLANs
continue
- execute_command(f'ethtool --driver {iface}', f'ethtool --driver {iface}')
- execute_command(f'ethtool --statistics {iface}', f'ethtool --statistics {iface}')
- execute_command(f'ethtool --show-ring {iface}', f'ethtool --show-ring {iface}')
- execute_command(f'ethtool --show-coalesce {iface}', f'ethtool --show-coalesce {iface}')
- execute_command(f'ethtool --pause {iface}', f'ethtool --pause {iface}')
- execute_command(f'ethtool --show-features {iface}', f'ethtool --show-features {iface}')
- execute_command(f'ethtool --phy-statistics {iface}', f'ethtool --phy-statistics {iface}')
- execute_command('netstat --interfaces', 'netstat --interfaces')
- execute_command('netstat --listening', 'netstat --listening')
- execute_command('cat /proc/net/dev', 'cat /proc/net/dev')
+ r.exec(f'ethtool --driver {iface}')
+ r.exec(f'ethtool --statistics {iface}')
+ r.exec(f'ethtool --show-ring {iface}')
+ r.exec(f'ethtool --show-coalesce {iface}')
+ r.exec(f'ethtool --pause {iface}')
+ r.exec(f'ethtool --show-features {iface}')
+ r.exec(f'ethtool --phy-statistics {iface}')
+ r.exec(f'ethtool --module-info {iface}')
-def show_bridge() -> None:
- """Show bridge interfaces."""
- execute_command(op('show bridge'), 'Show bridge')
+ for path in Path('/run/udev/vyos').glob('*'):
+ if path.is_file():
+ r.exec(f'cat {path.resolve()}')
-def show_arp() -> None:
- """Prints ARP entries."""
- execute_command(op('show arp'), 'ARP Table (Total entries)')
- execute_command(op('show ipv6 neighbors'), 'show ipv6 neighbors')
+def _exec_list_op(r: Runner, commands: list):
+ for command in commands:
+ r.exec(op(command))
-def show_route() -> None:
+def show_route(r: Runner):
"""Prints routing information."""
- cmd_list_route = [
- "show ip route bgp | head -108",
- "show ip route cache",
- "show ip route connected",
- "show ip route forward",
- "show ip route isis | head -108",
- "show ip route kernel",
- "show ip route ospf | head -108",
- "show ip route rip",
- "show ip route static",
- "show ip route summary",
- "show ip route supernets-only",
- "show ip route table all",
- "show ip route vrf all",
- "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 ospfv3",
- "show ipv6 route rip",
- "show ipv6 route static",
- "show ipv6 route summary",
- "show ipv6 route table all",
- "show ipv6 route vrf all",
+ commands = [
+ 'show ip route bgp | head -108',
+ 'show ip route connected',
+ 'show ip route forward | head -108',
+ 'show ip route isis | head -108',
+ 'show ip route kernel',
+ 'show ip route ospf | head -108',
+ 'show ip route rip | head -108',
+ 'show ip route static',
+ 'show ip route summary',
+ 'show ip route supernets-only | head -108',
+ 'show ip route table all | head -108',
+ 'show ip route vrf all | head -108',
+ 'show ipv6 route bgp | head -108',
+ 'show ipv6 route connected',
+ 'show ipv6 route forward | head -108',
+ 'show ipv6 route isis | head -108',
+ 'show ipv6 route kernel',
+ 'show ipv6 route ospfv3 | head -108',
+ 'show ipv6 route rip | head -108',
+ 'show ipv6 route static',
+ 'show ipv6 route summary',
+ 'show ipv6 route table all | head -108',
+ 'show ipv6 route vrf all | head -108',
]
- for command in cmd_list_route:
- execute_command(op(command), command)
-
-
-def show_firewall() -> None:
- """Prints firweall information."""
- execute_command('sudo nft list ruleset', 'nft list ruleset')
-
+ _exec_list_op(r, commands)
-def show_system() -> None:
- """Prints system parameters."""
- 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_evpn(r: Runner):
+ """Prints EVPN information."""
-def show_date() -> None:
- """Print the current date."""
- execute_command('date', 'Current Time')
-
-
-def show_installed_packages() -> None:
- """Prints installed packages."""
- execute_command('dpkg --list', 'Installed Packages')
-
-
-def show_loaded_modules() -> None:
- """Prints loaded modules /proc/modules"""
- execute_command('cat /proc/modules', 'Loaded Modules')
-
-
-def show_cpu_statistics() -> None:
- """Prints CPU statistics."""
- execute_command('/usr/bin/true', 'CPU')
- execute_command('lscpu', 'Installed CPU\'s')
- execute_command('top --iterations 1 --batch-mode --accum-time-toggle', 'Cumulative CPU Time Used by Running Processes')
- execute_command('cat /proc/loadavg', 'Load Average')
-
-
-def show_system_interrupts() -> None:
- """Prints system interrupts."""
- execute_command('cat /proc/interrupts', 'Hardware Interrupt Counters')
-
-
-def show_soft_irqs() -> None:
- """Prints soft IRQ's."""
- execute_command('cat /proc/softirqs', 'Soft IRQ\'s')
+ commands = [
+ 'show evpn mac vni all',
+ 'show evpn next-hops vni all',
+ 'show evpn rmac vni all',
+ 'show evpn access-vlan',
+ 'show evpn arp-cache vni all',
+ 'show evpn es',
+ 'show evpn es-evi',
+ ]
+ _exec_list_op(r, commands)
-def show_softnet_statistics() -> None:
- """Prints softnet statistics."""
- execute_command('cat /proc/net/softnet_stat', 'cat /proc/net/softnet_stat')
+def show_mpls(r: Runner):
+ """Prints MPLS information."""
+ commands = [
+ 'show mpls pseudowire',
+ 'show mpls table',
+ 'show mpls ldp binding',
+ 'show mpls ldp discovery',
+ 'show mpls ldp interface',
+ 'show mpls ldp neighbor',
+ ]
+ _exec_list_op(r, commands)
-def show_running_processes() -> None:
- """Prints current running processes"""
- execute_command('ps -ef', 'Running Processes')
+def show_rpki(r: Runner):
+ """Prints RPKI information."""
-def show_memory_usage() -> None:
- """Prints memory usage"""
- execute_command('/usr/bin/true', 'Memory')
- execute_command('cat /proc/meminfo', 'Installed Memory')
- execute_command('free', 'Memory Usage')
+ commands = [
+ 'show rpki cache-server',
+ 'show rpki cache-connection',
+ ]
+ _exec_list_op(r, commands)
-def list_disks():
+def _list_disks():
disks = set()
with open('/proc/partitions') as partitions_file:
for line in partitions_file:
@@ -254,60 +340,211 @@ def list_disks():
return disks
-def show_storage() -> None:
+def show_storage(r: Runner):
"""Prints storage information."""
- execute_command('cat /proc/devices', 'Devices')
- execute_command('cat /proc/partitions', 'Partitions')
-
- for disk in list_disks():
- execute_command(f'fdisk --list /dev/{disk}', f'Partitioning for disk {disk}')
-
-
-def main():
- # Configuration data
- show_version()
- show_config_file()
- show_running_config()
- show_package_repository_config()
- show_user_startup_scripts()
- show_frr_config()
-
- # Interfaces
- show_interfaces()
- show_interface_statistics()
- show_physical_interface_statistics()
- show_bridge()
- show_arp()
-
- # Routing
- show_route()
-
- # Firewall
- show_firewall()
-
- # System
- show_system()
- show_date()
- show_installed_packages()
- show_loaded_modules()
-
- # CPU
- show_cpu_statistics()
- show_system_interrupts()
- show_soft_irqs()
- show_softnet_statistics()
-
- # Memory
- show_memory_usage()
-
- # Storage
- show_storage()
-
- # Processes
- show_running_processes()
-
- # TODO: Get information from clouds
-
-if __name__ == "__main__":
- main()
+ r.exec('cat /proc/mounts', 'Mount table')
+ r.exec('cat /proc/partitions', 'Partitions table')
+
+ for disk in _list_disks():
+ r.exec(f'fdisk --list /dev/{disk}', f'Partitioning for disk {disk}')
+
+ r.exec('df -ah', 'Filesystem usage')
+ r.exec('df -ahi', 'Filesystem inode usage')
+
+
+def show_kernel_interface_counters(r: Runner):
+ """Prints kernel network interface counters by fixed format."""
+
+ headers = (
+ 'bytes',
+ 'packets',
+ 'errs',
+ 'drop',
+ 'fifo',
+ 'frame',
+ 'compressed',
+ 'multicast',
+ )
+ new_headers = (
+ ['Interface'] + [f'RX-{h}' for h in headers] + [f'TX-{h}' for h in headers]
+ )
+ echo_template = ' '.join(new_headers)
+ awk_template = ','.join([f'${i}' for i in range(1, len(new_headers) + 1)])
+
+ cmd = (
+ """(echo "{0}" && awk 'NR>2 {{print {1}}}' /proc/net/dev) | column -t""".format(
+ echo_template, awk_template
+ )
+ )
+ r.exec(cmd, 'cat /proc/net/dev | column -t')
+
+
+REPORTS: dict[str, tuple[BaseSpec]] = {
+ 'vyos-main-info': (
+ CommandSpec('VyOS version and package info', op('show version')),
+ CommandSpec(
+ 'Running configuration (commands)', op('show configuration commands')
+ ),
+ CommandSpec('Running configuration (structured)', op('show configuration')),
+ CommandSpec(
+ 'Configuration file (config.boot)',
+ 'cat /opt/vyatta/etc/config/config.boot',
+ report_only=True, # Ignored because already exists in 'tech-support archive'
+ ),
+ CommandSpec('Interfaces summary', op('show interfaces')),
+ CommandSpec('Bridge status', op('show bridge')),
+ CommandSpec('ARP table', op('show arp')),
+ CommandSpec('IPv6 neighbor table', op('show ipv6 neighbors')),
+ CommandSpec('System storage overview', op('show system storage')),
+ CommandSpec('Installed images details', op('show system image details')),
+ CommandSpec(
+ 'User startup script (preconfig)',
+ 'cat /config/scripts/vyos-preconfig-bootup.script',
+ report_only=True,
+ ),
+ CommandSpec(
+ 'User startup script (postconfig)',
+ 'cat /config/scripts/vyos-postconfig-bootup.script',
+ report_only=True,
+ ),
+ ),
+ 'routing-info': (
+ FuncSpec('Routing table (IPv4/IPv6)', show_route),
+ CommandSpec('BFD peers', op('show bfd peers')),
+ FuncSpec('EVPN status', show_evpn),
+ FuncSpec('MPLS status', show_mpls),
+ FuncSpec('RPKI status', show_rpki),
+ ),
+ 'frr-info': (
+ CommandSpec('FRR running configuration', 'vtysh -c "show running-config"'),
+ CommandSpec('FRR memory usage', 'vtysh -c "show memory"'),
+ CommandSpec('FRR work queues', 'vtysh -c "show work-queues"'),
+ CommandSpec('FRR IPv4 nexthop tracking (NHT)', 'vtysh -c "show ip nht"'),
+ CommandSpec('FRR IPv6 nexthop tracking (NHT)', 'vtysh -c "show ipv6 nht"'),
+ CommandSpec('FRR DMVPN status', 'vtysh -c "show dmvpn"'),
+ CommandSpec('FRR event CPU stats', 'vtysh -c "show event cpu"'),
+ CommandSpec('FRR event poll stats', 'vtysh -c "show event poll"'),
+ CommandSpec('FRR event timers', 'vtysh -c "show event timers"'),
+ CommandSpec(
+ 'FRR SRv6 locator',
+ 'vtysh -c "show segment-routing srv6 locator"',
+ ),
+ CommandSpec(
+ 'FRR SRv6 manager',
+ 'vtysh -c "show segment-routing srv6 manager"',
+ ),
+ ),
+ 'proc-and-sysctl-info': (
+ CommandSpec('System load average', 'cat /proc/loadavg'),
+ FuncSpec('Kernel network interface counters', show_kernel_interface_counters),
+ CommandSpec('Loaded kernel modules', 'cat /proc/modules'),
+ CommandSpec('Hardware interrupt counters', 'cat /proc/interrupts'),
+ CommandSpec('SoftIRQ counters', 'cat /proc/softirqs'),
+ CommandSpec('Softnet statistics', 'cat /proc/net/softnet_stat'),
+ CommandSpec('Memory info', 'cat /proc/meminfo'),
+ CommandSpec(
+ 'NUMA node memory info',
+ 'cat /sys/devices/system/node/node*/meminfo',
+ ),
+ CommandSpec('VM statistics', 'cat /proc/vmstat'),
+ CommandSpec('Registered character/block devices', 'cat /proc/devices'),
+ CommandSpec('Kernel command line', 'cat /proc/cmdline'),
+ CommandSpec('All sysctl values', 'sysctl -a'),
+ ),
+ 'net-and-processes-info': (
+ CommandSpec('Network interfaces', 'netstat --interfaces'),
+ CommandSpec('Listening sockets', 'netstat --listening'),
+ CommandSpec('Socket summary', 'ss -s'),
+ CommandSpec('All sockets with details', 'ss -a -e -m -p'),
+ CommandSpec('Full process listing', 'ps -eF'),
+ ),
+ 'ethtool-info': (
+ CommandSpec(
+ 'Link details and interface counters',
+ 'ip -s -d link show',
+ ),
+ FuncSpec('Physical interface statistics', show_physical_interface_statistics),
+ ),
+ 'lspci-and-numa-info': (
+ CommandSpec('PCI devices', 'lspci -knnv'),
+ CommandSpec('', 'numactl --hardware'),
+ CommandSpec('', 'numastat -cm'),
+ ),
+ 'nftables-info': (
+ CommandSpec('nftables ruleset', 'nft list ruleset'),
+ CommandSpec('VyOS firewall configuration', op('show firewall')),
+ CommandSpec('VyOS firewall zone policy', op('show firewall zone-policy')),
+ ),
+ 'dpkg-and-modules-info': (
+ CommandSpec('Installed packages', 'dpkg --list'),
+ CommandSpec(
+ 'Diff dpkg status (image vs running system)',
+ 'diff /usr/lib/live/mount/rootfs/*.squashfs/var/lib/dpkg/status /var/lib/dpkg/status',
+ ),
+ CommandSpec('APT sources list', 'cat /etc/apt/sources.list'),
+ CommandSpec(
+ 'APT sources.d directory listing', 'ls -l /etc/apt/sources.list.d/'
+ ),
+ CommandSpec('Loaded kernel modules', 'lsmod'),
+ ),
+ 'system-resources-info': (
+ CommandSpec('Current time (date)', 'date'),
+ CommandSpec('CPU information', 'lscpu'),
+ CommandSpec(
+ 'Per-process cumulative CPU usage snapshot (top batch, accumulated)',
+ 'top --iterations 1 --batch-mode --accum-time-toggle',
+ ),
+ CommandSpec('atop snapshot (CPU view)', 'atop -a -y -1 -c -g -C 1 1 | tee'),
+ CommandSpec('atop snapshot (memory view)', 'atop -a -y -1 -c -m -M 1 1 | tee'),
+ CommandSpec('atop snapshot (disk view)', 'atop -a -y -1 -c -d -D 1 1 | tee'),
+ CommandSpec('atop snapshot (network view)', 'atop -a -y -1 -c -n -N 1 1 | tee'),
+ CommandSpec('Memory usage', 'free -lhv'),
+ FuncSpec('Storage overview', show_storage),
+ ),
+ 'ipsec-debug-info': (
+ CommandSpec('strongSwan connections', 'swanctl -L'),
+ CommandSpec('strongSwan loaded connections', 'swanctl -l'),
+ CommandSpec('strongSwan policies', 'swanctl -P'),
+ CommandSpec('XFRM security associations', 'ip x sa show'),
+ CommandSpec('XFRM policies', 'ip x policy show'),
+ CommandSpec('XFRM state', 'ip xfrm state'),
+ CommandSpec('Tunnels', 'ip tunnel show'),
+ CommandSpec('Addresses', 'ip address'),
+ CommandSpec('Policy routing rules', 'ip rule show'),
+ CommandSpec('Routes', 'ip route | head -100'),
+ CommandSpec('Routes from table 220', 'ip route show table 220'),
+ ),
+ 'vpp-info': (
+ CommandSpec('', 'cat /run/vpp/vpp.conf'),
+ CommandSpec('', 'vppctl show version verbose cmdline'),
+ CommandSpec('', 'vppctl show hardware-interfaces'),
+ CommandSpec('', 'vppctl show interface address'),
+ CommandSpec('', 'vppctl show interface'),
+ CommandSpec('', 'vppctl show errors'),
+ CommandSpec('', 'vppctl show runtime'),
+ CommandSpec(
+ '',
+ 'vppctl show memory api-segment stats-segment numa-heaps main-heap map verbose',
+ ),
+ CommandSpec('', 'vppctl show buffers'),
+ CommandSpec('', 'vppctl show physmem detail'),
+ CommandSpec('', 'vppctl show physmem map'),
+ CommandSpec('', 'vppctl show cpu'),
+ CommandSpec('', 'vppctl show threads'),
+ CommandSpec('', 'vppctl show node counters'),
+ CommandSpec('', 'vppctl show l2fib'),
+ CommandSpec('', 'vppctl show bridge-domain'),
+ CommandSpec('', 'vppctl show ip fib | head -100'),
+ CommandSpec('', 'vppctl show ip neighbors'),
+ CommandSpec('', 'vppctl show ip6 fib | head -100'),
+ CommandSpec('', 'vppctl show ip6 neighbors'),
+ CommandSpec('', 'vppctl show mpls fib'),
+ CommandSpec('', 'vppctl show mpls tunnel'),
+ CommandSpec('', 'vppctl show trace'),
+ ),
+}
+
+
+if __name__ == '__main__':
+ main(sys.argv[1:])
diff --git a/src/op_mode/show_usb_serial.py b/src/op_mode/show_usb_serial.py
index 973bf19c8..7c1d2a07f 100755
--- a/src/op_mode/show_usb_serial.py
+++ b/src/op_mode/show_usb_serial.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/show_users.py b/src/op_mode/show_users.py
index 82bd585c9..086c8b1e2 100755
--- a/src/op_mode/show_users.py
+++ b/src/op_mode/show_users.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -13,15 +13,15 @@
#
# 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 argparse
-import pwd
import struct
import sys
from time import ctime
from tabulate import tabulate
from vyos.config import Config
-
+from vyos.utils.auth import get_local_passwd_entries
class UserInfo:
def __init__(self, uid, name, user_type, is_locked, login_time, tty, host):
@@ -79,7 +79,9 @@ def list_users():
vyos_users = cfg.list_effective_nodes('system login user')
users = []
with open('/var/log/lastlog', 'rb') as lastlog_file:
- for (name, _, uid, _, _, _, _) in pwd.getpwall():
+ for entry in get_local_passwd_entries():
+ name = entry.pw_name
+ uid = entry.pw_uid
lastlog_info = decode_lastlog(lastlog_file, uid)
if lastlog_info is None:
continue
diff --git a/src/op_mode/show_virtual_server.py b/src/op_mode/show_virtual_server.py
index 7880edc97..5c6ba4f4e 100755
--- a/src/op_mode/show_virtual_server.py
+++ b/src/op_mode/show_virtual_server.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/show_vpp_interfaces.py b/src/op_mode/show_vpp_interfaces.py
new file mode 100755
index 000000000..64f1eb086
--- /dev/null
+++ b/src/op_mode/show_vpp_interfaces.py
@@ -0,0 +1,263 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import argparse
+import json
+from tabulate import tabulate
+
+from vyos.configquery import ConfigTreeQuery
+from vyos.utils.process import rc_cmd
+
+from vyos.vpp import VPPControl
+from vyos.vpp.utils import (
+ vpp_ifaces_list,
+ vpp_ip_addresses_by_index,
+ vpp_ifaces_stats,
+)
+
+
+def get_iproute_address_list(interface: str) -> list:
+ """Get data from the Linux command 'ip --json address list dev {interface}' and return a list info
+ for the given interface.
+
+ Args:
+ interface (str): Interface name.
+
+ Returns:
+ list: A dictionary containing the JSON data from the 'ip --json address list' command for the specified interface.
+ """
+ rc, out = rc_cmd(f'ip --json address list dev {interface}')
+ if rc:
+ return []
+ return json.loads(out)
+
+
+def get_iproute_link_list(interface):
+ """Get data from the Linux command 'ip --json link show dev {interface}' and return a list info
+ for the given interface.
+
+ Args:
+ interface (str): Interface name.
+
+ Returns:
+ list: A dictionary containing the JSON data from the 'ip --json link show' command for the specified interface.
+ """
+ rc, out = rc_cmd(f'ip --json link list dev {interface}')
+ if rc:
+ return []
+ return json.loads(out)
+
+
+def merge_dicts(*dicts) -> dict:
+ """Merge dictionaries into a new dictionary.
+
+ Args:
+ *dicts: Any number of dictionaries.
+
+ Returns:
+ dict: A new dictionary containing all the key-value pairs from the given dictionaries.
+ """
+ merged = {}
+ for dictionary in dicts:
+ merged.update(dictionary)
+ return merged
+
+
+def show_interfaces(interfaces_list: list) -> str:
+ """Get JSON info from linux and represent it in a table format
+ Use tabulate to generate table
+
+ Interface IP Address Mtu S/L Description
+ --------- ---------- --- --- -----------
+ dum0 203.0.113.1/32 1500 u/u
+ 100.64.1.1/24
+ eth0 192.168.122.14/24 1500 u/u WAN
+
+ :return:
+ """
+ table = []
+ for interface in interfaces_list:
+ # Get the data for the interface
+ ip_address_data = get_iproute_address_list(interface)
+ link_data = get_iproute_link_list(interface)
+
+ # Skip this interface if data is not available
+ if not link_data:
+ continue
+ interface_data = merge_dicts(ip_address_data[0], link_data[0])
+
+ # Get the interface name
+ interface_name = interface_data['ifname']
+
+ # Get the IP addresses and their corresponding prefixes
+ ip_info = [
+ (address['local'], address.get('prefixlen', ''))
+ for address in interface_data['addr_info']
+ ]
+
+ # Format the IP addresses with prefixes and line breaks
+ ip_addresses = '\n'.join(f'{ip}/{prefix}' for ip, prefix in ip_info)
+
+ # Get the MAC address
+ mac = interface_data.get('address', 'n/a')
+
+ # Get the MTU
+ mtu = interface_data.get('mtu')
+
+ # Get the state of the interface
+ state = interface_data['operstate'].lower()
+
+ # Get the description of the interface
+ description = interface_data.get('ifalias', '')
+
+ # Create the list of values for the table
+ values = [interface_name, ip_addresses, mac, mtu, state, description]
+
+ # Append the list of values to the table
+ table.append(values)
+
+ # Print the table with IP addresses listed on separate lines
+ headers = ['Interface', 'IP Address', 'MAC', 'MTU', 'State', 'Description']
+ return tabulate(table, headers=headers, tablefmt='simple')
+
+
+def show_interfaces_dataplane(interfaces_list: list, filter_type: str = 'all') -> str:
+ table = []
+ interface_dp_filter = ('tun', 'tap')
+ lcp_pair_list = vpp.lcp_pairs_list()
+ vpp_name_kernel_to_kernel_name = {
+ entry['vpp_name_kernel']: entry['kernel_name'] for entry in lcp_pair_list
+ }
+ for interface in interfaces_list:
+ interface_name = interface.get('interface_name')
+ if filter_type == 'no_tun_tap' and interface_name.startswith(
+ interface_dp_filter
+ ):
+ continue
+ if filter_type == 'only_tun_tap' and not interface_name.startswith(
+ interface_dp_filter
+ ):
+ continue
+ kernel_name = vpp_name_kernel_to_kernel_name.get(interface_name, '')
+
+ dp_ip_addresses = vpp_ip_addresses_by_index(
+ vpp.api, interface.get('sw_if_index')
+ )
+ dp_ipv6_addresses = vpp_ip_addresses_by_index(
+ vpp.api, interface.get('sw_if_index'), is_ipv6=True
+ )
+ ip_addresses = '\n'.join(dp_ip_addresses + dp_ipv6_addresses)
+
+ mac = str(interface.get('l2_address', 'n/a'))
+ mtu = interface.get('mtu', [])[0]
+ # state
+ flags = interface.get('flags')
+ state = 'up' if flags == 3 else 'down'
+
+ iftype = interface.get('interface_dev_type').split()[0]
+
+ values = [kernel_name, interface_name, iftype, ip_addresses, mac, mtu, state]
+ table.append(values)
+ headers = [
+ 'Kernel',
+ 'Dataplane',
+ 'Type',
+ 'IP Address',
+ 'MAC',
+ 'MTU',
+ 'State',
+ ]
+ table = sorted(table)
+ return tabulate(table, headers=headers, tablefmt='simple')
+
+
+def show_interfaces_hardware(intf_name) -> str:
+ if not intf_name:
+ intf_name = ''
+
+ statistics = vpp_ifaces_stats(intf_name)
+ for intf, stats in sorted(statistics.items()):
+ print(f'\n---------------------------------\nInterface {intf}:\n')
+ table = []
+ for k, v in stats.items():
+ if isinstance(v, dict):
+ for i, j in v.items():
+ table.append([f"{k} {i}", j])
+ else:
+ table.append([k, v])
+ print(tabulate(table, tablefmt="presto"))
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description='Show VPP interfaces')
+ parser.add_argument(
+ '--summary',
+ action='store_true',
+ help='Show summary of VPP interfaces (ethernet and kernel tun)',
+ )
+ parser.add_argument(
+ '--dataplane', action='store_true', help='Show VPP ethernet interfaces'
+ )
+ parser.add_argument(
+ '--kernel', action='store_true', help='Show VPP kernel interfaces'
+ )
+ parser.add_argument(
+ '--iproute', action='store_true', help='Show interfaces (iproute2)'
+ )
+ parser.add_argument(
+ '--hardware',
+ action='store_true',
+ help='Show more detailed statistics for VPP interfaces',
+ )
+ parser.add_argument('--intf-name', action='store', help='Kernel interface name')
+
+ args = parser.parse_args()
+
+ config = ConfigTreeQuery()
+
+ if not config.exists('vpp settings interface'):
+ print('VPP interfaces not configured')
+ exit(0)
+
+ vpp = VPPControl()
+ dp_ifaces_list = vpp_ifaces_list(vpp.api)
+
+ if args.summary:
+ print(show_interfaces_dataplane(dp_ifaces_list, filter_type='all'))
+
+ if args.dataplane:
+ print(show_interfaces_dataplane(dp_ifaces_list, filter_type='no_tun_tap'))
+ exit(0)
+
+ if args.kernel:
+ print(show_interfaces_dataplane(dp_ifaces_list, filter_type='only_tun_tap'))
+
+ if args.iproute:
+ vpp_interfaces = []
+ vpp_ethernet = config.list_nodes('vpp settings interface')
+ vpp_interfaces.extend(vpp_ethernet)
+ if config.exists('interfaces vpp'):
+ for iface_type in config.list_nodes('interfaces vpp'):
+ vpp_kernel_interfaces = config.list_nodes(
+ f'interfaces vpp {iface_type}'
+ )
+ vpp_interfaces.extend(vpp_kernel_interfaces)
+ print(show_interfaces(interfaces_list=vpp_interfaces))
+
+ if args.hardware:
+ show_interfaces_hardware(intf_name=args.intf_name)
diff --git a/src/op_mode/show_wwan.py b/src/op_mode/show_wwan.py
index bd97bb0e5..05e7d0e75 100755
--- a/src/op_mode/show_wwan.py
+++ b/src/op_mode/show_wwan.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/snmp.py b/src/op_mode/snmp.py
index 3d6cd220a..c7dfb51ef 100755
--- a/src/op_mode/snmp.py
+++ b/src/op_mode/snmp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -22,7 +22,7 @@ from vyos.utils.process import call
config_file_daemon = r'/etc/snmp/snmpd.conf'
-parser = argparse.ArgumentParser(description='Retrieve infomration from running SNMP daemon')
+parser = argparse.ArgumentParser(description='Retrieve information from running SNMP daemon')
parser.add_argument('--allowed', action="store_true", help='Show available SNMP communities')
parser.add_argument('--community', action="store", help='Show status of given SNMP community', type=str)
parser.add_argument('--host', action="store", help='SNMP host to connect to', type=str, default='localhost')
diff --git a/src/op_mode/snmp_ifmib.py b/src/op_mode/snmp_ifmib.py
index c71febac9..d733540f5 100755
--- a/src/op_mode/snmp_ifmib.py
+++ b/src/op_mode/snmp_ifmib.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -28,7 +28,7 @@ from vyos.utils.process import popen
parser = argparse.ArgumentParser(description='Retrieve SNMP interfaces information')
parser.add_argument('--ifindex', action='store', nargs='?', const='all', help='Show interface index')
-parser.add_argument('--ifalias', action='store', nargs='?', const='all', help='Show interface aliase')
+parser.add_argument('--ifalias', action='store', nargs='?', const='all', help='Show interface alias')
parser.add_argument('--ifdescr', action='store', nargs='?', const='all', help='Show interface description')
def show_ifindex(intf):
diff --git a/src/op_mode/snmp_v3.py b/src/op_mode/snmp_v3.py
index abeb524dd..94c6691b0 100755
--- a/src/op_mode/snmp_v3.py
+++ b/src/op_mode/snmp_v3.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/ssh.py b/src/op_mode/ssh.py
index 0c51576b0..a4442c301 100755
--- a/src/op_mode/ssh.py
+++ b/src/op_mode/ssh.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright 2017-2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
diff --git a/src/op_mode/storage.py b/src/op_mode/storage.py
index 8fd2ffea1..0f3fabe32 100755
--- a/src/op_mode/storage.py
+++ b/src/op_mode/storage.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -13,10 +13,8 @@
#
# 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 sys
-
import vyos.opmode
from jinja2 import Template
@@ -28,9 +26,6 @@ Used: {{used}} ({{use_percentage}}%)
Available: {{avail}} ({{avail_percentage}}%)
"""
-def _get_formatted_output():
- return _get_system_storage()
-
def show(raw: bool):
from vyos.utils.disk import get_persistent_storage_stats
@@ -49,7 +44,7 @@ def show(raw: bool):
tmpl = Template(output_tmpl)
return tmpl.render(data).strip()
- return output
+ return None
if __name__ == '__main__':
try:
@@ -59,4 +54,3 @@ if __name__ == '__main__':
except (ValueError, vyos.opmode.Error) as e:
print(e)
sys.exit(1)
-
diff --git a/src/op_mode/stp.py b/src/op_mode/stp.py
new file mode 100755
index 000000000..c2a897183
--- /dev/null
+++ b/src/op_mode/stp.py
@@ -0,0 +1,185 @@
+#!/usr/bin/env python3
+#
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# 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 sys
+import typing
+import json
+from tabulate import tabulate
+
+import vyos.opmode
+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_bridge_vlan_data(iface):
+ allowed_vlans = []
+ native_vlan = None
+ vlanData = json.loads(cmd(f"bridge -j -d vlan show"))
+ for vlans in vlanData:
+ if vlans['ifname'] == iface:
+ for allowed in vlans['vlans']:
+ if "flags" in allowed and "PVID" in allowed["flags"]:
+ native_vlan = allowed['vlan']
+ elif allowed.get('vlanEnd', None):
+ allowed_vlans.append(f"{allowed['vlan']}-{allowed['vlanEnd']}")
+ else:
+ allowed_vlans.append(str(allowed['vlan']))
+
+ if not allowed_vlans:
+ allowed_vlans = ["none"]
+ if not native_vlan:
+ native_vlan = "none"
+
+ return ",".join(allowed_vlans), native_vlan
+
+def _get_stp_data(ifname, brInfo, brStatus):
+ tmpInfo = {}
+
+ tmpInfo['bridge_name'] = brInfo.get('ifname')
+ tmpInfo['up_state'] = brInfo.get('operstate')
+ tmpInfo['priority'] = brInfo.get('linkinfo').get('info_data').get('priority')
+ tmpInfo['vlan_filtering'] = "Enabled" if brInfo.get('linkinfo').get('info_data').get('vlan_filtering') == 1 else "Disabled"
+ tmpInfo['vlan_protocol'] = brInfo.get('linkinfo').get('info_data').get('vlan_protocol')
+
+ # The version of VyOS I tested had am issue with the "ip -d link show type bridge"
+ # output. The root_id was always the local bridge, even though the underlying system
+ # understood when it wasn't. Could be an upstream Bug. I pull from the "/sys/class/net"
+ # structure instead. This can be changed later if the "ip link" behavior is corrected.
+
+ #tmpInfo['bridge_id'] = brInfo.get('linkinfo').get('info_data').get('bridge_id')
+ #tmpInfo['root_id'] = brInfo.get('linkinfo').get('info_data').get('root_id')
+
+ tmpInfo['bridge_id'] = cmd(f"cat /sys/class/net/{brInfo.get('ifname')}/bridge/bridge_id").split('.')
+ tmpInfo['root_id'] = cmd(f"cat /sys/class/net/{brInfo.get('ifname')}/bridge/root_id").split('.')
+
+ # The "/sys/class/net" structure stores the IDs without separators like ':' or '.'
+ # This adds a ':' after every 2 characters to make it resemble a MAC Address
+ tmpInfo['bridge_id'][1] = ':'.join(tmpInfo['bridge_id'][1][i:i+2] for i in range(0, len(tmpInfo['bridge_id'][1]), 2))
+ tmpInfo['root_id'][1] = ':'.join(tmpInfo['root_id'][1][i:i+2] for i in range(0, len(tmpInfo['root_id'][1]), 2))
+
+ tmpInfo['stp_state'] = "Enabled" if brInfo.get('linkinfo', {}).get('info_data', {}).get('stp_state') == 1 else "Disabled"
+
+ # I don't call any of these values, but I created them to be called within raw output if desired
+
+ tmpInfo['mcast_snooping'] = "Enabled" if brInfo.get('linkinfo').get('info_data').get('mcast_snooping') == 1 else "Disabled"
+ tmpInfo['rxbytes'] = brInfo.get('stats64').get('rx').get('bytes')
+ tmpInfo['rxpackets'] = brInfo.get('stats64').get('rx').get('packets')
+ tmpInfo['rxerrors'] = brInfo.get('stats64').get('rx').get('errors')
+ tmpInfo['rxdropped'] = brInfo.get('stats64').get('rx').get('dropped')
+ tmpInfo['rxover_errors'] = brInfo.get('stats64').get('rx').get('over_errors')
+ tmpInfo['rxmulticast'] = brInfo.get('stats64').get('rx').get('multicast')
+ tmpInfo['txbytes'] = brInfo.get('stats64').get('tx').get('bytes')
+ tmpInfo['txpackets'] = brInfo.get('stats64').get('tx').get('packets')
+ tmpInfo['txerrors'] = brInfo.get('stats64').get('tx').get('errors')
+ tmpInfo['txdropped'] = brInfo.get('stats64').get('tx').get('dropped')
+ tmpInfo['txcarrier_errors'] = brInfo.get('stats64').get('tx').get('carrier_errors')
+ tmpInfo['txcollosions'] = brInfo.get('stats64').get('tx').get('collisions')
+
+ tmpStatus = []
+ for members in brStatus:
+ if members.get('master') == brInfo.get('ifname'):
+ allowed_vlans, native_vlan = _get_bridge_vlan_data(members['ifname'])
+ tmpStatus.append({'interface': members.get('ifname'),
+ 'state': members.get('state').capitalize(),
+ 'mtu': members.get('mtu'),
+ 'pathcost': members.get('cost'),
+ 'bpduguard': "Enabled" if members.get('guard') == True else "Disabled",
+ 'rootguard': "Enabled" if members.get('root_block') == True else "Disabled",
+ 'mac_learning': "Enabled" if members.get('learning') == True else "Disabled",
+ 'neigh_suppress': "Enabled" if members.get('neigh_suppress') == True else "Disabled",
+ 'vlan_tunnel': "Enabled" if members.get('vlan_tunnel') == True else "Disabled",
+ 'isolated': "Enabled" if members.get('isolated') == True else "Disabled",
+ **({'allowed_vlans': allowed_vlans} if allowed_vlans else {}),
+ **({'native_vlan': native_vlan} if native_vlan else {})})
+
+ tmpInfo['members'] = tmpStatus
+ return tmpInfo
+
+def show_stp(raw: bool, ifname: typing.Optional[str], detail: bool):
+ rawList = []
+ rawDict = {'stp': []}
+
+ if ifname:
+ if not interface_exists(ifname):
+ raise vyos.opmode.Error(f"{ifname} does not exist!")
+ else:
+ ifname = ""
+
+ bridgeInfo = json.loads(cmd(f"ip -j -d -s link show type bridge {ifname}"))
+
+ if not bridgeInfo:
+ raise vyos.opmode.Error(f"No Bridges configured!")
+
+ bridgeStatus = json.loads(cmd(f"bridge -j -s -d link show"))
+
+ for bridges in bridgeInfo:
+ output_list = []
+ amRoot = ""
+ bridgeDict = _get_stp_data(ifname, bridges, bridgeStatus)
+
+ if bridgeDict['bridge_id'][1] == bridgeDict['root_id'][1]:
+ amRoot = " (This bridge is the root)"
+
+ print('-' * 80)
+ print(f"Bridge interface {bridgeDict['bridge_name']} ({bridgeDict['up_state']}):\n")
+ print(f"Spanning Tree is {bridgeDict['stp_state']}")
+ print(f"Bridge ID {bridgeDict['bridge_id'][1]}, Priority {int(bridgeDict['bridge_id'][0], 16)}")
+ print(f"Root ID {bridgeDict['root_id'][1]}, Priority {int(bridgeDict['root_id'][0], 16)}{amRoot}")
+ print(f"VLANs {bridgeDict['vlan_filtering'].capitalize()}, Protocol {bridgeDict['vlan_protocol']}")
+ print()
+
+ for members in bridgeDict['members']:
+ output_list.append([members['interface'],
+ members['state'],
+ *([members['pathcost']] if detail else []),
+ members['bpduguard'],
+ members['rootguard'],
+ members['mac_learning'],
+ *([members['neigh_suppress']] if detail else []),
+ *([members['vlan_tunnel']] if detail else []),
+ *([members['isolated']] if detail else []),
+ *([members['allowed_vlans']] if detail else []),
+ *([members['native_vlan']] if detail else [])])
+
+ if raw:
+ rawList.append(bridgeDict)
+ elif detail:
+ headers = ['Interface', 'State', 'Pathcost', 'BPDU_Guard', 'Root_Guard', 'Learning', 'Neighbor_Suppression', 'Q-in-Q', 'Port_Isolation', 'Allowed VLANs', 'Native VLAN']
+ detailed_output(output_list, headers)
+ else:
+ headers = ['Interface', 'State', 'BPDU_Guard', 'Root_Guard', 'Learning']
+ print(tabulate(output_list, headers))
+ print()
+
+ if raw:
+ rawDict['stp'] = rawList
+ return rawDict
+
+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/system.py b/src/op_mode/system.py
index 854b4b699..6d0815a4a 100755
--- a/src/op_mode/system.py
+++ b/src/op_mode/system.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/tcpdump.py b/src/op_mode/tcpdump.py
index 607b59603..351b3d520 100644
--- a/src/op_mode/tcpdump.py
+++ b/src/op_mode/tcpdump.py
@@ -1,6 +1,6 @@
#! /usr/bin/env python3
-# Copyright (C) 2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -16,6 +16,7 @@
import sys
+from vyos.utils.io import catch_broken_pipe
from vyos.utils.process import call
options = {
@@ -51,8 +52,6 @@ options = {
},
}
-tcpdump = 'sudo /usr/bin/tcpdump'
-
class List(list):
def first(self):
return self.pop(0) if self else ''
@@ -92,7 +91,8 @@ def complete(prefix):
return [o for o in options if o.startswith(prefix)]
-def convert(command, args):
+def convert(args):
+ command = 'sudo /usr/bin/tcpdump'
while args:
shortname = args.first()
longnames = complete(shortname)
@@ -109,6 +109,9 @@ def convert(command, args):
command=command, value=args.first())
return command
+@catch_broken_pipe
+def run_tcpdump(command: str, ifname: str) -> None:
+ call(f'{command} -i {ifname}')
if __name__ == '__main__':
args = List(sys.argv[1:])
@@ -161,5 +164,4 @@ if __name__ == '__main__':
sys.stdout.write(helplines)
sys.exit(0)
- command = convert(tcpdump, args)
- call(f'{command} -i {ifname}')
+ run_tcpdump(convert(args), ifname)
diff --git a/src/op_mode/tech_support.py b/src/op_mode/tech_support.py
index 24ac0af1b..6055cbf15 100644
--- a/src/op_mode/tech_support.py
+++ b/src/op_mode/tech_support.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -20,6 +20,7 @@ import json
import vyos.opmode
from vyos.utils.process import cmd
+from vyos.base import Warning
def _get_version_data():
from vyos.version import get_version_data
@@ -51,7 +52,12 @@ def _get_storage():
def _get_devices():
devices = {}
devices["pci"] = cmd("lspci")
- devices["usb"] = cmd("lsusb")
+
+ try:
+ devices["usb"] = cmd("lsusb")
+ except OSError:
+ Warning("Could not retrieve information about USB devices")
+ devices["usb"] = {}
return devices
diff --git a/src/op_mode/toggle_help_binding.sh b/src/op_mode/toggle_help_binding.sh
index a8708f3da..7c8bc05ce 100755
--- a/src/op_mode/toggle_help_binding.sh
+++ b/src/op_mode/toggle_help_binding.sh
@@ -1,6 +1,6 @@
#!/bin/bash
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/traceroute.py b/src/op_mode/traceroute.py
index d2bac3f7c..ef67b7416 100755
--- a/src/op_mode/traceroute.py
+++ b/src/op_mode/traceroute.py
@@ -1,6 +1,6 @@
#! /usr/bin/env python3
-# Copyright (C) 2022 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -85,7 +85,7 @@ options = {
'help': 'Use TCP SYN for tracerouting (default port is 80)'
},
'tos': {
- 'traceroute': '{commad} -t {value}',
+ 'traceroute': '{command} -t {value}',
'type': '<tos>',
'help': 'Mark packets with specified TOS'
},
@@ -222,11 +222,23 @@ if __name__ == '__main__':
args.append(name)
args.append(option['dflt'])
+ af = socket.AF_UNSPEC
+ for i in range(len(args)):
+ matched = complete(args[i])
+ if len(matched) == 1 and matched[0] == 'source-address' and i + 1 < len(args):
+ try:
+ src_version = ipaddress.ip_address(args[i + 1]).version
+ af = socket.AF_INET6 if src_version == 6 else socket.AF_INET
+ except ValueError:
+ pass
+ break
+
try:
- ip = socket.gethostbyname(host)
+ info = socket.getaddrinfo(host, None, af, socket.SOCK_STREAM)
+ ip = info[0][4][0]
except UnicodeError:
- sys.exit(f'tracroute: Unknown host: {host}')
- except socket.gaierror:
+ sys.exit(f'traceroute: Unknown host: {host}')
+ except OSError:
ip = host
try:
diff --git a/src/op_mode/update_suricata.sh b/src/op_mode/update_suricata.sh
new file mode 100755
index 000000000..6e4e605f4
--- /dev/null
+++ b/src/op_mode/update_suricata.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+if test -f /run/suricata/suricata.yaml; then
+ suricata-update --suricata-conf /run/suricata/suricata.yaml;
+ systemctl restart suricata;
+else
+ echo "Service Suricata not configured";
+fi
diff --git a/src/op_mode/uptime.py b/src/op_mode/uptime.py
index 1c1a149ec..90475cb50 100755
--- a/src/op_mode/uptime.py
+++ b/src/op_mode/uptime.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
diff --git a/src/op_mode/version.py b/src/op_mode/version.py
index 71a40dd50..b93e3081b 100755
--- a/src/op_mode/version.py
+++ b/src/op_mode/version.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2016-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/vpn_ike_sa.py b/src/op_mode/vpn_ike_sa.py
index 9385bcd0c..0cd192174 100755
--- a/src/op_mode/vpn_ike_sa.py
+++ b/src/op_mode/vpn_ike_sa.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/vpn_ipsec.py b/src/op_mode/vpn_ipsec.py
index ef89e605f..3d7049a14 100755
--- a/src/op_mode/vpn_ipsec.py
+++ b/src/op_mode/vpn_ipsec.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2022 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -23,13 +23,13 @@ SWANCTL_CONF = '/etc/swanctl/swanctl.conf'
def get_peer_connections(peer, tunnel, return_all = False):
- search = rf'^[\s]*(peer_{peer}_(tunnel_[\d]+|vti)).*'
+ search = rf'^[\s]*({peer}-(tunnel-[\d]+|vti))[\s]*{{'
matches = []
with open(SWANCTL_CONF, 'r') as f:
for line in f.readlines():
result = re.match(search, line)
if result:
- suffix = f'tunnel_{tunnel}' if tunnel.isnumeric() else tunnel
+ suffix = f'tunnel-{tunnel}' if tunnel.isnumeric() else tunnel
if return_all or (result[2] == suffix):
matches.append(result[1])
return matches
@@ -66,7 +66,8 @@ def debug_peer(peer, tunnel):
return
for conn in conns:
- call(f'/usr/sbin/ipsec statusall | grep {conn}')
+ command = f'/usr/sbin/ipsec statusall | grep {conn}'
+ call(command)
if __name__ == '__main__':
diff --git a/src/op_mode/vpp.py b/src/op_mode/vpp.py
new file mode 100755
index 000000000..80697f503
--- /dev/null
+++ b/src/op_mode/vpp.py
@@ -0,0 +1,525 @@
+#!/usr/bin/env python3
+#
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# 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 sys
+import json
+import typing
+
+from tabulate import tabulate
+from vyos.vpp import VPPControl
+from vyos.vpp.utils import vpp_iface_name_transform
+from vyos.configquery import ConfigTreeQuery
+import vyos.opmode
+
+NO_INDEX = 0xFFFFFFFF
+
+class VPPShow:
+ RX_STATES = {
+ 0: 'INITIALIZE',
+ 1: 'PORT_DISABLED',
+ 2: 'EXPIRED',
+ 3: 'LACP_DISABLED',
+ 4: 'DEFAULTED',
+ 5: 'CURRENT',
+ }
+ TX_STATES = {0: 'TRANSMIT'}
+ MUX_STATES = {
+ 0: 'DETACHED',
+ 1: 'WAITING',
+ 2: 'ATTACHED',
+ 3: 'COLLECTING_DISTRIBUTING',
+ }
+ PTX_STATES = {0: 'NO_PERIODIC', 1: 'FAST', 2: 'SLOW', 3: 'PERIODIC_TX'}
+ BOND_MODE = {
+ 1: 'round-robin',
+ 2: 'active-backup',
+ 3: 'xor',
+ 4: 'broadcast',
+ 5: 'lacp',
+ }
+ BOND_LB = {
+ 0: 'layer2',
+ 1: 'layer3+4',
+ 2: 'layer2+3',
+ 3: 'round-robin',
+ 4: 'broadcast',
+ 5: 'active-backup',
+ }
+
+ def __init__(self):
+ self.config = ConfigTreeQuery()
+ self.vpp = VPPControl()
+
+ # -----------------------------
+ # IPFIX Interfaces
+ # -----------------------------
+ def _get_ipfix_interfaces_raw(self) -> typing.List[dict]:
+ interfaces = self.vpp.api.flowprobe_interface_dump()
+ index_map = {
+ i.sw_if_index: i.interface_name for i in self.vpp.api.sw_interface_dump()
+ }
+
+ return [
+ {
+ 'interface': index_map.get(e.sw_if_index, f'if{e.sw_if_index}'),
+ 'sw_if_index': e.sw_if_index,
+ 'which': e.which.name.replace('FLOWPROBE_WHICH_', '').lower(),
+ 'direction': e.direction.name.replace(
+ 'FLOWPROBE_DIRECTION_', ''
+ ).lower(),
+ }
+ for e in interfaces
+ ]
+
+ def _show_ipfix_interfaces_formatted(self, data: typing.List[dict]) -> str:
+ if not data:
+ return 'No flowprobe interfaces configured.'
+ table_data = [
+ {
+ 'Interface': d['interface'],
+ 'VppIfIndex': d['sw_if_index'],
+ 'Flow-variant': d['which'],
+ 'Direction': d['direction'],
+ }
+ for d in data
+ ]
+ return tabulate(table_data, headers='keys', tablefmt='simple')
+
+ def ipfix_interfaces(self, raw: bool):
+ base = ['vpp', 'ipfix', 'interface']
+ if not self.config.exists(base):
+ raise vyos.opmode.UnconfiguredSubsystem(
+ 'vpp ipfix interface is not configured'
+ )
+
+ data = self._get_ipfix_interfaces_raw()
+ return data if raw else self._show_ipfix_interfaces_formatted(data)
+
+ # -----------------------------
+ # IPFIX Collectors
+ # -----------------------------
+ def _get_ipfix_collectors_raw(self) -> typing.List[dict]:
+ _, collectors = self.vpp.api.ipfix_all_exporter_get()
+ return [
+ {
+ 'collector_address': str(c.collector_address),
+ 'collector_port': c.collector_port,
+ 'src_address': str(c.src_address),
+ 'vrf_id': c.vrf_id,
+ 'path_mtu': c.path_mtu,
+ 'template_interval': c.template_interval,
+ 'udp_checksum': bool(c.udp_checksum),
+ }
+ for c in collectors
+ ]
+
+ def _show_ipfix_collectors_formatted(self, data: typing.List[dict]) -> str:
+ if not data:
+ return 'No IPFIX collectors configured.'
+ table_data = [
+ {
+ 'Collector': f"{d['collector_address']}:{d['collector_port']}",
+ 'Source': d['src_address'],
+ 'VRF': d['vrf_id'],
+ 'MTU': d['path_mtu'],
+ 'Template Intvl': d['template_interval'],
+ 'UDP Cksum': 'on' if d['udp_checksum'] else 'off',
+ }
+ for d in data
+ ]
+ return tabulate(table_data, headers='keys', tablefmt='simple')
+
+ def ipfix_collectors(self, raw: bool):
+ base = ['vpp', 'ipfix', 'collector']
+ if not self.config.exists(base):
+ raise vyos.opmode.UnconfiguredSubsystem(
+ 'vpp ipfix collector is not configured'
+ )
+
+ data = self._get_ipfix_collectors_raw()
+ return data if raw else self._show_ipfix_collectors_formatted(data)
+
+ # -----------------------------
+ # IPFIX table
+ # -----------------------------
+ def _get_ipfix_table_raw(self):
+ # VPP does not have API call to get this data
+ data = self.vpp.cli_cmd('show flowprobe table')
+ return [data.reply]
+
+ def _show_ipfix_table_formatted(self) -> str:
+ data = self.vpp.cli_cmd('show flowprobe table')
+ return data.reply
+
+ def ipfix_table(self, raw: bool):
+ base = ['vpp', 'ipfix', 'collector']
+ if not self.config.exists(base):
+ raise vyos.opmode.UnconfiguredSubsystem(
+ 'vpp ipfix collector is not configured'
+ )
+
+ data = self._get_ipfix_table_raw()
+ return data if raw else self._show_ipfix_table_formatted()
+
+ # -----------------------------
+ # Bonding information
+ # -----------------------------
+ def _get_raw_output(self, data_dump: typing.List[dict]) -> list[dict]:
+ data = [json.loads(json.dumps(d._asdict(), default=str)) for d in data_dump]
+ return data
+
+ def _get_lacp_raw(self, ifname: typing.Optional[str]) -> list[dict]:
+ lacp_dump = self.vpp.api.sw_interface_lacp_dump()
+ data = self._get_raw_output(lacp_dump)
+
+ if ifname:
+ res = next((d for d in data if d['interface_name'] == ifname), None)
+ if not res:
+ raise vyos.opmode.IncorrectValue(
+ f'Interface {ifname} is not a member of any LACP bond'
+ )
+ data = [res]
+
+ return data
+
+ def _get_lacp_info_formatted(self, data):
+
+ def bit(x, n):
+ return (x >> n) & 1
+
+ def bits_to_str(x):
+ return ' '.join(f'{bit(x, n):3d}' for n in range(7, -1, -1))
+
+ # Headers (exactly like VPP)
+ print(f'{"":55} {"actor state":32} {"partner state":32}')
+ print(
+ 'interface name'.ljust(26)
+ + 'sw_if_index'.ljust(13)
+ + 'bond interface'.ljust(17)
+ + 'exp/def/dis/col/syn/agg/tim/act'.ljust(33)
+ + 'exp/def/dis/col/syn/agg/tim/act'.ljust(32)
+ )
+
+ for d in data:
+ iface = d['interface_name']
+ sw_if = str(d['sw_if_index'])
+ bond_if = d['bond_interface_name']
+ actor_bits = bits_to_str(d['actor_state'])
+ partner_bits = bits_to_str(d['partner_state'])
+
+ print(
+ f'{iface:25} {sw_if:12} {bond_if:16} {actor_bits:32} {partner_bits:32}'
+ )
+
+ # LAG ID formatting
+ lag_line = (
+ f' LAG ID: '
+ f'[({d["actor_system_priority"]:04x},{d["actor_system"].replace(":", "-")},'
+ f'{d["actor_key"]:04x},{d["actor_port_priority"]:04x},{d["actor_port_number"]:04x}), '
+ f'({d["partner_system_priority"]:04x},{d["partner_system"].replace(":", "-")},'
+ f'{d["partner_key"]:04x},{d["partner_port_priority"]:04x},{d["partner_port_number"]:04x})]'
+ )
+ print(lag_line)
+
+ # State machine line
+ print(
+ f' RX-state: {self.RX_STATES[d["rx_state"]]}, '
+ f'TX-state: {self.TX_STATES[d["tx_state"]]}, '
+ f'MUX-state: {self.MUX_STATES[d["mux_state"]]}, '
+ f'PTX-state: {self.PTX_STATES[d["ptx_state"]]}'
+ )
+
+ def _get_bond_raw(self, index: typing.Optional[str]) -> list[dict]:
+ bond_dump = self.vpp.api.sw_bond_interface_dump(sw_if_index=index)
+
+ result = []
+ for bond in bond_dump:
+ bond_info = {
+ 'interface_name': bond.interface_name,
+ 'sw_if_index': bond.sw_if_index,
+ 'mode': self.BOND_MODE[bond.mode],
+ 'hash_policy': self.BOND_LB[bond.lb],
+ 'active_members': bond.active_members,
+ 'members': {},
+ }
+ members = self.vpp.api.sw_member_interface_dump(
+ sw_if_index=bond.sw_if_index
+ )
+ for member in members:
+ bond_info['members'][member.interface_name] = {
+ 'sw_if_index': member.sw_if_index,
+ 'is_passive': member.is_passive,
+ 'is_long_timeout': member.is_long_timeout,
+ 'is_local_numa': member.is_local_numa,
+ 'weight': member.weight,
+ }
+ result.append(bond_info)
+
+ return result
+
+ def _show_bond_info_formatted(self, data: typing.List[dict]) -> str:
+ table_data = [
+ {
+ 'Interface': d['interface_name'],
+ 'Mode': d['mode'],
+ 'Hash': d['hash_policy'],
+ 'Members': '\n'.join(sorted(d['members'].keys())),
+ 'Active members': d['active_members'],
+ }
+ for d in data
+ ]
+ return tabulate(table_data, headers='keys', tablefmt='simple', numalign='left')
+
+ def lacp_info(self, raw: bool, ifname: typing.Optional[str]):
+ data = self._get_lacp_raw(ifname)
+
+ if not data:
+ raise vyos.opmode.DataUnavailable(
+ 'No VPP interface is configured with LACP (802.3ad) mode'
+ )
+
+ if raw:
+ return data
+
+ return self._get_lacp_info_formatted(data)
+
+ def lacp_details(self, raw: bool, ifname: typing.Optional[str]) -> str:
+ # Check if interface is a part of any LACP bond
+ self._get_lacp_raw(ifname)
+
+ # VPP does not have API call to get this data
+ cmd_command = f'show lacp{f" {ifname}" if ifname else ""} details'
+ data = self.vpp.cli_cmd(cmd_command)
+
+ if raw:
+ return [data.reply]
+
+ return data.reply
+
+ def bond_info(self, raw: bool, ifname: typing.Optional[str]) -> str:
+ index = NO_INDEX
+ if ifname:
+ if not ifname.startswith('vppbond') or not ifname[7:].isdigit():
+ raise vyos.opmode.IncorrectValue(
+ f'"{ifname}" is not a valid bonding interface name (expected vppbondN)'
+ )
+
+ ifname_vpp = vpp_iface_name_transform(ifname)
+ index = self.vpp.get_sw_if_index(ifname_vpp)
+ if index is None:
+ raise vyos.opmode.IncorrectValue(
+ f'Bonding interface {ifname} does not exist in VPP'
+ )
+
+ data = self._get_bond_raw(index)
+
+ return data if raw else self._show_bond_info_formatted(data)
+
+ def bond_details(self, raw: bool) -> str:
+ # VPP API call is not so informative -> use CLI command
+ cmd_command = 'show bond details'
+ data = self.vpp.cli_cmd(cmd_command)
+ return [data.reply] if raw else data.reply
+
+ # -----------------------------
+ # Bridge-domain information
+ # -----------------------------
+ def _parse_bridge_id(self, ifname: typing.Optional[str]) -> typing.Optional[int]:
+ if ifname is None:
+ return None
+
+ if not ifname.startswith('vppbr') and not ifname[5:].isdigit():
+ raise vyos.opmode.IncorrectValue(
+ f'"{ifname}" is not a valid bridge interface name (expected vppbrN)'
+ )
+
+ if not self.config.exists(['interfaces', 'vpp', 'bridge', ifname]):
+ raise vyos.opmode.IncorrectValue(
+ f'Bridge interface {ifname} does not exist'
+ )
+
+ return int(ifname[5:])
+
+ def _get_bridge_domain_raw(
+ self, bd_id: typing.Optional[int] = None
+ ) -> typing.List[dict]:
+ # Dump bridge domains
+ domains = self.vpp.api.bridge_domain_dump(
+ bd_id=bd_id if bd_id is not None else NO_INDEX
+ )
+
+ result = []
+ for d in domains:
+ domain_info = {
+ 'bd_id': d.bd_id,
+ 'learning': bool(d.learn),
+ 'forward': bool(d.forward),
+ 'uu_flood': bool(d.uu_flood),
+ 'flood': bool(d.flood),
+ 'arp_term': bool(d.arp_term),
+ 'arp_ufwd': bool(d.arp_ufwd),
+ 'mac_age': d.mac_age,
+ 'bvi_interface': d.bvi_sw_if_index,
+ 'n_sw_ifs': d.n_sw_ifs,
+ 'members': [
+ {
+ 'ifname': self.vpp.get_interface_name(m.sw_if_index),
+ 'sw_if_index': m.sw_if_index,
+ 'shg': m.shg,
+ }
+ for m in d.sw_if_details
+ ],
+ }
+ result.append(domain_info)
+
+ result.sort(key=lambda x: x['bd_id'])
+
+ return result
+
+ def _show_bridge_domain_formatted(self, data: typing.List[dict]) -> str:
+ if not data:
+ return 'No bridge domains configured.'
+
+ table_data = [
+ {
+ 'BD-ID': d['bd_id'],
+ 'Age(min)': 'off' if d['mac_age'] == 0 else d['mac_age'],
+ 'Learning': 'on' if d['learning'] else 'off',
+ 'U-Forwrd': 'on' if d['forward'] else 'off',
+ 'UU-Flood': 'flood' if d['uu_flood'] else 'drop',
+ 'Flooding': 'on' if d['flood'] else 'off',
+ 'ARP-Term': 'on' if d['arp_term'] else 'off',
+ 'arp-ufwd': 'on' if d['arp_ufwd'] else 'off',
+ 'BVI-Intf': (
+ self.vpp.get_interface_name(d['bvi_interface'])
+ if d['bvi_interface'] != NO_INDEX
+ else 'N/A'
+ ),
+ }
+ for d in data
+ ]
+ return tabulate(table_data, headers='keys', tablefmt='simple', numalign='left')
+
+ def bridge_domain(self, raw: bool, ifname: typing.Optional[str] = None):
+ bd_id = self._parse_bridge_id(ifname)
+ data = self._get_bridge_domain_raw(bd_id)
+ return data if raw else self._show_bridge_domain_formatted(data)
+
+ def bridge_domain_details(self, raw: bool, ifname: typing.List):
+ bd_id = self._parse_bridge_id(ifname)
+
+ # VPP API call is not so informative -> use CLI command
+ cmd_command = f'show bridge-domain {bd_id} detail'
+ data = self.vpp.cli_cmd(cmd_command)
+
+ if raw:
+ return [data.reply]
+
+ return data.reply
+
+ # -----------------------------
+ # Runtime table
+ # -----------------------------
+ def _get_runtime_raw(self):
+ # VPP does not have API call to get this data
+ data = self.vpp.cli_cmd('show runtime')
+ return [data.reply]
+
+ def _show_runtime_formatted(self) -> str:
+ data = self.vpp.cli_cmd('show runtime')
+ return data.reply
+
+ def runtime(self, raw: bool):
+ data = self._get_runtime_raw()
+ return data if raw else self._show_runtime_formatted()
+
+ # -----------------------------
+ # Interfaces mode
+ # -----------------------------
+ def mode(self, raw: bool):
+ # VPP does not have API call to get this data
+ data = self.vpp.cli_cmd('show mode')
+ return [data.reply] if raw else data.reply
+
+
+# -----------------------------
+# VyOS IPFIX op-mode entries
+# -----------------------------
+@vyos.opmode.verify_cli_exists(['vpp', 'ipfix', 'interface'])
+def show_ipfix_interfaces(raw: bool):
+ return VPPShow().ipfix_interfaces(raw)
+
+@vyos.opmode.verify_cli_exists(['vpp', 'ipfix', 'collector'])
+def show_ipfix_collectors(raw: bool):
+ return VPPShow().ipfix_collectors(raw)
+
+@vyos.opmode.verify_cli_exists(['vpp', 'ipfix'])
+def show_ipfix_table(raw: bool):
+ return VPPShow().ipfix_table(raw)
+
+# -----------------------------
+# VPP Bonding information
+# -----------------------------
+@vyos.opmode.verify_cli_exists(['interfaces', 'vpp', 'bonding'])
+def show_lacp(raw: bool, ifname: typing.Optional[str]):
+ return VPPShow().lacp_info(raw, ifname)
+
+@vyos.opmode.verify_cli_exists(['interfaces', 'vpp', 'bonding'])
+def show_lacp_details(raw: bool, ifname: typing.Optional[str]):
+ return VPPShow().lacp_details(raw, ifname)
+
+@vyos.opmode.verify_cli_exists(['interfaces', 'vpp', 'bonding'])
+def show_bond(raw: bool, ifname: typing.Optional[str]):
+ return VPPShow().bond_info(raw, ifname)
+
+@vyos.opmode.verify_cli_exists(['interfaces', 'vpp', 'bonding'])
+def show_bond_details(raw: bool):
+ return VPPShow().bond_details(raw)
+
+# -----------------------------
+# Bridge op-mode entry
+# -----------------------------
+@vyos.opmode.verify_cli_exists(['interfaces', 'vpp', 'bridge'])
+def show_bridge(raw: bool, ifname: typing.Optional[str] = None):
+ return VPPShow().bridge_domain(raw, ifname)
+
+@vyos.opmode.verify_cli_exists(['interfaces', 'vpp', 'bridge'])
+def show_bridge_details(raw: bool, ifname: typing.Optional[str] = None):
+ return VPPShow().bridge_domain_details(raw, ifname)
+
+# -----------------------------
+# show runtime
+# -----------------------------
+@vyos.opmode.verify_cli_exists(['vpp'])
+def show_runtime(raw: bool):
+ return VPPShow().runtime(raw)
+
+# -----------------------------
+# show mode
+# -----------------------------
+@vyos.opmode.verify_cli_exists(['vpp'])
+def show_mode(raw: bool):
+ return VPPShow().mode(raw)
+
+
+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/vpp_acl.py b/src/op_mode/vpp_acl.py
new file mode 100644
index 000000000..6d5daba06
--- /dev/null
+++ b/src/op_mode/vpp_acl.py
@@ -0,0 +1,342 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import json
+import sys
+import typing
+from tabulate import tabulate
+
+import vyos.opmode
+from vyos.config import Config
+from vyos.configquery import ConfigTreeQuery
+
+from vyos.vpp import VPPControl
+
+
+NO_ACL_INDEX = 0xFFFFFFFF
+
+# ACL action flags
+action_map = {
+ 0: 'deny',
+ 1: 'permit',
+ 2: 'permit-reflect',
+}
+
+# TCP flag names to bit values
+TCP_FLAGS = {
+ 'FIN': 0x01,
+ 'SYN': 0x02,
+ 'RST': 0x04,
+ 'PSH': 0x08,
+ 'ACK': 0x10,
+ 'URG': 0x20,
+ 'ECN': 0x40,
+ 'CWR': 0x80,
+}
+
+
+def _verify(target):
+ """Decorator checks if config for VPP NAT CGNAT exists"""
+ from functools import wraps
+
+ if target not in ['ip', 'mac', 'no_target']:
+ raise ValueError('Invalid target')
+
+ def _verify_target(func):
+ @wraps(func)
+ def _wrapper(*args, **kwargs):
+ config = ConfigTreeQuery()
+ path = 'vpp acl'
+ if target == 'ip':
+ path += ' ip'
+ elif target == 'mac':
+ path += ' mac'
+ if not config.exists(path):
+ raise vyos.opmode.UnconfiguredSubsystem(f'"{path}" is not configured')
+ return func(*args, **kwargs)
+
+ return _wrapper
+
+ return _verify_target
+
+
+def _get_acl_tag_by_index(vpp, acl_index):
+ acl = vpp.api.acl_dump(acl_index=acl_index)
+ if acl:
+ return acl[0].tag
+
+ return None
+
+
+def _get_mac_acl_tag_by_index(vpp, acl_index):
+ acl = vpp.api.macip_acl_dump(acl_index=acl_index)
+ if acl:
+ return acl[0].tag
+
+ return None
+
+
+def _get_tcp_flag_states(value, mask):
+ set_flags = []
+ unset_flags = []
+ for flag, bit in TCP_FLAGS.items():
+ if mask & bit: # This flag is being checked
+ if value & bit:
+ set_flags.append(flag)
+ else:
+ unset_flags.append(flag)
+ return sorted(set_flags), sorted(unset_flags)
+
+
+def _get_raw_output_acls(data_dump):
+ out = []
+ for data in data_dump:
+ rules = [json.loads(json.dumps(d._asdict(), default=str)) for d in data.r]
+ out.append(
+ {
+ 'acl_index': data.acl_index,
+ 'tag': data.tag,
+ 'count': data.count,
+ 'r': rules,
+ }
+ )
+ return out
+
+
+def _get_raw_output_interfaces(data_dump):
+ ifaces_list = []
+ for iface in data_dump:
+ if iface.count != 0:
+ ifaces_list.append(json.loads(json.dumps(iface._asdict(), default=str)))
+ return ifaces_list
+
+
+def _get_formatted_output_interfaces(vpp, interfaces):
+ data_entries = []
+ for interface in interfaces:
+ name = vpp.get_interface_name(interface.get('sw_if_index'))
+ input_acls = []
+ for acl_index in interface.get('acls')[: interface.get('n_input')]:
+ input_acls.append(_get_acl_tag_by_index(vpp, int(acl_index)))
+ output_acls = []
+ for acl_index in interface.get('acls')[interface.get('n_input') :]:
+ output_acls.append(_get_acl_tag_by_index(vpp, int(acl_index)))
+ values = [
+ name,
+ '\n'.join(input_acls),
+ '\n'.join(output_acls),
+ ]
+ data_entries.append(values)
+
+ headers = ['Interface', 'Input ACLs', 'Output ACLs']
+ return tabulate(data_entries, headers=headers, tablefmt='simple')
+
+
+def _get_formatted_output_mac_interfaces(vpp, interfaces):
+ data_entries = []
+ for interface in interfaces:
+ name = vpp.get_interface_name(interface.get('sw_if_index'))
+ acl = _get_mac_acl_tag_by_index(vpp, int(interface.get('acls')[0]))
+ data_entries.append([name, acl])
+
+ headers = ['Interface', 'ACL']
+ return tabulate(data_entries, headers=headers, tablefmt='simple')
+
+
+def _get_formatted_output_acls(acls_list):
+ conf = Config()
+
+ for acl in acls_list:
+ acl_index = acl.get('acl_index')
+ tag = acl.get('tag')
+ rules = acl.get('r')
+ print(
+ '\n---------------------------------\n'
+ f'IP ACL "tag-name {tag}" acl_index {acl_index}\n'
+ )
+
+ path = ['vpp', 'acl', 'ip', 'tag-name', tag, 'rule']
+ conf_rules = conf.list_nodes(path)
+ data_entries = []
+ for rule_index, rule in enumerate(rules):
+ srcport_first = str(rule.get('srcport_or_icmptype_first'))
+ srcport_last = str(rule.get('srcport_or_icmptype_last'))
+ dstport_first = str(rule.get('dstport_or_icmpcode_first'))
+ dstport_last = str(rule.get('dstport_or_icmpcode_last'))
+ set_flags, unset_flags = _get_tcp_flag_states(
+ rule.get('tcp_flags_value'), rule.get('tcp_flags_mask')
+ )
+
+ values = [
+ conf_rules[rule_index],
+ action_map.get(rule.get('is_permit')),
+ rule.get('src_prefix'),
+ (
+ f'{srcport_first}-{srcport_last}'
+ if srcport_first != srcport_last
+ else srcport_first
+ ),
+ rule.get('dst_prefix'),
+ (
+ f'{dstport_first}-{dstport_last}'
+ if dstport_first != dstport_last
+ else dstport_first
+ ),
+ rule.get('proto'),
+ '\n'.join(set_flags),
+ '\n'.join(unset_flags),
+ ]
+ data_entries.append(values)
+
+ headers = [
+ 'Rule',
+ 'Action',
+ 'Src prefix',
+ 'Src port',
+ 'Dst prefix',
+ 'Dst port',
+ 'Proto',
+ 'TCP flags set',
+ 'TCP flags not set',
+ ]
+ print(tabulate(data_entries, headers=headers, tablefmt='simple'))
+ print('\n')
+
+
+def _get_formatted_output_mac_acls(acls_list):
+ conf = Config()
+
+ for acl in acls_list:
+ acl_index = acl.get('acl_index')
+ tag = acl.get('tag')
+ rules = acl.get('r')
+ print(
+ '\n---------------------------------\n'
+ f'MACIP ACL "tag-name {tag}" acl_index {acl_index}\n'
+ )
+
+ path = ['vpp', 'acl', 'mac', 'tag-name', tag, 'rule']
+ conf_rules = conf.list_nodes(path)
+ data_entries = []
+ for rule_index, rule in enumerate(rules):
+ values = [
+ conf_rules[rule_index],
+ action_map.get(rule.get('is_permit')),
+ rule.get('src_prefix'),
+ rule.get('src_mac'),
+ rule.get('src_mac_mask'),
+ ]
+ data_entries.append(values)
+
+ headers = [
+ 'Rule',
+ 'Action',
+ 'IP prefix',
+ 'MAC address',
+ 'MAC mask',
+ ]
+ print(tabulate(data_entries, headers=headers, tablefmt='simple'))
+ print('\n')
+
+
+def _find_acl_by_tag(acls, tag_name):
+ return [acl for acl in acls if acl['tag'] == tag_name]
+
+
+@_verify('ip')
+def show_ip_acls(raw: bool, tag_name: typing.Optional[str]):
+ vpp = VPPControl()
+ acls_dump = vpp.api.acl_dump(acl_index=NO_ACL_INDEX)
+ acls: list[dict] = _get_raw_output_acls(acls_dump)
+
+ if tag_name:
+ acls = _find_acl_by_tag(acls, tag_name)
+
+ if raw:
+ return acls
+
+ else:
+ return _get_formatted_output_acls(acls)
+
+
+@_verify('mac')
+def show_mac_acls(raw: bool, tag_name: typing.Optional[str]):
+ vpp = VPPControl()
+ acls_dump = vpp.api.macip_acl_dump(acl_index=NO_ACL_INDEX)
+ acls: list[dict] = _get_raw_output_acls(acls_dump)
+
+ if tag_name:
+ acls = _find_acl_by_tag(acls, tag_name)
+
+ if raw:
+ return acls
+
+ else:
+ return _get_formatted_output_mac_acls(acls)
+
+
+@_verify('ip')
+def show_interfaces(raw: bool):
+ vpp = VPPControl()
+ interfaces_dump = vpp.api.acl_interface_list_dump()
+ interfaces: list[dict] = _get_raw_output_interfaces(interfaces_dump)
+
+ if raw:
+ return interfaces
+
+ else:
+ return _get_formatted_output_interfaces(vpp, interfaces)
+
+
+@_verify('mac')
+def show_mac_interfaces(raw: bool):
+ vpp = VPPControl()
+ interfaces_dump = vpp.api.macip_acl_interface_list_dump()
+ interfaces: list[dict] = _get_raw_output_interfaces(interfaces_dump)
+
+ if raw:
+ return interfaces
+
+ else:
+ return _get_formatted_output_mac_interfaces(vpp, interfaces)
+
+
+@_verify('no_target')
+def show_all_acls(raw: bool):
+ conf = Config()
+ acls_all = {}
+ path = ['vpp', 'acl']
+ if conf.exists(path + ['ip']):
+ ip_acls = show_ip_acls(raw, tag_name=None)
+ acls_all['ip'] = ip_acls
+ if conf.exists(path + ['mac']):
+ mac_acls = show_mac_acls(raw, tag_name=None)
+ acls_all['mac'] = mac_acls
+
+ if raw:
+ return acls_all
+
+
+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/vpp_nat_cgnat.py b/src/op_mode/vpp_nat_cgnat.py
new file mode 100644
index 000000000..6699d9c55
--- /dev/null
+++ b/src/op_mode/vpp_nat_cgnat.py
@@ -0,0 +1,178 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import json
+import sys
+from tabulate import tabulate
+
+import vyos.opmode
+from vyos.configquery import ConfigTreeQuery
+
+from vyos.vpp import VPPControl
+
+
+def _verify(func):
+ """Decorator checks if config for VPP NAT CGNAT exists"""
+ from functools import wraps
+
+ @wraps(func)
+ def _wrapper(*args, **kwargs):
+ config = ConfigTreeQuery()
+ base = 'vpp nat cgnat'
+ if not config.exists(base):
+ raise vyos.opmode.UnconfiguredSubsystem(f'{base} is not configured')
+
+ return func(*args, **kwargs)
+
+ return _wrapper
+
+
+def _get_raw_output(data_dump):
+ data = [json.loads(json.dumps(d._asdict(), default=str)) for d in data_dump]
+ return data
+
+
+def _get_formatted_output_interfaces(vpp, interfaces):
+ print('CGNAT interfaces:')
+ for interface in interfaces:
+ name = vpp.get_interface_name(interface['sw_if_index'])
+ iface_type = 'in' if interface['is_inside'] else 'out'
+ print(f' {name} {iface_type}')
+
+
+def _get_formatted_output_mappings(rules_list):
+ data_entries = []
+ for rule in rules_list:
+ in_addr = rule.get('in_addr')
+ in_plen = str(rule.get('in_plen'))
+ out_addr = rule.get('out_addr')
+ out_plen = str(rule.get('out_plen'))
+ sharing_ratio = rule.get('sharing_ratio')
+ ports_per_host = rule.get('ports_per_host')
+ ses_num = rule.get('ses_num')
+
+ values = [
+ f'{in_addr}/{in_plen}',
+ f'{out_addr}/{out_plen}',
+ sharing_ratio,
+ ports_per_host,
+ ses_num,
+ ]
+ data_entries.append(values)
+ headers = [
+ 'Inside',
+ 'Outside',
+ 'Sharing ratio',
+ 'Ports per host',
+ 'Sessions',
+ ]
+ out = sorted(data_entries, key=lambda x: x[0])
+ return tabulate(out, headers=headers, tablefmt='simple')
+
+
+@_verify
+def show_sessions(raw: bool):
+ vpp = VPPControl()
+ out = vpp.cli_cmd('show det44 sessions').reply
+ out = out.replace('NAT44 deterministic', 'CGNAT')
+ return out
+
+
+@_verify
+def show_mappings(raw: bool):
+ vpp = VPPControl()
+ nat_static_dump = vpp.api.det44_map_dump()
+ rules_list: list[dict] = _get_raw_output(nat_static_dump)
+
+ if raw:
+ return rules_list
+
+ else:
+ return _get_formatted_output_mappings(rules_list)
+
+
+@_verify
+def show_interfaces(raw: bool):
+ vpp = VPPControl()
+ interfaces_dump = vpp.api.det44_interface_dump()
+ interfaces: list[dict] = _get_raw_output(interfaces_dump)
+
+ if raw:
+ return interfaces
+
+ else:
+ return _get_formatted_output_interfaces(vpp, interfaces)
+
+
+@_verify
+def show_exclude_rules(raw: bool):
+ """Show CGNAT exclude rules (identity mappings)"""
+ vpp = VPPControl()
+ identity_mappings_dump = vpp.api.det44_identity_mapping_dump()
+ mappings: list[dict] = _get_raw_output(identity_mappings_dump)
+
+ if raw:
+ return mappings
+
+ if not mappings:
+ return "No CGNAT exclude rules configured"
+
+ data_entries = []
+ for m in mappings:
+ proto_map = {0: 'all', 1: 'icmp', 6: 'tcp', 17: 'udp', 255: 'all'}
+ proto_name = proto_map.get(m.get('protocol'), str(m.get('protocol')))
+ port_str = str(m.get('port')) if m.get('port') else 'any'
+
+ # Check if address-only (flag & 1)
+ if m.get('flags', 0) & 1:
+ proto_name = 'all'
+ port_str = 'any'
+
+ tag_raw = m.get('tag')
+ if isinstance(tag_raw, bytes):
+ tag = tag_raw.decode('utf-8', errors='replace').rstrip('\x00')
+ else:
+ tag = str(tag_raw) if tag_raw else ''
+
+ values = [m.get('addr'), proto_name, port_str, m.get('vrf_id', 0), tag]
+ data_entries.append(values)
+
+ headers = ['Address', 'Protocol', 'Port', 'VRF', 'Description']
+ out = sorted(data_entries, key=lambda x: x[0])
+ return tabulate(out, headers=headers, tablefmt='simple')
+
+
+@_verify
+def clear_session(address: str, port: str, ext_address: str, ext_port: str):
+ vpp = VPPControl()
+ vpp.api.det44_close_session_in(
+ in_addr=address,
+ in_port=int(port),
+ ext_addr=ext_address,
+ ext_port=int(ext_port),
+ )
+
+
+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/vpp_nat_nat44.py b/src/op_mode/vpp_nat_nat44.py
new file mode 100644
index 000000000..97fca8dbc
--- /dev/null
+++ b/src/op_mode/vpp_nat_nat44.py
@@ -0,0 +1,251 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) VyOS Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# 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, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+import json
+import sys
+from tabulate import tabulate
+
+import vyos.opmode
+from vyos.configquery import ConfigTreeQuery
+
+from vyos.vpp import VPPControl
+
+
+protocol_map = {
+ 0: 'all',
+ 1: 'icmp',
+ 6: 'tcp',
+ 17: 'udp',
+}
+
+# NAT flags
+flags_map = {
+ 'twice-nat': 0x01,
+ 'self-twice-nat': 0x02,
+ 'out2in-only': 0x04,
+ 'out': 0x10,
+ 'in': 0x20,
+}
+
+
+def _verify(func):
+ """Decorator checks if config for VPP NAT44 exists"""
+ from functools import wraps
+
+ @wraps(func)
+ def _wrapper(*args, **kwargs):
+ config = ConfigTreeQuery()
+ base = 'vpp nat nat44'
+ if not config.exists(base):
+ raise vyos.opmode.UnconfiguredSubsystem(f'{base} is not configured')
+
+ return func(*args, **kwargs)
+
+ return _wrapper
+
+
+def decode_bitmask(bitmask: int) -> list:
+ """Decode a bitmask into a list of flag names"""
+ return [name for name, value in flags_map.items() if bitmask & value]
+
+
+def _get_raw_output(data_dump):
+ data = [json.loads(json.dumps(d._asdict(), default=str)) for d in data_dump]
+ return data
+
+
+def _get_raw_output_sessions(vpp_api):
+ users: list[dict] = vpp_api.nat44_user_dump()
+ sessions_list: list[dict] = []
+ for user in users:
+ ip_address = str(user._asdict().get('ip_address'))
+ user_sessions_dump = vpp_api.nat44_user_session_v3_dump(ip_address=ip_address)
+ user_sessions = [
+ json.loads(json.dumps(session._asdict(), default=str))
+ for session in user_sessions_dump
+ ]
+ sessions_list.extend(user_sessions)
+ return sorted(sessions_list, key=lambda x: x["inside_ip_address"])
+
+
+def _get_formatted_output_sessions(sessions_list):
+ print('NAT44 ED sessions:')
+ print(f'--------------- {len(sessions_list)} sessions ---------------')
+ for session in sessions_list:
+ in_ip_addr = session.get('inside_ip_address')
+ in_port = session.get('inside_port')
+ out_ip_addr = session.get('outside_ip_address')
+ out_port = session.get('outside_port')
+ protocol = protocol_map[session.get('protocol')].upper()
+ last_heard = session.get('last_heard')
+ time_since_last_heard = session.get('time_since_last_heard')
+ total_bytes = session.get('total_bytes')
+ total_pkts = session.get('total_pkts')
+ ext_host_address = session.get('ext_host_address')
+ ext_host_port = session.get('ext_host_port')
+ is_timed_out = session.get('is_timed_out')
+
+ print(f' i2o {in_ip_addr} proto {protocol} port {in_port}')
+ print(f' o2i {out_ip_addr} proto {protocol} port {out_port}')
+ print(f' external host {ext_host_address}:{ext_host_port}')
+ print(
+ f' i2o flow: match: saddr {in_ip_addr} sport {in_port} daddr {ext_host_address} dport {ext_host_port} proto {protocol} rewrite: saddr {out_ip_addr}'
+ + (
+ f' sport {out_port}'
+ if protocol != 'ICMP'
+ else f' daddr {ext_host_address} icmp-id {ext_host_port}'
+ )
+ )
+ print(
+ f' o2i flow: match: saddr {ext_host_address} sport {ext_host_port} daddr {out_ip_addr} dport {out_port} proto {protocol} rewrite: '
+ + (
+ f'daddr {in_ip_addr} dport {in_port}'
+ if protocol != 'ICMP'
+ else f' saddr {ext_host_address} daddr {in_ip_addr} icmp-id {ext_host_port}'
+ )
+ )
+ print(f' last heard {last_heard}')
+ print(f' time since last heard {time_since_last_heard}')
+ print(f' total packets {total_pkts}, total bytes {total_bytes}')
+ if is_timed_out:
+ print(' session timed out')
+ print('\n')
+
+
+def _get_formatted_output_addresses(addresses):
+ twice_nat_address = []
+ translation_address = []
+ for address_info in addresses:
+ address = address_info.get('ip_address')
+ if address_info.get('flags') & flags_map['twice-nat']:
+ twice_nat_address.append(address)
+ else:
+ translation_address.append(address)
+
+ print('NAT44 pool addresses:')
+ for addr in translation_address:
+ print(f' {addr}')
+ print('NAT44 twice-nat pool addresses:')
+ for addr in twice_nat_address:
+ print(f' {addr}')
+
+
+def _get_formatted_output_interfaces(vpp, interfaces):
+ print('NAT44 interfaces:')
+ for interface in interfaces:
+ name = vpp.get_interface_name(interface['sw_if_index'])
+ iface_type = decode_bitmask(interface['flags'])
+ print(f' {name} {" ".join(iface_type)}')
+
+
+def _get_formatted_output_rules(rules_list):
+ data_entries = []
+ for rule in rules_list:
+ external_address = rule.get('external_ip_address')
+ external_port = rule.get('external_port') or ''
+ local_address = rule.get('local_ip_address')
+ local_port = rule.get('local_port') or ''
+ protocol = protocol_map[rule.get('protocol', 0)]
+ options = ' '.join(decode_bitmask(rule.get('flags')))
+
+ values = [
+ external_address,
+ external_port,
+ local_address,
+ local_port,
+ protocol,
+ options,
+ ]
+ data_entries.append(values)
+ headers = [
+ 'External address',
+ 'External port',
+ 'Local address',
+ 'Local port',
+ 'Protocol',
+ 'Options',
+ ]
+ out = sorted(data_entries, key=lambda x: x[2])
+ return tabulate(out, headers=headers, tablefmt='simple')
+
+
+@_verify
+def show_sessions(raw: bool):
+ vpp = VPPControl()
+ sessions_list: list[dict] = _get_raw_output_sessions(vpp.api)
+
+ if raw:
+ return sessions_list
+
+ else:
+ return _get_formatted_output_sessions(sessions_list)
+
+
+@_verify
+def show_summary(raw: bool):
+ vpp = VPPControl()
+ return vpp.cli_cmd('show nat44 summary').reply
+
+
+@_verify
+def show_static(raw: bool):
+ vpp = VPPControl()
+ nat_static_dump = vpp.api.nat44_static_mapping_dump()
+ rules_list: list[dict] = _get_raw_output(nat_static_dump)
+
+ if raw:
+ return rules_list
+
+ else:
+ return _get_formatted_output_rules(rules_list)
+
+
+@_verify
+def show_addresses(raw: bool):
+ vpp = VPPControl()
+ addresses_dump = vpp.api.nat44_address_dump()
+ addresses: list[dict] = _get_raw_output(addresses_dump)
+
+ if raw:
+ return addresses
+
+ else:
+ return _get_formatted_output_addresses(addresses)
+
+
+@_verify
+def show_interfaces(raw: bool):
+ vpp = VPPControl()
+ interfaces_dump = vpp.api.nat44_interface_dump()
+ interfaces: list[dict] = _get_raw_output(interfaces_dump)
+
+ if raw:
+ return interfaces
+
+ else:
+ return _get_formatted_output_interfaces(vpp, interfaces)
+
+
+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/vrf.py b/src/op_mode/vrf.py
index 51032a4b5..a13b48866 100755
--- a/src/op_mode/vrf.py
+++ b/src/op_mode/vrf.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022-2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/vrrp.py b/src/op_mode/vrrp.py
index ef1338e23..92eb12d81 100755
--- a/src/op_mode/vrrp.py
+++ b/src/op_mode/vrrp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -191,7 +191,7 @@ def _get_formatted_statistics_output(data: list) -> str:
Prepare formatted statistics output from the given data.
Args:
- data (list): A list of dictionaries containing vrrp grop information
+ data (list): A list of dictionaries containing vrrp group information
and statistics.
Returns:
@@ -228,7 +228,7 @@ def _get_formatted_detail_output(data: list) -> str:
Prepare formatted detail information output from the given data.
Args:
- data (list): A list of dictionaries containing vrrp grop information
+ data (list): A list of dictionaries containing vrrp group information
and statistics.
Returns:
diff --git a/src/op_mode/webproxy_update_blacklist.sh b/src/op_mode/webproxy_update_blacklist.sh
index 05ea86f9e..90594daf6 100755
--- a/src/op_mode/webproxy_update_blacklist.sh
+++ b/src/op_mode/webproxy_update_blacklist.sh
@@ -1,6 +1,6 @@
#!/bin/sh
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
diff --git a/src/op_mode/wireguard_client.py b/src/op_mode/wireguard_client.py
index 04d8ce28c..04d91cc47 100755
--- a/src/op_mode/wireguard_client.py
+++ b/src/op_mode/wireguard_client.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2023 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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
@@ -33,7 +33,7 @@ server_config = """WireGuard client configuration for interface: {{ interface }}
To enable this configuration on a VyOS router you can use the following commands:
-=== VyOS (server) configurtation ===
+=== VyOS (server) configuration ===
{% for addr in address if address is defined %}
set interfaces wireguard {{ interface }} peer {{ name }} allowed-ips '{{ addr }}'
diff --git a/src/op_mode/zone.py b/src/op_mode/zone.py
index df39549d2..8bdf373a8 100644
--- a/src/op_mode/zone.py
+++ b/src/op_mode/zone.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2024 VyOS maintainers and contributors
+# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# 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