summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
Diffstat (limited to 'python')
-rw-r--r--python/setup.py5
-rw-r--r--python/vyos/accel_ppp.py74
-rw-r--r--python/vyos/base.py44
-rw-r--r--python/vyos/component_version.py192
-rw-r--r--python/vyos/component_versions.py57
-rw-r--r--python/vyos/config_mgmt.py686
-rw-r--r--python/vyos/configdep.py95
-rw-r--r--python/vyos/configdiff.py31
-rw-r--r--python/vyos/configsession.py14
-rw-r--r--python/vyos/configtree.py23
-rw-r--r--python/vyos/configverify.py6
-rw-r--r--python/vyos/cpu.py2
-rw-r--r--python/vyos/ethtool.py8
-rw-r--r--python/vyos/firewall.py134
-rw-r--r--python/vyos/formatversions.py109
-rw-r--r--python/vyos/frr.py2
-rw-r--r--python/vyos/ifconfig/__init__.py4
-rw-r--r--python/vyos/ifconfig/ethernet.py12
-rw-r--r--python/vyos/ifconfig/input.py12
-rw-r--r--python/vyos/ifconfig/macvlan.py9
-rw-r--r--python/vyos/ifconfig/sstpc.py40
-rw-r--r--python/vyos/ifconfig/veth.py54
-rw-r--r--python/vyos/migrator.py95
-rw-r--r--python/vyos/nat.py59
-rw-r--r--python/vyos/opmode.py103
-rw-r--r--python/vyos/qos/__init__.py28
-rw-r--r--python/vyos/qos/base.py282
-rw-r--r--python/vyos/qos/cake.py55
-rw-r--r--python/vyos/qos/droptail.py28
-rw-r--r--python/vyos/qos/fairqueue.py31
-rw-r--r--python/vyos/qos/fqcodel.py40
-rw-r--r--python/vyos/qos/limiter.py27
-rw-r--r--python/vyos/qos/netem.py53
-rw-r--r--python/vyos/qos/priority.py41
-rw-r--r--python/vyos/qos/randomdetect.py54
-rw-r--r--python/vyos/qos/ratelimiter.py37
-rw-r--r--python/vyos/qos/roundrobin.py44
-rw-r--r--python/vyos/qos/trafficshaper.py106
-rw-r--r--python/vyos/systemversions.py46
-rw-r--r--python/vyos/template.py12
-rw-r--r--python/vyos/util.py59
41 files changed, 2430 insertions, 383 deletions
diff --git a/python/setup.py b/python/setup.py
index e2d28bd6b..2d614e724 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -24,4 +24,9 @@ setup(
"Topic :: Utilities",
"License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)",
],
+ entry_points={
+ "console_scripts": [
+ "config-mgmt = vyos.config_mgmt:run",
+ ],
+ },
)
diff --git a/python/vyos/accel_ppp.py b/python/vyos/accel_ppp.py
new file mode 100644
index 000000000..bfc8ee5a9
--- /dev/null
+++ b/python/vyos/accel_ppp.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+import sys
+
+import vyos.opmode
+from vyos.util import rc_cmd
+
+
+def get_server_statistics(accel_statistics, pattern, sep=':') -> dict:
+ import re
+
+ stat_dict = {'sessions': {}}
+
+ cpu = re.search(r'cpu(.*)', accel_statistics).group(0)
+ # Find all lines with pattern, for example 'sstp:'
+ data = re.search(rf'{pattern}(.*)', accel_statistics, re.DOTALL).group(0)
+ session_starting = re.search(r'starting(.*)', data).group(0)
+ session_active = re.search(r'active(.*)', data).group(0)
+
+ for entry in {cpu, session_starting, session_active}:
+ if sep in entry:
+ key, value = entry.split(sep)
+ if key in ['starting', 'active', 'finishing']:
+ stat_dict['sessions'][key] = value.strip()
+ continue
+ stat_dict[key] = value.strip()
+ return stat_dict
+
+
+def accel_cmd(port: int, command: str) -> str:
+ _, output = rc_cmd(f'/usr/bin/accel-cmd -p{port} {command}')
+ return output
+
+
+def accel_out_parse(accel_output: list[str]) -> list[dict[str, str]]:
+ """ Parse accel-cmd show sessions output """
+ data_list: list[dict[str, str]] = list()
+ field_names: list[str] = list()
+
+ field_names_unstripped: list[str] = accel_output.pop(0).split('|')
+ for field_name in field_names_unstripped:
+ field_names.append(field_name.strip())
+
+ while accel_output:
+ if '|' not in accel_output[0]:
+ accel_output.pop(0)
+ continue
+
+ current_item: list[str] = accel_output.pop(0).split('|')
+ item_dict: dict[str, str] = {}
+
+ for field_index in range(len(current_item)):
+ field_name: str = field_names[field_index]
+ field_value: str = current_item[field_index].strip()
+ item_dict[field_name] = field_value
+
+ data_list.append(item_dict)
+
+ return data_list
diff --git a/python/vyos/base.py b/python/vyos/base.py
index 78067d5b2..9b93cb2f2 100644
--- a/python/vyos/base.py
+++ b/python/vyos/base.py
@@ -15,17 +15,47 @@
from textwrap import fill
+
+class BaseWarning:
+ def __init__(self, header, message, **kwargs):
+ self.message = message
+ self.kwargs = kwargs
+ if 'width' not in kwargs:
+ self.width = 72
+ if 'initial_indent' in kwargs:
+ del self.kwargs['initial_indent']
+ if 'subsequent_indent' in kwargs:
+ del self.kwargs['subsequent_indent']
+ self.textinitindent = header
+ self.standardindent = ''
+
+ def print(self):
+ messages = self.message.split('\n')
+ isfirstmessage = True
+ initial_indent = self.textinitindent
+ print('')
+ for mes in messages:
+ mes = fill(mes, initial_indent=initial_indent,
+ subsequent_indent=self.standardindent, **self.kwargs)
+ if isfirstmessage:
+ isfirstmessage = False
+ initial_indent = self.standardindent
+ print(f'{mes}')
+ print('')
+
+
class Warning():
- def __init__(self, message):
- # Reformat the message and trim it to 72 characters in length
- message = fill(message, width=72)
- print(f'\nWARNING: {message}')
+ def __init__(self, message, **kwargs):
+ self.BaseWarn = BaseWarning('WARNING: ', message, **kwargs)
+ self.BaseWarn.print()
+
class DeprecationWarning():
- def __init__(self, message):
+ def __init__(self, message, **kwargs):
# Reformat the message and trim it to 72 characters in length
- message = fill(message, width=72)
- print(f'\nDEPRECATION WARNING: {message}\n')
+ self.BaseWarn = BaseWarning('DEPRECATION WARNING: ', message, **kwargs)
+ self.BaseWarn.print()
+
class ConfigError(Exception):
def __init__(self, message):
diff --git a/python/vyos/component_version.py b/python/vyos/component_version.py
new file mode 100644
index 000000000..a4e318d08
--- /dev/null
+++ b/python/vyos/component_version.py
@@ -0,0 +1,192 @@
+# Copyright 2022 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
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Functions for reading/writing component versions.
+
+The config file version string has the following form:
+
+VyOS 1.3/1.4:
+
+// Warning: Do not remove the following line.
+// vyos-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack@3:conntrack-sync@2:dhcp-relay@2:dhcp-server@6:dhcpv6-server@1:dns-forwarding@3:firewall@5:https@2:interfaces@22:ipoe-server@1:ipsec@5:isis@1:l2tp@3:lldp@1:mdns@1:nat@5:ntp@1:pppoe-server@5:pptp@2:qos@1:quagga@8:rpki@1:salt@1:snmp@2:ssh@2:sstp@3:system@21:vrrp@2:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2:zone-policy@1"
+// Release version: 1.3.0
+
+VyOS 1.2:
+
+/* Warning: Do not remove the following line. */
+/* === vyatta-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack-sync@1:conntrack@1:dhcp-relay@2:dhcp-server@5:dns-forwarding@1:firewall@5:ipsec@5:l2tp@1:mdns@1:nat@4:ntp@1:pppoe-server@2:pptp@1:qos@1:quagga@7:snmp@1:ssh@1:system@10:vrrp@2:wanloadbalance@3:webgui@1:webproxy@2:zone-policy@1" === */
+/* Release version: 1.2.8 */
+
+"""
+
+import os
+import re
+import sys
+import fileinput
+
+from vyos.xml import component_version
+from vyos.version import get_version
+from vyos.defaults import directories
+
+DEFAULT_CONFIG_PATH = os.path.join(directories['config'], 'config.boot')
+
+def from_string(string_line, vintage='vyos'):
+ """
+ Get component version dictionary from string.
+ Return empty dictionary if string contains no config information
+ or raise error if component version string malformed.
+ """
+ version_dict = {}
+
+ if vintage == 'vyos':
+ if re.match(r'// vyos-config-version:.+', string_line):
+ if not re.match(r'// vyos-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s*', string_line):
+ raise ValueError(f"malformed configuration string: {string_line}")
+
+ for pair in re.findall(r'([\w,-]+)@(\d+)', string_line):
+ version_dict[pair[0]] = int(pair[1])
+
+ elif vintage == 'vyatta':
+ if re.match(r'/\* === vyatta-config-version:.+=== \*/$', string_line):
+ if not re.match(r'/\* === vyatta-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s+=== \*/$', string_line):
+ raise ValueError(f"malformed configuration string: {string_line}")
+
+ for pair in re.findall(r'([\w,-]+)@(\d+)', string_line):
+ version_dict[pair[0]] = int(pair[1])
+ else:
+ raise ValueError("Unknown config string vintage")
+
+ return version_dict
+
+def from_file(config_file_name=DEFAULT_CONFIG_PATH, vintage='vyos'):
+ """
+ Get component version dictionary parsing config file line by line
+ """
+ with open(config_file_name, 'r') as f:
+ for line_in_config in f:
+ version_dict = from_string(line_in_config, vintage=vintage)
+ if version_dict:
+ return version_dict
+
+ # no version information
+ return {}
+
+def from_system():
+ """
+ Get system component version dict.
+ """
+ return component_version()
+
+def legacy_from_system():
+ """
+ Get system component version dict from legacy location.
+ This is for a transitional sanity check; the directory will eventually
+ be removed.
+ """
+ system_versions = {}
+ legacy_dir = directories['current']
+
+ # To be removed:
+ if not os.path.isdir(legacy_dir):
+ return system_versions
+
+ try:
+ version_info = os.listdir(legacy_dir)
+ except OSError as err:
+ sys.exit(repr(err))
+
+ for info in version_info:
+ if re.match(r'[\w,-]+@\d+', info):
+ pair = info.split('@')
+ system_versions[pair[0]] = int(pair[1])
+
+ return system_versions
+
+def format_string(ver: dict) -> str:
+ """
+ Version dict to string.
+ """
+ keys = list(ver)
+ keys.sort()
+ l = []
+ for k in keys:
+ v = ver[k]
+ l.append(f'{k}@{v}')
+ sep = ':'
+ return sep.join(l)
+
+def version_footer(ver: dict, vintage='vyos') -> str:
+ """
+ Version footer as string.
+ """
+ ver_str = format_string(ver)
+ release = get_version()
+ if vintage == 'vyos':
+ ret_str = (f'// Warning: Do not remove the following line.\n'
+ + f'// vyos-config-version: "{ver_str}"\n'
+ + f'// Release version: {release}\n')
+ elif vintage == 'vyatta':
+ ret_str = (f'/* Warning: Do not remove the following line. */\n'
+ + f'/* === vyatta-config-version: "{ver_str}" === */\n'
+ + f'/* Release version: {release} */\n')
+ else:
+ raise ValueError("Unknown config string vintage")
+
+ return ret_str
+
+def system_footer(vintage='vyos') -> str:
+ """
+ System version footer as string.
+ """
+ ver_d = from_system()
+ return version_footer(ver_d, vintage=vintage)
+
+def write_version_footer(ver: dict, file_name, vintage='vyos'):
+ """
+ Write version footer to file.
+ """
+ footer = version_footer(ver=ver, vintage=vintage)
+ if file_name:
+ with open(file_name, 'a') as f:
+ f.write(footer)
+ else:
+ sys.stdout.write(footer)
+
+def write_system_footer(file_name, vintage='vyos'):
+ """
+ Write system version footer to file.
+ """
+ ver_d = from_system()
+ return write_version_footer(ver_d, file_name=file_name, vintage=vintage)
+
+def remove_footer(file_name):
+ """
+ Remove old version footer.
+ """
+ for line in fileinput.input(file_name, inplace=True):
+ if re.match(r'/\* Warning:.+ \*/$', line):
+ continue
+ if re.match(r'/\* === vyatta-config-version:.+=== \*/$', line):
+ continue
+ if re.match(r'/\* Release version:.+ \*/$', line):
+ continue
+ if re.match('// vyos-config-version:.+', line):
+ continue
+ if re.match('// Warning:.+', line):
+ continue
+ if re.match('// Release version:.+', line):
+ continue
+ sys.stdout.write(line)
diff --git a/python/vyos/component_versions.py b/python/vyos/component_versions.py
deleted file mode 100644
index 90b458aae..000000000
--- a/python/vyos/component_versions.py
+++ /dev/null
@@ -1,57 +0,0 @@
-# Copyright 2017 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
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
-#
-# This library is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with this library. If not, see <http://www.gnu.org/licenses/>.
-
-"""
-The version data looks like:
-
-/* Warning: Do not remove the following line. */
-/* === vyatta-config-version:
-"cluster@1:config-management@1:conntrack-sync@1:conntrack@1:dhcp-relay@1:dhcp-server@4:firewall@5:ipsec@4:nat@4:qos@1:quagga@2:system@8:vrrp@1:wanloadbalance@3:webgui@1:webproxy@1:zone-policy@1"
-=== */
-/* Release version: 1.2.0-rolling+201806131737 */
-"""
-
-import re
-
-def get_component_version(string_line):
- """
- Get component version dictionary from string
- return empty dictionary if string contains no config information
- or raise error if component version string malformed
- """
- return_value = {}
- if re.match(r'/\* === vyatta-config-version:.+=== \*/$', string_line):
-
- if not re.match(r'/\* === vyatta-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s+=== \*/$', string_line):
- raise ValueError("malformed configuration string: " + str(string_line))
-
- for pair in re.findall(r'([\w,-]+)@(\d+)', string_line):
- if pair[0] in return_value.keys():
- raise ValueError("duplicate unit name: \"" + str(pair[0]) + "\" in string: \"" + string_line + "\"")
- return_value[pair[0]] = int(pair[1])
-
- return return_value
-
-
-def get_component_versions_from_file(config_file_name='/opt/vyatta/etc/config/config.boot'):
- """
- Get component version dictionary parsing config file line by line
- """
- f = open(config_file_name, 'r')
- for line_in_config in f:
- component_version = get_component_version(line_in_config)
- if component_version:
- return component_version
- raise ValueError("no config string in file:", config_file_name)
diff --git a/python/vyos/config_mgmt.py b/python/vyos/config_mgmt.py
new file mode 100644
index 000000000..22a49ff50
--- /dev/null
+++ b/python/vyos/config_mgmt.py
@@ -0,0 +1,686 @@
+# Copyright 2023 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
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import re
+import sys
+import gzip
+import logging
+from typing import Optional, Tuple, Union
+from filecmp import cmp
+from datetime import datetime
+from tabulate import tabulate
+
+from vyos.config import Config
+from vyos.configtree import ConfigTree
+from vyos.defaults import directories
+from vyos.util import is_systemd_service_active, ask_yes_no, rc_cmd
+
+SAVE_CONFIG = '/opt/vyatta/sbin/vyatta-save-config.pl'
+
+# created by vyatta-cfg-postinst
+commit_post_hook_dir = '/etc/commit/post-hooks.d'
+
+commit_hooks = {'commit_revision': '01vyos-commit-revision',
+ 'commit_archive': '02vyos-commit-archive'}
+
+DEFAULT_TIME_MINUTES = 10
+timer_name = 'commit-confirm'
+
+config_file = os.path.join(directories['config'], 'config.boot')
+archive_dir = os.path.join(directories['config'], 'archive')
+archive_config_file = os.path.join(archive_dir, 'config.boot')
+commit_log_file = os.path.join(archive_dir, 'commits')
+logrotate_conf = os.path.join(archive_dir, 'lr.conf')
+logrotate_state = os.path.join(archive_dir, 'lr.state')
+rollback_config = os.path.join(archive_dir, 'config.boot-rollback')
+prerollback_config = os.path.join(archive_dir, 'config.boot-prerollback')
+tmp_log_entry = '/tmp/commit-rev-entry'
+
+logger = logging.getLogger('config_mgmt')
+logger.setLevel(logging.INFO)
+ch = logging.StreamHandler()
+formatter = logging.Formatter('%(funcName)s: %(levelname)s:%(message)s')
+ch.setFormatter(formatter)
+logger.addHandler(ch)
+
+class ConfigMgmtError(Exception):
+ pass
+
+class ConfigMgmt:
+ def __init__(self, session_env=None, config=None):
+ if session_env:
+ self._session_env = session_env
+ else:
+ self._session_env = None
+
+ if config is None:
+ config = Config()
+
+ d = config.get_config_dict(['system', 'config-management'],
+ key_mangling=('-', '_'),
+ get_first_key=True)
+
+ self.max_revisions = int(d.get('commit_revisions', 0))
+ self.locations = d.get('commit_archive', {}).get('location', [])
+ self.source_address = d.get('commit_archive',
+ {}).get('source_address', '')
+ if config.exists(['system', 'host-name']):
+ self.hostname = config.return_value(['system', 'host-name'])
+ else:
+ self.hostname = 'vyos'
+
+ # upload only on existence of effective values, notably, on boot.
+ # one still needs session self.locations (above) for setting
+ # post-commit hook in conf_mode script
+ path = ['system', 'config-management', 'commit-archive', 'location']
+ if config.exists_effective(path):
+ self.effective_locations = config.return_effective_values(path)
+ else:
+ self.effective_locations = []
+
+ # a call to compare without args is edit_level aware
+ edit_level = os.getenv('VYATTA_EDIT_LEVEL', '')
+ edit_path = [l for l in edit_level.split('/') if l]
+ if edit_path:
+ eff_conf = config.show_config(edit_path, effective=True)
+ self.edit_level_active_config = ConfigTree(eff_conf)
+ conf = config.show_config(edit_path)
+ self.edit_level_working_config = ConfigTree(conf)
+ else:
+ self.edit_level_active_config = None
+ self.edit_level_working_config = None
+
+ self.active_config = config._running_config
+ self.working_config = config._session_config
+
+ @staticmethod
+ def save_config(target):
+ cmd = f'{SAVE_CONFIG} {target}'
+ rc, out = rc_cmd(cmd)
+ if rc != 0:
+ logger.critical(f'save config failed: {out}')
+
+ def _unsaved_commits(self) -> bool:
+ tmp_save = '/tmp/config.boot.check-save'
+ self.save_config(tmp_save)
+ ret = not cmp(tmp_save, config_file, shallow=False)
+ os.unlink(tmp_save)
+ return ret
+
+ # Console script functions
+ #
+ def commit_confirm(self, minutes: int=DEFAULT_TIME_MINUTES,
+ no_prompt: bool=False) -> Tuple[str,int]:
+ """Commit with reboot to saved config in 'minutes' minutes if
+ 'confirm' call is not issued.
+ """
+ if is_systemd_service_active(f'{timer_name}.timer'):
+ msg = 'Another confirm is pending'
+ return msg, 1
+
+ if self._unsaved_commits():
+ W = '\nYou should save previous commits before commit-confirm !\n'
+ else:
+ W = ''
+
+ prompt_str = f'''
+commit-confirm will automatically reboot in {minutes} minutes unless changes
+are confirmed.\n
+Proceed ?'''
+ prompt_str = W + prompt_str
+ if not no_prompt and not ask_yes_no(prompt_str, default=True):
+ msg = 'commit-confirm canceled'
+ return msg, 1
+
+ action = 'sg vyattacfg "/usr/bin/config-mgmt revert"'
+ cmd = f'sudo systemd-run --quiet --on-active={minutes}m --unit={timer_name} {action}'
+ rc, out = rc_cmd(cmd)
+ if rc != 0:
+ raise ConfigMgmtError(out)
+
+ # start notify
+ cmd = f'sudo -b /usr/libexec/vyos/commit-confirm-notify.py {minutes}'
+ os.system(cmd)
+
+ msg = f'Initialized commit-confirm; {minutes} minutes to confirm before reboot'
+ return msg, 0
+
+ def confirm(self) -> Tuple[str,int]:
+ """Do not reboot to saved config following 'commit-confirm'.
+ Update commit log and archive.
+ """
+ if not is_systemd_service_active(f'{timer_name}.timer'):
+ msg = 'No confirm pending'
+ return msg, 0
+
+ cmd = f'sudo systemctl stop --quiet {timer_name}.timer'
+ rc, out = rc_cmd(cmd)
+ if rc != 0:
+ raise ConfigMgmtError(out)
+
+ # kill notify
+ cmd = 'sudo pkill -f commit-confirm-notify.py'
+ rc, out = rc_cmd(cmd)
+ if rc != 0:
+ raise ConfigMgmtError(out)
+
+ entry = self._read_tmp_log_entry()
+ self._add_log_entry(**entry)
+
+ if self._archive_active_config():
+ self._update_archive()
+
+ msg = 'Reboot timer stopped'
+ return msg, 0
+
+ def revert(self) -> Tuple[str,int]:
+ """Reboot to saved config, dropping commits from 'commit-confirm'.
+ """
+ _ = self._read_tmp_log_entry()
+
+ # archived config will be reverted on boot
+ rc, out = rc_cmd('sudo systemctl reboot')
+ if rc != 0:
+ raise ConfigMgmtError(out)
+
+ return '', 0
+
+ def rollback(self, rev: int, no_prompt: bool=False) -> Tuple[str,int]:
+ """Reboot to config revision 'rev'.
+ """
+ from shutil import copy
+
+ msg = ''
+
+ if not self._check_revision_number(rev):
+ msg = f'Invalid revision number {rev}: must be 0 < rev < {maxrev}'
+ return msg, 1
+
+ prompt_str = 'Proceed with reboot ?'
+ if not no_prompt and not ask_yes_no(prompt_str, default=True):
+ msg = 'Canceling rollback'
+ return msg, 0
+
+ rc, out = rc_cmd(f'sudo cp {archive_config_file} {prerollback_config}')
+ if rc != 0:
+ raise ConfigMgmtError(out)
+
+ path = os.path.join(archive_dir, f'config.boot.{rev}.gz')
+ with gzip.open(path) as f:
+ config = f.read()
+ try:
+ with open(rollback_config, 'wb') as f:
+ f.write(config)
+ copy(rollback_config, config_file)
+ except OSError as e:
+ raise ConfigMgmtError from e
+
+ rc, out = rc_cmd('sudo systemctl reboot')
+ if rc != 0:
+ raise ConfigMgmtError(out)
+
+ return msg, 0
+
+ def compare(self, saved: bool=False, commands: bool=False,
+ rev1: Optional[int]=None,
+ rev2: Optional[int]=None) -> Tuple[str,int]:
+ """General compare function for config file revisions:
+ revision n vs. revision m; working version vs. active version;
+ or working version vs. saved version.
+ """
+ from difflib import unified_diff
+
+ ct1 = self.edit_level_active_config
+ if ct1 is None:
+ ct1 = self.active_config
+ ct2 = self.edit_level_working_config
+ if ct2 is None:
+ ct2 = self.working_config
+ msg = 'No changes between working and active configurations.\n'
+ if saved:
+ ct1 = self._get_saved_config_tree()
+ ct2 = self.working_config
+ msg = 'No changes between working and saved configurations.\n'
+ if rev1 is not None:
+ if not self._check_revision_number(rev1):
+ return f'Invalid revision number {rev1}', 1
+ ct1 = self._get_config_tree_revision(rev1)
+ ct2 = self.working_config
+ msg = f'No changes between working and revision {rev1} configurations.\n'
+ if rev2 is not None:
+ if not self._check_revision_number(rev2):
+ return f'Invalid revision number {rev2}', 1
+ # compare older to newer
+ ct2 = ct1
+ ct1 = self._get_config_tree_revision(rev2)
+ msg = f'No changes between revisions {rev2} and {rev1} configurations.\n'
+
+ if commands:
+ lines1 = ct1.to_commands().splitlines(keepends=True)
+ lines2 = ct2.to_commands().splitlines(keepends=True)
+ else:
+ lines1 = ct1.to_string().splitlines(keepends=True)
+ lines2 = ct2.to_string().splitlines(keepends=True)
+
+ out = ''
+ comp = unified_diff(lines1, lines2)
+ for line in comp:
+ if re.match(r'(\-\-)|(\+\+)|(@@)', line):
+ continue
+ out += line
+ if out:
+ msg = out
+
+ return msg, 0
+
+ def wrap_compare(self, options) -> Tuple[str,int]:
+ """Interface to vyatta-cfg-run: args collected as 'options' to parse
+ for compare.
+ """
+ cmnds = False
+ r1 = None
+ r2 = None
+ if 'commands' in options:
+ cmnds=True
+ options.remove('commands')
+ for i in options:
+ if not i.isnumeric():
+ options.remove(i)
+ if len(options) > 0:
+ r1 = int(options[0])
+ if len(options) > 1:
+ r2 = int(options[1])
+
+ return self.compare(commands=cmnds, rev1=r1, rev2=r2)
+
+ # Initialization and post-commit hooks for conf-mode
+ #
+ def initialize_revision(self):
+ """Initialize config archive, logrotate conf, and commit log.
+ """
+ mask = os.umask(0o002)
+ os.makedirs(archive_dir, exist_ok=True)
+
+ self._add_logrotate_conf()
+
+ if (not os.path.exists(commit_log_file) or
+ self._get_number_of_revisions() == 0):
+ user = self._get_user()
+ via = 'init'
+ comment = ''
+ self._add_log_entry(user, via, comment)
+ # add empty init config before boot-config load for revision
+ # and diff consistency
+ if self._archive_active_config():
+ self._update_archive()
+
+ os.umask(mask)
+
+ def commit_revision(self):
+ """Update commit log and rotate archived config.boot.
+
+ commit_revision is called in post-commit-hooks, if
+ ['commit-archive', 'commit-revisions'] is configured.
+ """
+ if os.getenv('IN_COMMIT_CONFIRM', ''):
+ self._new_log_entry(tmp_file=tmp_log_entry)
+ return
+
+ self._add_log_entry()
+
+ if self._archive_active_config():
+ self._update_archive()
+
+ def commit_archive(self):
+ """Upload config to remote archive.
+ """
+ from vyos.remote import upload
+
+ hostname = self.hostname
+ t = datetime.now()
+ timestamp = t.strftime('%Y%m%d_%H%M%S')
+ remote_file = f'config.boot-{hostname}.{timestamp}'
+ source_address = self.source_address
+
+ for location in self.effective_locations:
+ upload(archive_config_file, f'{location}/{remote_file}',
+ source_host=source_address)
+
+ # op-mode functions
+ #
+ def get_raw_log_data(self) -> list:
+ """Return list of dicts of log data:
+ keys: [timestamp, user, commit_via, commit_comment]
+ """
+ log = self._get_log_entries()
+ res_l = []
+ for line in log:
+ d = self._get_log_entry(line)
+ res_l.append(d)
+
+ return res_l
+
+ @staticmethod
+ def format_log_data(data: list) -> str:
+ """Return formatted log data as str.
+ """
+ res_l = []
+ for l_no, l in enumerate(data):
+ time_d = datetime.fromtimestamp(int(l['timestamp']))
+ time_str = time_d.strftime("%Y-%m-%d %H:%M:%S")
+
+ res_l.append([l_no, time_str,
+ f"by {l['user']}", f"via {l['commit_via']}"])
+
+ if l['commit_comment'] != 'commit': # default comment
+ res_l.append([None, l['commit_comment']])
+
+ ret = tabulate(res_l, tablefmt="plain")
+ return ret
+
+ @staticmethod
+ def format_log_data_brief(data: list) -> str:
+ """Return 'brief' form of log data as str.
+
+ Slightly compacted format used in completion help for
+ 'rollback'.
+ """
+ res_l = []
+ for l_no, l in enumerate(data):
+ time_d = datetime.fromtimestamp(int(l['timestamp']))
+ time_str = time_d.strftime("%Y-%m-%d %H:%M:%S")
+
+ res_l.append(['\t', l_no, time_str,
+ f"{l['user']}", f"by {l['commit_via']}"])
+
+ ret = tabulate(res_l, tablefmt="plain")
+ return ret
+
+ def show_commit_diff(self, rev: int, rev2: Optional[int]=None,
+ commands: bool=False) -> str:
+ """Show commit diff at revision number, compared to previous
+ revision, or to another revision.
+ """
+ if rev2 is None:
+ out, _ = self.compare(commands=commands, rev1=rev, rev2=(rev+1))
+ return out
+
+ out, _ = self.compare(commands=commands, rev1=rev, rev2=rev2)
+ return out
+
+ def show_commit_file(self, rev: int) -> str:
+ return self._get_file_revision(rev)
+
+ # utility functions
+ #
+ @staticmethod
+ def _strip_version(s):
+ return re.split(r'(^//)', s, maxsplit=1, flags=re.MULTILINE)[0]
+
+ def _get_saved_config_tree(self):
+ with open(config_file) as f:
+ c = self._strip_version(f.read())
+ return ConfigTree(c)
+
+ def _get_file_revision(self, rev: int):
+ if rev not in range(0, self._get_number_of_revisions()):
+ raise ConfigMgmtError('revision not available')
+ revision = os.path.join(archive_dir, f'config.boot.{rev}.gz')
+ with gzip.open(revision) as f:
+ r = f.read().decode()
+ return r
+
+ def _get_config_tree_revision(self, rev: int):
+ c = self._strip_version(self._get_file_revision(rev))
+ return ConfigTree(c)
+
+ def _add_logrotate_conf(self):
+ conf = f"""{archive_config_file} {{
+ su root vyattacfg
+ rotate {self.max_revisions}
+ start 0
+ compress
+ copy
+}}"""
+ mask = os.umask(0o133)
+
+ with open(logrotate_conf, 'w') as f:
+ f.write(conf)
+
+ os.umask(mask)
+
+ def _archive_active_config(self) -> bool:
+ mask = os.umask(0o113)
+
+ ext = os.getpid()
+ tmp_save = f'/tmp/config.boot.{ext}'
+ self.save_config(tmp_save)
+
+ try:
+ if cmp(tmp_save, archive_config_file, shallow=False):
+ # this will be the case on boot, as well as certain
+ # re-initialiation instances after delete/set
+ os.unlink(tmp_save)
+ return False
+ except FileNotFoundError:
+ pass
+
+ rc, out = rc_cmd(f'sudo mv {tmp_save} {archive_config_file}')
+ os.umask(mask)
+
+ if rc != 0:
+ logger.critical(f'mv file to archive failed: {out}')
+ return False
+
+ return True
+
+ @staticmethod
+ def _update_archive():
+ cmd = f"sudo logrotate -f -s {logrotate_state} {logrotate_conf}"
+ rc, out = rc_cmd(cmd)
+ if rc != 0:
+ logger.critical(f'logrotate failure: {out}')
+
+ @staticmethod
+ def _get_log_entries() -> list:
+ """Return lines of commit log as list of strings
+ """
+ entries = []
+ if os.path.exists(commit_log_file):
+ with open(commit_log_file) as f:
+ entries = f.readlines()
+
+ return entries
+
+ def _get_number_of_revisions(self) -> int:
+ l = self._get_log_entries()
+ return len(l)
+
+ def _check_revision_number(self, rev: int) -> bool:
+ # exclude init revision:
+ maxrev = self._get_number_of_revisions()
+ if not 0 <= rev < maxrev - 1:
+ return False
+ return True
+
+ @staticmethod
+ def _get_user() -> str:
+ import pwd
+
+ try:
+ user = os.getlogin()
+ except OSError:
+ try:
+ user = pwd.getpwuid(os.geteuid())[0]
+ except KeyError:
+ user = 'unknown'
+ return user
+
+ def _new_log_entry(self, user: str='', commit_via: str='',
+ commit_comment: str='', timestamp: Optional[int]=None,
+ tmp_file: str=None) -> Optional[str]:
+ # Format log entry and return str or write to file.
+ #
+ # Usage is within a post-commit hook, using env values. In case of
+ # commit-confirm, it can be written to a temporary file for
+ # inclusion on 'confirm'.
+ from time import time
+
+ if timestamp is None:
+ timestamp = int(time())
+
+ if not user:
+ user = self._get_user()
+ if not commit_via:
+ commit_via = os.getenv('COMMIT_VIA', 'other')
+ if not commit_comment:
+ commit_comment = os.getenv('COMMIT_COMMENT', 'commit')
+
+ # the commit log reserves '|' as field demarcation, so replace in
+ # comment if present; undo this in _get_log_entry, below
+ if re.search(r'\|', commit_comment):
+ commit_comment = commit_comment.replace('|', '%%')
+
+ entry = f'|{timestamp}|{user}|{commit_via}|{commit_comment}|\n'
+
+ mask = os.umask(0o113)
+ if tmp_file is not None:
+ try:
+ with open(tmp_file, 'w') as f:
+ f.write(entry)
+ except OSError as e:
+ logger.critical(f'write to {tmp_file} failed: {e}')
+ os.umask(mask)
+ return None
+
+ os.umask(mask)
+ return entry
+
+ @staticmethod
+ def _get_log_entry(line: str) -> dict:
+ log_fmt = re.compile(r'\|.*\|\n?$')
+ keys = ['user', 'commit_via', 'commit_comment', 'timestamp']
+ if not log_fmt.match(line):
+ logger.critical(f'Invalid log format {line}')
+ return {}
+
+ timestamp, user, commit_via, commit_comment = (
+ tuple(line.strip().strip('|').split('|')))
+
+ commit_comment = commit_comment.replace('%%', '|')
+ d = dict(zip(keys, [user, commit_via,
+ commit_comment, timestamp]))
+
+ return d
+
+ def _read_tmp_log_entry(self) -> dict:
+ try:
+ with open(tmp_log_entry) as f:
+ entry = f.read()
+ os.unlink(tmp_log_entry)
+ except OSError as e:
+ logger.critical(f'error on file {tmp_log_entry}: {e}')
+
+ return self._get_log_entry(entry)
+
+ def _add_log_entry(self, user: str='', commit_via: str='',
+ commit_comment: str='', timestamp: Optional[int]=None):
+ mask = os.umask(0o113)
+
+ entry = self._new_log_entry(user=user, commit_via=commit_via,
+ commit_comment=commit_comment,
+ timestamp=timestamp)
+
+ log_entries = self._get_log_entries()
+ log_entries.insert(0, entry)
+ if len(log_entries) > self.max_revisions:
+ log_entries = log_entries[:-1]
+
+ try:
+ with open(commit_log_file, 'w') as f:
+ f.writelines(log_entries)
+ except OSError as e:
+ logger.critical(e)
+
+ os.umask(mask)
+
+# entry_point for console script
+#
+def run():
+ from argparse import ArgumentParser, REMAINDER
+
+ config_mgmt = ConfigMgmt()
+
+ for s in list(commit_hooks):
+ if sys.argv[0].replace('-', '_').endswith(s):
+ func = getattr(config_mgmt, s)
+ try:
+ func()
+ except Exception as e:
+ print(f'{s}: {e}')
+ sys.exit(0)
+
+ parser = ArgumentParser()
+ subparsers = parser.add_subparsers(dest='subcommand')
+
+ commit_confirm = subparsers.add_parser('commit_confirm',
+ help="Commit with opt-out reboot to saved config")
+ commit_confirm.add_argument('-t', dest='minutes', type=int,
+ default=DEFAULT_TIME_MINUTES,
+ help="Minutes until reboot, unless 'confirm'")
+ commit_confirm.add_argument('-y', dest='no_prompt', action='store_true',
+ help="Execute without prompt")
+
+ subparsers.add_parser('confirm', help="Confirm commit")
+ subparsers.add_parser('revert', help="Revert commit-confirm")
+
+ rollback = subparsers.add_parser('rollback',
+ help="Rollback to earlier config")
+ rollback.add_argument('--rev', type=int,
+ help="Revision number for rollback")
+ rollback.add_argument('-y', dest='no_prompt', action='store_true',
+ help="Excute without prompt")
+
+ compare = subparsers.add_parser('compare',
+ help="Compare config files")
+
+ compare.add_argument('--saved', action='store_true',
+ help="Compare session config with saved config")
+ compare.add_argument('--commands', action='store_true',
+ help="Show difference between commands")
+ compare.add_argument('--rev1', type=int, default=None,
+ help="Compare revision with session config or other revision")
+ compare.add_argument('--rev2', type=int, default=None,
+ help="Compare revisions")
+
+ wrap_compare = subparsers.add_parser('wrap_compare',
+ help="Wrapper interface for vyatta-cfg-run")
+ wrap_compare.add_argument('--options', nargs=REMAINDER)
+
+ args = vars(parser.parse_args())
+
+ func = getattr(config_mgmt, args['subcommand'])
+ del args['subcommand']
+
+ res = ''
+ try:
+ res, rc = func(**args)
+ except ConfigMgmtError as e:
+ print(e)
+ sys.exit(1)
+ if res:
+ print(res)
+ sys.exit(rc)
diff --git a/python/vyos/configdep.py b/python/vyos/configdep.py
new file mode 100644
index 000000000..d4b2cc78f
--- /dev/null
+++ b/python/vyos/configdep.py
@@ -0,0 +1,95 @@
+# Copyright 2022 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
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import json
+import typing
+from inspect import stack
+
+from vyos.util import load_as_module
+from vyos.defaults import directories
+from vyos.configsource import VyOSError
+from vyos import ConfigError
+
+# https://peps.python.org/pep-0484/#forward-references
+# for type 'Config'
+if typing.TYPE_CHECKING:
+ from vyos.config import Config
+
+dependent_func: dict[str, list[typing.Callable]] = {}
+
+def canon_name(name: str) -> str:
+ return os.path.splitext(name)[0].replace('-', '_')
+
+def canon_name_of_path(path: str) -> str:
+ script = os.path.basename(path)
+ return canon_name(script)
+
+def caller_name() -> str:
+ return stack()[-1].filename
+
+def read_dependency_dict() -> dict:
+ path = os.path.join(directories['data'],
+ 'config-mode-dependencies.json')
+ with open(path) as f:
+ d = json.load(f)
+ return d
+
+def get_dependency_dict(config: 'Config') -> dict:
+ if hasattr(config, 'cached_dependency_dict'):
+ d = getattr(config, 'cached_dependency_dict')
+ else:
+ d = read_dependency_dict()
+ setattr(config, 'cached_dependency_dict', d)
+ return d
+
+def run_config_mode_script(script: str, config: 'Config'):
+ path = os.path.join(directories['conf_mode'], script)
+ name = canon_name(script)
+ mod = load_as_module(name, path)
+
+ config.set_level([])
+ try:
+ c = mod.get_config(config)
+ mod.verify(c)
+ mod.generate(c)
+ mod.apply(c)
+ except (VyOSError, ConfigError) as e:
+ raise ConfigError(repr(e))
+
+def def_closure(target: str, config: 'Config',
+ tagnode: typing.Optional[str] = None) -> typing.Callable:
+ script = target + '.py'
+ def func_impl():
+ if tagnode:
+ os.environ['VYOS_TAGNODE_VALUE'] = tagnode
+ run_config_mode_script(script, config)
+ return func_impl
+
+def set_dependents(case: str, config: 'Config',
+ tagnode: typing.Optional[str] = None):
+ d = get_dependency_dict(config)
+ k = canon_name_of_path(caller_name())
+ l = dependent_func.setdefault(k, [])
+ for target in d[k][case]:
+ func = def_closure(target, config, tagnode)
+ l.append(func)
+
+def call_dependents():
+ k = canon_name_of_path(caller_name())
+ l = dependent_func.get(k, [])
+ while l:
+ f = l.pop(0)
+ f()
diff --git a/python/vyos/configdiff.py b/python/vyos/configdiff.py
index 9185575df..ac86af09c 100644
--- a/python/vyos/configdiff.py
+++ b/python/vyos/configdiff.py
@@ -78,23 +78,34 @@ def get_config_diff(config, key_mangling=None):
isinstance(key_mangling[1], str)):
raise ValueError("key_mangling must be a tuple of two strings")
- diff_t = DiffTree(config._running_config, config._session_config)
+ if hasattr(config, 'cached_diff_tree'):
+ diff_t = getattr(config, 'cached_diff_tree')
+ else:
+ diff_t = DiffTree(config._running_config, config._session_config)
+ setattr(config, 'cached_diff_tree', diff_t)
- return ConfigDiff(config, key_mangling, diff_tree=diff_t)
+ if hasattr(config, 'cached_diff_dict'):
+ diff_d = getattr(config, 'cached_diff_dict')
+ else:
+ diff_d = diff_t.dict
+ setattr(config, 'cached_diff_dict', diff_d)
+
+ return ConfigDiff(config, key_mangling, diff_tree=diff_t,
+ diff_dict=diff_d)
class ConfigDiff(object):
"""
The class of config changes as represented by comparison between the
session config dict and the effective config dict.
"""
- def __init__(self, config, key_mangling=None, diff_tree=None):
+ def __init__(self, config, key_mangling=None, diff_tree=None, diff_dict=None):
self._level = config.get_level()
self._session_config_dict = config.get_cached_root_dict(effective=False)
self._effective_config_dict = config.get_cached_root_dict(effective=True)
self._key_mangling = key_mangling
self._diff_tree = diff_tree
- self._diff_dict = diff_tree.dict if diff_tree else {}
+ self._diff_dict = diff_dict
# mirrored from Config; allow path arguments relative to level
def _make_path(self, path):
@@ -209,9 +220,9 @@ class ConfigDiff(object):
if self._diff_tree is None:
raise NotImplementedError("diff_tree class not available")
else:
- add = get_sub_dict(self._diff_tree.dict, ['add'], get_first_key=True)
- sub = get_sub_dict(self._diff_tree.dict, ['sub'], get_first_key=True)
- inter = get_sub_dict(self._diff_tree.dict, ['inter'], get_first_key=True)
+ add = get_sub_dict(self._diff_dict, ['add'], get_first_key=True)
+ sub = get_sub_dict(self._diff_dict, ['sub'], get_first_key=True)
+ inter = get_sub_dict(self._diff_dict, ['inter'], get_first_key=True)
ret = {}
ret[enum_to_key(Diff.MERGE)] = session_dict
ret[enum_to_key(Diff.DELETE)] = get_sub_dict(sub, self._make_path(path),
@@ -284,9 +295,9 @@ class ConfigDiff(object):
if self._diff_tree is None:
raise NotImplementedError("diff_tree class not available")
else:
- add = get_sub_dict(self._diff_tree.dict, ['add'], get_first_key=True)
- sub = get_sub_dict(self._diff_tree.dict, ['sub'], get_first_key=True)
- inter = get_sub_dict(self._diff_tree.dict, ['inter'], get_first_key=True)
+ add = get_sub_dict(self._diff_dict, ['add'], get_first_key=True)
+ sub = get_sub_dict(self._diff_dict, ['sub'], get_first_key=True)
+ inter = get_sub_dict(self._diff_dict, ['inter'], get_first_key=True)
ret = {}
ret[enum_to_key(Diff.MERGE)] = session_dict
ret[enum_to_key(Diff.DELETE)] = get_sub_dict(sub, self._make_path(path))
diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py
index 3a60f6d92..df44fd8d6 100644
--- a/python/vyos/configsession.py
+++ b/python/vyos/configsession.py
@@ -34,6 +34,8 @@ REMOVE_IMAGE = ['/opt/vyatta/bin/vyatta-boot-image.pl', '--del']
GENERATE = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'generate']
SHOW = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'show']
RESET = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'reset']
+OP_CMD_ADD = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'add']
+OP_CMD_DELETE = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'delete']
# Default "commit via" string
APP = "vyos-http-api"
@@ -204,3 +206,15 @@ class ConfigSession(object):
def reset(self, path):
out = self.__run_command(RESET + path)
return out
+
+ def add_container_image(self, name):
+ out = self.__run_command(OP_CMD_ADD + ['container', 'image'] + [name])
+ return out
+
+ def delete_container_image(self, name):
+ out = self.__run_command(OP_CMD_DELETE + ['container', 'image'] + [name])
+ return out
+
+ def show_container_image(self):
+ out = self.__run_command(SHOW + ['container', 'image'])
+ return out
diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py
index e9cdb69e4..f2358ee4f 100644
--- a/python/vyos/configtree.py
+++ b/python/vyos/configtree.py
@@ -1,5 +1,5 @@
# configtree -- a standalone VyOS config file manipulation library (Python bindings)
-# Copyright (C) 2018 VyOS maintainers and contributors
+# Copyright (C) 2018-2022 VyOS maintainers and contributors
#
# This library is free software; you can redistribute it and/or modify it under the terms of
# the GNU Lesser General Public License as published by the Free Software Foundation;
@@ -12,6 +12,7 @@
# You should have received a copy of the GNU Lesser General Public License along with this library;
# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+import os
import re
import json
@@ -147,6 +148,8 @@ class ConfigTree(object):
self.__config = address
self.__version = ''
+ self.__migration = os.environ.get('VYOS_MIGRATION')
+
def __del__(self):
if self.__config is not None:
self.__destroy(self.__config)
@@ -191,18 +194,27 @@ class ConfigTree(object):
else:
self.__set_add_value(self.__config, path_str, str(value).encode())
+ if self.__migration:
+ print(f"- op: set path: {path} value: {value} replace: {replace}")
+
def delete(self, path):
check_path(path)
path_str = " ".join(map(str, path)).encode()
self.__delete(self.__config, path_str)
+ if self.__migration:
+ print(f"- op: delete path: {path}")
+
def delete_value(self, path, value):
check_path(path)
path_str = " ".join(map(str, path)).encode()
self.__delete_value(self.__config, path_str, value.encode())
+ if self.__migration:
+ print(f"- op: delete_value path: {path} value: {value}")
+
def rename(self, path, new_name):
check_path(path)
path_str = " ".join(map(str, path)).encode()
@@ -216,6 +228,9 @@ class ConfigTree(object):
if (res != 0):
raise ConfigTreeError("Path [{}] doesn't exist".format(path))
+ if self.__migration:
+ print(f"- op: rename old_path: {path} new_path: {new_path}")
+
def copy(self, old_path, new_path):
check_path(old_path)
check_path(new_path)
@@ -227,7 +242,11 @@ class ConfigTree(object):
raise ConfigTreeError()
res = self.__copy(self.__config, oldpath_str, newpath_str)
if (res != 0):
- raise ConfigTreeError("Path [{}] doesn't exist".format(old_path))
+ msg = self.__get_error().decode()
+ raise ConfigTreeError(msg)
+
+ if self.__migration:
+ print(f"- op: copy old_path: {old_path} new_path: {new_path}")
def exists(self, path):
check_path(path)
diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py
index afa0c5b33..8e0ce701e 100644
--- a/python/vyos/configverify.py
+++ b/python/vyos/configverify.py
@@ -388,8 +388,10 @@ def verify_accel_ppp_base_service(config, local_users=True):
"""
# vertify auth settings
if local_users and dict_search('authentication.mode', config) == 'local':
- if dict_search(f'authentication.local_users', config) == None:
- raise ConfigError('Authentication mode local requires local users to be configured!')
+ if (dict_search(f'authentication.local_users', config) is None or
+ dict_search(f'authentication.local_users', config) == {}):
+ raise ConfigError(
+ 'Authentication mode local requires local users to be configured!')
for user in dict_search('authentication.local_users.username', config):
user_config = config['authentication']['local_users']['username'][user]
diff --git a/python/vyos/cpu.py b/python/vyos/cpu.py
index 488ae79fb..d2e5f6504 100644
--- a/python/vyos/cpu.py
+++ b/python/vyos/cpu.py
@@ -73,7 +73,7 @@ def _find_physical_cpus():
# On other architectures, e.g. on ARM, there's no such field.
# We just assume they are different CPUs,
# whether single core ones or cores of physical CPUs.
- phys_cpus[num] = cpu[num]
+ phys_cpus[num] = cpus[num]
return phys_cpus
diff --git a/python/vyos/ethtool.py b/python/vyos/ethtool.py
index 2b6012a73..abf8de688 100644
--- a/python/vyos/ethtool.py
+++ b/python/vyos/ethtool.py
@@ -56,10 +56,10 @@ class Ethtool:
def __init__(self, ifname):
# Get driver used for interface
- sysfs_file = f'/sys/class/net/{ifname}/device/driver/module'
- if os.path.exists(sysfs_file):
- link = os.readlink(sysfs_file)
- self._driver_name = os.path.basename(link)
+ out, err = popen(f'ethtool --driver {ifname}')
+ driver = re.search(r'driver:\s(\w+)', out)
+ if driver:
+ self._driver_name = driver.group(1)
# Build a dictinary of supported link-speed and dupley settings.
out, err = popen(f'ethtool {ifname}')
diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py
index 4075e55b0..b4b9e67bb 100644
--- a/python/vyos/firewall.py
+++ b/python/vyos/firewall.py
@@ -20,6 +20,9 @@ import os
import re
from pathlib import Path
+from socket import AF_INET
+from socket import AF_INET6
+from socket import getaddrinfo
from time import strftime
from vyos.remote import download
@@ -31,65 +34,31 @@ from vyos.util import dict_search_args
from vyos.util import dict_search_recursive
from vyos.util import run
+# Domain Resolver
-# Functions for firewall group domain-groups
-def get_ips_domains_dict(list_domains):
- """
- Get list of IPv4 addresses by list of domains
- Ex: get_ips_domains_dict(['ex1.com', 'ex2.com'])
- {'ex1.com': ['192.0.2.1'], 'ex2.com': ['192.0.2.2', '192.0.2.3']}
- """
- from socket import gethostbyname_ex
- from socket import gaierror
-
- ip_dict = {}
- for domain in list_domains:
- try:
- _, _, ips = gethostbyname_ex(domain)
- ip_dict[domain] = ips
- except gaierror:
- pass
-
- return ip_dict
-
-def nft_init_set(group_name, table="vyos_filter", family="ip"):
- """
- table ip vyos_filter {
- set GROUP_NAME
- type ipv4_addr
- flags interval
- }
- """
- return call(f'nft add set ip {table} {group_name} {{ type ipv4_addr\\; flags interval\\; }}')
-
-
-def nft_add_set_elements(group_name, elements, table="vyos_filter", family="ip"):
- """
- table ip vyos_filter {
- set GROUP_NAME {
- type ipv4_addr
- flags interval
- elements = { 192.0.2.1, 192.0.2.2 }
- }
- """
- elements = ", ".join(elements)
- return call(f'nft add element {family} {table} {group_name} {{ {elements} }} ')
-
-def nft_flush_set(group_name, table="vyos_filter", family="ip"):
- """
- Flush elements of nft set
- """
- return call(f'nft flush set {family} {table} {group_name}')
-
-def nft_update_set_elements(group_name, elements, table="vyos_filter", family="ip"):
- """
- Update elements of nft set
- """
- flush_set = nft_flush_set(group_name, table="vyos_filter", family="ip")
- nft_add_set = nft_add_set_elements(group_name, elements, table="vyos_filter", family="ip")
- return flush_set, nft_add_set
-
-# END firewall group domain-group (sets)
+def fqdn_config_parse(firewall):
+ firewall['ip_fqdn'] = {}
+ firewall['ip6_fqdn'] = {}
+
+ for domain, path in dict_search_recursive(firewall, 'fqdn'):
+ fw_name = path[1] # name/ipv6-name
+ rule = path[3] # rule id
+ suffix = path[4][0] # source/destination (1 char)
+ set_name = f'{fw_name}_{rule}_{suffix}'
+
+ if path[0] == 'name':
+ firewall['ip_fqdn'][set_name] = domain
+ elif path[0] == 'ipv6_name':
+ firewall['ip6_fqdn'][set_name] = domain
+
+def fqdn_resolve(fqdn, ipv6=False):
+ try:
+ res = getaddrinfo(fqdn, None, AF_INET6 if ipv6 else AF_INET)
+ return set(item[4][0] for item in res)
+ except:
+ return None
+
+# End Domain Resolver
def find_nftables_rule(table, chain, rule_matches=[]):
# Find rule in table/chain that matches all criteria and return the handle
@@ -144,12 +113,26 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name):
if side in rule_conf:
prefix = side[0]
side_conf = rule_conf[side]
+ address_mask = side_conf.get('address_mask', None)
if 'address' in side_conf:
suffix = side_conf['address']
- if suffix[0] == '!':
- suffix = f'!= {suffix[1:]}'
- output.append(f'{ip_name} {prefix}addr {suffix}')
+ operator = ''
+ exclude = suffix[0] == '!'
+ if exclude:
+ operator = '!= '
+ suffix = suffix[1:]
+ if address_mask:
+ operator = '!=' if exclude else '=='
+ operator = f'& {address_mask} {operator} '
+ output.append(f'{ip_name} {prefix}addr {operator}{suffix}')
+
+ if 'fqdn' in side_conf:
+ fqdn = side_conf['fqdn']
+ operator = ''
+ if fqdn[0] == '!':
+ operator = '!='
+ output.append(f'{ip_name} {prefix}addr {operator} @FQDN_{fw_name}_{rule_id}_{prefix}')
if dict_search_args(side_conf, 'geoip', 'country_code'):
operator = ''
@@ -192,9 +175,13 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name):
if 'address_group' in group:
group_name = group['address_group']
operator = ''
- if group_name[0] == '!':
+ exclude = group_name[0] == "!"
+ if exclude:
operator = '!='
group_name = group_name[1:]
+ if address_mask:
+ operator = '!=' if exclude else '=='
+ operator = f'& {address_mask} {operator}'
output.append(f'{ip_name} {prefix}addr {operator} @A{def_suffix}_{group_name}')
# Generate firewall group domain-group
elif 'domain_group' in group:
@@ -249,12 +236,20 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name):
output.append(f'ip6 hoplimit {operator} {value}')
if 'inbound_interface' in rule_conf:
- iiface = rule_conf['inbound_interface']
- output.append(f'iifname {iiface}')
+ if 'interface_name' in rule_conf['inbound_interface']:
+ iiface = rule_conf['inbound_interface']['interface_name']
+ output.append(f'iifname {{{iiface}}}')
+ else:
+ iiface = rule_conf['inbound_interface']['interface_group']
+ output.append(f'iifname @I_{iiface}')
if 'outbound_interface' in rule_conf:
- oiface = rule_conf['outbound_interface']
- output.append(f'oifname {oiface}')
+ if 'interface_name' in rule_conf['outbound_interface']:
+ oiface = rule_conf['outbound_interface']['interface_name']
+ output.append(f'oifname {{{oiface}}}')
+ else:
+ oiface = rule_conf['outbound_interface']['interface_group']
+ output.append(f'oifname @I_{oiface}')
if 'ttl' in rule_conf:
operators = {'eq': '==', 'gt': '>', 'lt': '<'}
@@ -327,6 +322,10 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name):
if tcp_mss:
output.append(f'tcp option maxseg size {tcp_mss}')
+ if 'connection_mark' in rule_conf:
+ conn_mark_str = ','.join(rule_conf['connection_mark'])
+ output.append(f'ct mark {{{conn_mark_str}}}')
+
output.append('counter')
if 'set' in rule_conf:
@@ -373,6 +372,9 @@ def parse_time(time):
def parse_policy_set(set_conf, def_suffix):
out = []
+ if 'connection_mark' in set_conf:
+ conn_mark = set_conf['connection_mark']
+ out.append(f'ct mark set {conn_mark}')
if 'dscp' in set_conf:
dscp = set_conf['dscp']
out.append(f'ip{def_suffix} dscp set {dscp}')
diff --git a/python/vyos/formatversions.py b/python/vyos/formatversions.py
deleted file mode 100644
index 29117a5d3..000000000
--- a/python/vyos/formatversions.py
+++ /dev/null
@@ -1,109 +0,0 @@
-# Copyright 2019 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
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
-#
-# This library is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this library. If not, see <http://www.gnu.org/licenses/>.
-
-import sys
-import os
-import re
-import fileinput
-
-def read_vyatta_versions(config_file):
- config_file_versions = {}
-
- with open(config_file, 'r') as config_file_handle:
- for config_line in config_file_handle:
- if re.match(r'/\* === vyatta-config-version:.+=== \*/$', config_line):
- if not re.match(r'/\* === vyatta-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s+=== \*/$', config_line):
- raise ValueError("malformed configuration string: "
- "{}".format(config_line))
-
- for pair in re.findall(r'([\w,-]+)@(\d+)', config_line):
- config_file_versions[pair[0]] = int(pair[1])
-
-
- return config_file_versions
-
-def read_vyos_versions(config_file):
- config_file_versions = {}
-
- with open(config_file, 'r') as config_file_handle:
- for config_line in config_file_handle:
- if re.match(r'// vyos-config-version:.+', config_line):
- if not re.match(r'// vyos-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s*', config_line):
- raise ValueError("malformed configuration string: "
- "{}".format(config_line))
-
- for pair in re.findall(r'([\w,-]+)@(\d+)', config_line):
- config_file_versions[pair[0]] = int(pair[1])
-
- return config_file_versions
-
-def remove_versions(config_file):
- """
- Remove old version string.
- """
- for line in fileinput.input(config_file, inplace=True):
- if re.match(r'/\* Warning:.+ \*/$', line):
- continue
- if re.match(r'/\* === vyatta-config-version:.+=== \*/$', line):
- continue
- if re.match(r'/\* Release version:.+ \*/$', line):
- continue
- if re.match('// vyos-config-version:.+', line):
- continue
- if re.match('// Warning:.+', line):
- continue
- if re.match('// Release version:.+', line):
- continue
- sys.stdout.write(line)
-
-def format_versions_string(config_versions):
- cfg_keys = list(config_versions.keys())
- cfg_keys.sort()
-
- component_version_strings = []
-
- for key in cfg_keys:
- cfg_vers = config_versions[key]
- component_version_strings.append('{}@{}'.format(key, cfg_vers))
-
- separator = ":"
- component_version_string = separator.join(component_version_strings)
-
- return component_version_string
-
-def write_vyatta_versions_foot(config_file, component_version_string,
- os_version_string):
- if config_file:
- with open(config_file, 'a') as config_file_handle:
- config_file_handle.write('/* Warning: Do not remove the following line. */\n')
- config_file_handle.write('/* === vyatta-config-version: "{}" === */\n'.format(component_version_string))
- config_file_handle.write('/* Release version: {} */\n'.format(os_version_string))
- else:
- sys.stdout.write('/* Warning: Do not remove the following line. */\n')
- sys.stdout.write('/* === vyatta-config-version: "{}" === */\n'.format(component_version_string))
- sys.stdout.write('/* Release version: {} */\n'.format(os_version_string))
-
-def write_vyos_versions_foot(config_file, component_version_string,
- os_version_string):
- if config_file:
- with open(config_file, 'a') as config_file_handle:
- config_file_handle.write('// Warning: Do not remove the following line.\n')
- config_file_handle.write('// vyos-config-version: "{}"\n'.format(component_version_string))
- config_file_handle.write('// Release version: {}\n'.format(os_version_string))
- else:
- sys.stdout.write('// Warning: Do not remove the following line.\n')
- sys.stdout.write('// vyos-config-version: "{}"\n'.format(component_version_string))
- sys.stdout.write('// Release version: {}\n'.format(os_version_string))
-
diff --git a/python/vyos/frr.py b/python/vyos/frr.py
index 0ffd5cba9..ccb132dd5 100644
--- a/python/vyos/frr.py
+++ b/python/vyos/frr.py
@@ -477,7 +477,7 @@ class FRRConfig:
# for the listed FRR issues above
pass
if count >= count_max:
- raise ConfigurationNotValid(f'Config commit retry counter ({count_max}) exceeded')
+ raise ConfigurationNotValid(f'Config commit retry counter ({count_max}) exceeded for {daemon} dameon!')
# Save configuration to /run/frr/config/frr.conf
save_configuration()
diff --git a/python/vyos/ifconfig/__init__.py b/python/vyos/ifconfig/__init__.py
index a37615c8f..206b2bba1 100644
--- a/python/vyos/ifconfig/__init__.py
+++ b/python/vyos/ifconfig/__init__.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2021 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2019-2022 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
@@ -36,4 +36,6 @@ from vyos.ifconfig.tunnel import TunnelIf
from vyos.ifconfig.wireless import WiFiIf
from vyos.ifconfig.l2tpv3 import L2TPv3If
from vyos.ifconfig.macsec import MACsecIf
+from vyos.ifconfig.veth import VethIf
from vyos.ifconfig.wwan import WWANIf
+from vyos.ifconfig.sstpc import SSTPCIf
diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py
index 519cfc58c..5080144ff 100644
--- a/python/vyos/ifconfig/ethernet.py
+++ b/python/vyos/ifconfig/ethernet.py
@@ -239,7 +239,7 @@ class EthernetIf(Interface):
if not isinstance(state, bool):
raise ValueError('Value out of range')
- rps_cpus = '0'
+ rps_cpus = 0
queues = len(glob(f'/sys/class/net/{self.ifname}/queues/rx-*'))
if state:
# Enable RPS on all available CPUs except CPU0 which we will not
@@ -248,10 +248,16 @@ class EthernetIf(Interface):
# representation of the CPUs which should participate on RPS, we
# can enable more CPUs that are physically present on the system,
# Linux will clip that internally!
- rps_cpus = 'ffffffff,ffffffff,ffffffff,fffffffe'
+ rps_cpus = (1 << os.cpu_count()) -1
+
+ # XXX: we should probably reserve one core when the system is under
+ # high preasure so we can still have a core left for housekeeping.
+ # This is done by masking out the lowst bit so CPU0 is spared from
+ # receive packet steering.
+ rps_cpus &= ~1
for i in range(0, queues):
- self._write_sysfs(f'/sys/class/net/{self.ifname}/queues/rx-{i}/rps_cpus', rps_cpus)
+ self._write_sysfs(f'/sys/class/net/{self.ifname}/queues/rx-{i}/rps_cpus', f'{rps_cpus:x}')
# send bitmask representation as hex string without leading '0x'
return True
diff --git a/python/vyos/ifconfig/input.py b/python/vyos/ifconfig/input.py
index db7d2b6b4..3e5f5790d 100644
--- a/python/vyos/ifconfig/input.py
+++ b/python/vyos/ifconfig/input.py
@@ -1,4 +1,4 @@
-# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2023 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,6 +17,16 @@ from vyos.ifconfig.interface import Interface
@Interface.register
class InputIf(Interface):
+ """
+ The Intermediate Functional Block (ifb) pseudo network interface acts as a
+ QoS concentrator for multiple different sources of traffic. Packets from
+ or to other interfaces have to be redirected to it using the mirred action
+ in order to be handled, regularly routed traffic will be dropped. This way,
+ a single stack of qdiscs, classes and filters can be shared between
+ multiple interfaces.
+ """
+
+ iftype = 'ifb'
definition = {
**Interface.definition,
**{
diff --git a/python/vyos/ifconfig/macvlan.py b/python/vyos/ifconfig/macvlan.py
index 776014bc3..2266879ec 100644
--- a/python/vyos/ifconfig/macvlan.py
+++ b/python/vyos/ifconfig/macvlan.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2021 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2019-2022 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
@@ -30,10 +30,17 @@ class MACVLANIf(Interface):
}
def _create(self):
+ """
+ Create MACvlan interface in OS kernel. Interface is administrative
+ down by default.
+ """
# please do not change the order when assembling the command
cmd = 'ip link add {ifname} link {source_interface} type {type} mode {mode}'
self._cmd(cmd.format(**self.config))
+ # interface is always A/D down. It needs to be enabled explicitly
+ self.set_admin_state('down')
+
def set_mode(self, mode):
ifname = self.config['ifname']
cmd = f'ip link set dev {ifname} type macvlan mode {mode}'
diff --git a/python/vyos/ifconfig/sstpc.py b/python/vyos/ifconfig/sstpc.py
new file mode 100644
index 000000000..50fc6ee6b
--- /dev/null
+++ b/python/vyos/ifconfig/sstpc.py
@@ -0,0 +1,40 @@
+# Copyright 2022 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
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+from vyos.ifconfig.interface import Interface
+
+@Interface.register
+class SSTPCIf(Interface):
+ iftype = 'sstpc'
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'sstpc',
+ 'prefixes': ['sstpc', ],
+ 'eternal': 'sstpc[0-9]+$',
+ },
+ }
+
+ def _create(self):
+ # we can not create this interface as it is managed outside
+ pass
+
+ def _delete(self):
+ # we can not create this interface as it is managed outside
+ pass
+
+ def get_mac(self):
+ """ Get a synthetic MAC address. """
+ return self.get_mac_synthetic()
diff --git a/python/vyos/ifconfig/veth.py b/python/vyos/ifconfig/veth.py
new file mode 100644
index 000000000..aafbf226a
--- /dev/null
+++ b/python/vyos/ifconfig/veth.py
@@ -0,0 +1,54 @@
+# Copyright 2022 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
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+from vyos.ifconfig.interface import Interface
+
+
+@Interface.register
+class VethIf(Interface):
+ """
+ Abstraction of a Linux veth interface
+ """
+ iftype = 'veth'
+ definition = {
+ **Interface.definition,
+ **{
+ 'section': 'virtual-ethernet',
+ 'prefixes': ['veth', ],
+ 'bridgeable': True,
+ },
+ }
+
+ def _create(self):
+ """
+ Create veth interface in OS kernel. Interface is administrative
+ down by default.
+ """
+ # check before create, as we have 2 veth interfaces in our CLI
+ # interface virtual-ethernet veth0 peer-name 'veth1'
+ # interface virtual-ethernet veth1 peer-name 'veth0'
+ #
+ # but iproute2 creates the pair with one command:
+ # ip link add vet0 type veth peer name veth1
+ if self.exists(self.config['peer_name']):
+ return
+
+ # create virtual-ethernet interface
+ cmd = 'ip link add {ifname} type {type}'.format(**self.config)
+ cmd += f' peer name {self.config["peer_name"]}'
+ self._cmd(cmd)
+
+ # interface is always A/D down. It needs to be enabled explicitly
+ self.set_admin_state('down')
diff --git a/python/vyos/migrator.py b/python/vyos/migrator.py
index c6e3435ca..87c74e1ea 100644
--- a/python/vyos/migrator.py
+++ b/python/vyos/migrator.py
@@ -1,4 +1,4 @@
-# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2019-2022 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
@@ -16,11 +16,13 @@
import sys
import os
import json
-import subprocess
-import vyos.version
+import logging
+
import vyos.defaults
-import vyos.systemversions as systemversions
-import vyos.formatversions as formatversions
+import vyos.component_version as component_version
+from vyos.util import cmd
+
+log_file = os.path.join(vyos.defaults.directories['config'], 'vyos-migrate.log')
class MigratorError(Exception):
pass
@@ -31,9 +33,21 @@ class Migrator(object):
self._force = force
self._set_vintage = set_vintage
self._config_file_vintage = None
- self._log_file = None
self._changed = False
+ def init_logger(self):
+ self.logger = logging.getLogger(__name__)
+ self.logger.setLevel(logging.DEBUG)
+
+ # on adding the file handler, allow write permission for cfg_group;
+ # restore original umask on exit
+ mask = os.umask(0o113)
+ fh = logging.FileHandler(log_file)
+ formatter = logging.Formatter('%(message)s')
+ fh.setFormatter(formatter)
+ self.logger.addHandler(fh)
+ os.umask(mask)
+
def read_config_file_versions(self):
"""
Get component versions from config file footer and set vintage;
@@ -42,13 +56,13 @@ class Migrator(object):
cfg_file = self._config_file
component_versions = {}
- cfg_versions = formatversions.read_vyatta_versions(cfg_file)
+ cfg_versions = component_version.from_file(cfg_file, vintage='vyatta')
if cfg_versions:
self._config_file_vintage = 'vyatta'
component_versions = cfg_versions
- cfg_versions = formatversions.read_vyos_versions(cfg_file)
+ cfg_versions = component_version.from_file(cfg_file, vintage='vyos')
if cfg_versions:
self._config_file_vintage = 'vyos'
@@ -70,34 +84,15 @@ class Migrator(object):
else:
return True
- def open_log_file(self):
- """
- Open log file for migration, catching any error.
- Note that, on boot, migration takes place before the canonical log
- directory is created, hence write to the config file directory.
- """
- self._log_file = os.path.join(vyos.defaults.directories['config'],
- 'vyos-migrate.log')
- # on creation, allow write permission for cfg_group;
- # restore original umask on exit
- mask = os.umask(0o113)
- try:
- log = open('{0}'.format(self._log_file), 'w')
- log.write("List of executed migration scripts:\n")
- except Exception as e:
- os.umask(mask)
- print("Logging error: {0}".format(e))
- return None
-
- os.umask(mask)
- return log
-
def run_migration_scripts(self, config_file_versions, system_versions):
"""
Run migration scripts iteratively, until config file version equals
system component version.
"""
- log = self.open_log_file()
+ os.environ['VYOS_MIGRATION'] = '1'
+ self.init_logger()
+
+ self.logger.info("List of executed migration scripts:")
cfg_versions = config_file_versions
sys_versions = system_versions
@@ -129,8 +124,9 @@ class Migrator(object):
'{}-to-{}'.format(cfg_ver, next_ver))
try:
- subprocess.check_call([migrate_script,
- self._config_file])
+ out = cmd([migrate_script, self._config_file])
+ self.logger.info(f'{migrate_script}')
+ if out: self.logger.info(out)
except FileNotFoundError:
pass
except Exception as err:
@@ -138,38 +134,25 @@ class Migrator(object):
"".format(migrate_script, err))
sys.exit(1)
- if log:
- try:
- log.write('{0}\n'.format(migrate_script))
- except Exception as e:
- print("Error writing log: {0}".format(e))
-
cfg_ver = next_ver
-
rev_versions[key] = cfg_ver
- if log:
- log.close()
-
+ del os.environ['VYOS_MIGRATION']
return rev_versions
def write_config_file_versions(self, cfg_versions):
"""
Write new versions string.
"""
- versions_string = formatversions.format_versions_string(cfg_versions)
-
- os_version_string = vyos.version.get_version()
-
if self._config_file_vintage == 'vyatta':
- formatversions.write_vyatta_versions_foot(self._config_file,
- versions_string,
- os_version_string)
+ component_version.write_version_footer(cfg_versions,
+ self._config_file,
+ vintage='vyatta')
if self._config_file_vintage == 'vyos':
- formatversions.write_vyos_versions_foot(self._config_file,
- versions_string,
- os_version_string)
+ component_version.write_version_footer(cfg_versions,
+ self._config_file,
+ vintage='vyos')
def save_json_record(self, component_versions: dict):
"""
@@ -200,7 +183,7 @@ class Migrator(object):
# This will force calling all migration scripts:
cfg_versions = {}
- sys_versions = systemversions.get_system_component_version()
+ sys_versions = component_version.from_system()
# save system component versions in json file for easy reference
self.save_json_record(sys_versions)
@@ -216,7 +199,7 @@ class Migrator(object):
if not self._changed:
return
- formatversions.remove_versions(cfg_file)
+ component_version.remove_footer(cfg_file)
self.write_config_file_versions(rev_versions)
@@ -237,7 +220,7 @@ class VirtualMigrator(Migrator):
if not self._changed:
return
- formatversions.remove_versions(cfg_file)
+ component_version.remove_footer(cfg_file)
self.write_config_file_versions(cfg_versions)
diff --git a/python/vyos/nat.py b/python/vyos/nat.py
index 31bbdc386..8a311045a 100644
--- a/python/vyos/nat.py
+++ b/python/vyos/nat.py
@@ -16,6 +16,8 @@
from vyos.template import is_ip_network
from vyos.util import dict_search_args
+from vyos.template import bracketize_ipv6
+
def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False):
output = []
@@ -69,6 +71,7 @@ def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False):
else:
translation_output.append('to')
if addr:
+ addr = bracketize_ipv6(addr)
translation_output.append(addr)
options = []
@@ -85,8 +88,13 @@ def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False):
translation_str += f' {",".join(options)}'
for target in ['source', 'destination']:
+ if target not in rule_conf:
+ continue
+
+ side_conf = rule_conf[target]
prefix = target[:1]
- addr = dict_search_args(rule_conf, target, 'address')
+
+ addr = dict_search_args(side_conf, 'address')
if addr and not (ignore_type_addr and target == nat_type):
operator = ''
if addr[:1] == '!':
@@ -94,7 +102,7 @@ def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False):
addr = addr[1:]
output.append(f'{ip_prefix} {prefix}addr {operator} {addr}')
- addr_prefix = dict_search_args(rule_conf, target, 'prefix')
+ addr_prefix = dict_search_args(side_conf, 'prefix')
if addr_prefix and ipv6:
operator = ''
if addr_prefix[:1] == '!':
@@ -102,7 +110,7 @@ def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False):
addr_prefix = addr[1:]
output.append(f'ip6 {prefix}addr {operator} {addr_prefix}')
- port = dict_search_args(rule_conf, target, 'port')
+ port = dict_search_args(side_conf, 'port')
if port:
protocol = rule_conf['protocol']
if protocol == 'tcp_udp':
@@ -113,6 +121,51 @@ def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False):
port = port[1:]
output.append(f'{protocol} {prefix}port {operator} {{ {port} }}')
+ if 'group' in side_conf:
+ group = side_conf['group']
+ if 'address_group' in group and not (ignore_type_addr and target == nat_type):
+ group_name = group['address_group']
+ operator = ''
+ if group_name[0] == '!':
+ operator = '!='
+ group_name = group_name[1:]
+ output.append(f'{ip_prefix} {prefix}addr {operator} @A_{group_name}')
+ # Generate firewall group domain-group
+ elif 'domain_group' in group and not (ignore_type_addr and target == nat_type):
+ group_name = group['domain_group']
+ operator = ''
+ if group_name[0] == '!':
+ operator = '!='
+ group_name = group_name[1:]
+ output.append(f'{ip_prefix} {prefix}addr {operator} @D_{group_name}')
+ elif 'network_group' in group and not (ignore_type_addr and target == nat_type):
+ group_name = group['network_group']
+ operator = ''
+ if group_name[0] == '!':
+ operator = '!='
+ group_name = group_name[1:]
+ output.append(f'{ip_prefix} {prefix}addr {operator} @N_{group_name}')
+ if 'mac_group' in group:
+ group_name = group['mac_group']
+ operator = ''
+ if group_name[0] == '!':
+ operator = '!='
+ group_name = group_name[1:]
+ output.append(f'ether {prefix}addr {operator} @M_{group_name}')
+ if 'port_group' in group:
+ proto = rule_conf['protocol']
+ group_name = group['port_group']
+
+ if proto == 'tcp_udp':
+ proto = 'th'
+
+ operator = ''
+ if group_name[0] == '!':
+ operator = '!='
+ group_name = group_name[1:]
+
+ output.append(f'{proto} {prefix}port {operator} @P_{group_name}')
+
output.append('counter')
if 'log' in rule_conf:
diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py
index 7e3545c87..d02ad4de6 100644
--- a/python/vyos/opmode.py
+++ b/python/vyos/opmode.py
@@ -1,4 +1,4 @@
-# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2022-2023 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
@@ -16,11 +16,16 @@
import re
import sys
import typing
+from humps import decamelize
class Error(Exception):
""" Any error that makes requested operation impossible to complete
for reasons unrelated to the user input or script logic.
+
+ This is the base class, scripts should not use it directly
+ and should raise more specific errors instead,
+ whenever possible.
"""
pass
@@ -44,15 +49,45 @@ class PermissionDenied(Error):
"""
pass
+class InsufficientResources(Error):
+ """ Requested operation and its arguments are valid but the system
+ does not have enough resources (such as drive space or memory)
+ to complete it.
+ """
+ pass
+
+class UnsupportedOperation(Error):
+ """ Requested operation is technically valid but is not implemented yet. """
+ pass
+
+class IncorrectValue(Error):
+ """ Requested operation is valid, but an argument provided has an
+ incorrect value, preventing successful completion.
+ """
+ pass
+
+class CommitInProgress(Error):
+ """ Requested operation is valid, but not possible at the time due
+ to a commit being in progress.
+ """
+ pass
+
+class InternalError(Error):
+ """ Any situation when VyOS detects that it could not perform
+ an operation correctly due to logic errors in its own code
+ or errors in underlying software.
+ """
+ pass
+
def _is_op_mode_function_name(name):
- if re.match(r"^(show|clear|reset|restart)", name):
+ if re.match(r"^(show|clear|reset|restart|add|delete|generate|set)", name):
return True
else:
return False
-def _is_show(name):
- if re.match(r"^show", name):
+def _capture_output(name):
+ if re.match(r"^(show|generate)", name):
return True
else:
return False
@@ -93,6 +128,51 @@ def _get_arg_type(t):
else:
return t
+def _normalize_field_name(name):
+ # Convert the name to string if it is not
+ # (in some cases they may be numbers)
+ name = str(name)
+
+ # Replace all separators with underscores
+ name = re.sub(r'(\s|[\(\)\[\]\{\}\-\.\,:\"\'\`])+', '_', name)
+
+ # Replace specific characters with textual descriptions
+ name = re.sub(r'@', '_at_', name)
+ name = re.sub(r'%', '_percentage_', name)
+ name = re.sub(r'~', '_tilde_', name)
+
+ # Force all letters to lowercase
+ name = name.lower()
+
+ # Remove leading and trailing underscores, if any
+ name = re.sub(r'(^(_+)(?=[^_])|_+$)', '', name)
+
+ # Ensure there are only single underscores
+ name = re.sub(r'_+', '_', name)
+
+ return name
+
+def _normalize_dict_field_names(old_dict):
+ new_dict = {}
+
+ for key in old_dict:
+ new_key = _normalize_field_name(key)
+ new_dict[new_key] = _normalize_field_names(old_dict[key])
+
+ # Sanity check
+ if len(old_dict) != len(new_dict):
+ raise InternalError("Dictionary fields do not allow unique normalization")
+ else:
+ return new_dict
+
+def _normalize_field_names(value):
+ if isinstance(value, dict):
+ return _normalize_dict_field_names(value)
+ elif isinstance(value, list):
+ return list(map(lambda v: _normalize_field_names(v), value))
+ else:
+ return value
+
def run(module):
from argparse import ArgumentParser
@@ -134,20 +214,25 @@ def run(module):
# it would cause an extra argument error when we pass the dict to a function
del args["subcommand"]
- # Show commands must always get the "raw" argument,
- # but other commands (clear/reset/restart) should not,
+ # Show and generate commands must always get the "raw" argument,
+ # but other commands (clear/reset/restart/add/delete) should not,
# because they produce no output and it makes no sense for them.
- if ("raw" not in args) and _is_show(function_name):
+ if ("raw" not in args) and _capture_output(function_name):
args["raw"] = False
- if re.match(r"^show", function_name):
- # Show commands are slightly special:
+ if _capture_output(function_name):
+ # Show and generate commands are slightly special:
# they may return human-formatted output
# or a raw dict that we need to serialize in JSON for printing
res = func(**args)
if not args["raw"]:
return res
else:
+ if not isinstance(res, dict) and not isinstance(res, list):
+ raise InternalError(f"Bare literal is not an acceptable raw output, must be a list or an object.\
+ The output was:{res}")
+ res = decamelize(res)
+ res = _normalize_field_names(res)
from json import dumps
return dumps(res, indent=4)
else:
diff --git a/python/vyos/qos/__init__.py b/python/vyos/qos/__init__.py
new file mode 100644
index 000000000..a2980ccde
--- /dev/null
+++ b/python/vyos/qos/__init__.py
@@ -0,0 +1,28 @@
+# Copyright 2022 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
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+from vyos.qos.base import QoSBase
+from vyos.qos.cake import CAKE
+from vyos.qos.droptail import DropTail
+from vyos.qos.fairqueue import FairQueue
+from vyos.qos.fqcodel import FQCodel
+from vyos.qos.limiter import Limiter
+from vyos.qos.netem import NetEm
+from vyos.qos.priority import Priority
+from vyos.qos.randomdetect import RandomDetect
+from vyos.qos.ratelimiter import RateLimiter
+from vyos.qos.roundrobin import RoundRobin
+from vyos.qos.trafficshaper import TrafficShaper
+from vyos.qos.trafficshaper import TrafficShaperHFSC
diff --git a/python/vyos/qos/base.py b/python/vyos/qos/base.py
new file mode 100644
index 000000000..28635b5e7
--- /dev/null
+++ b/python/vyos/qos/base.py
@@ -0,0 +1,282 @@
+# Copyright 2022 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
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from vyos.base import Warning
+from vyos.util import cmd
+from vyos.util import dict_search
+from vyos.util import read_file
+
+class QoSBase:
+ _debug = False
+ _direction = ['egress']
+ _parent = 0xffff
+
+ def __init__(self, interface):
+ if os.path.exists('/tmp/vyos.qos.debug'):
+ self._debug = True
+ self._interface = interface
+
+ def _cmd(self, command):
+ if self._debug:
+ print(f'DEBUG/QoS: {command}')
+ return cmd(command)
+
+ def get_direction(self) -> list:
+ return self._direction
+
+ def _get_class_max_id(self, config) -> int:
+ if 'class' in config:
+ tmp = list(config['class'].keys())
+ tmp.sort(key=lambda ii: int(ii))
+ return tmp[-1]
+ return None
+
+ def _build_base_qdisc(self, config : dict, cls_id : int):
+ """
+ Add/replace qdisc for every class (also default is a class). This is
+ a genetic method which need an implementation "per" queue-type.
+
+ This matches the old mapping as defined in Perl here:
+ https://github.com/vyos/vyatta-cfg-qos/blob/equuleus/lib/Vyatta/Qos/ShaperClass.pm#L223-L229
+ """
+ queue_type = dict_search('queue_type', config)
+ default_tc = f'tc qdisc replace dev {self._interface} parent {self._parent}:{cls_id:x}'
+
+ if queue_type == 'priority':
+ handle = 0x4000 + cls_id
+ default_tc += f' handle {handle:x}: prio'
+ self._cmd(default_tc)
+
+ queue_limit = dict_search('queue_limit', config)
+ for ii in range(1, 4):
+ tmp = f'tc qdisc replace dev {self._interface} parent {handle:x}:{ii:x} pfifo limit {queue_limit}'
+ self._cmd(tmp)
+
+ elif queue_type == 'fair-queue':
+ default_tc += f' sfq'
+
+ tmp = dict_search('queue_limit', config)
+ if tmp: default_tc += f' limit {tmp}'
+
+ self._cmd(default_tc)
+
+ elif queue_type == 'fq-codel':
+ default_tc += f' fq_codel'
+ tmp = dict_search('codel_quantum', config)
+ if tmp: default_tc += f' quantum {tmp}'
+
+ tmp = dict_search('flows', config)
+ if tmp: default_tc += f' flows {tmp}'
+
+ tmp = dict_search('interval', config)
+ if tmp: default_tc += f' interval {tmp}'
+
+ tmp = dict_search('interval', config)
+ if tmp: default_tc += f' interval {tmp}'
+
+ tmp = dict_search('queue_limit', config)
+ if tmp: default_tc += f' limit {tmp}'
+
+ tmp = dict_search('target', config)
+ if tmp: default_tc += f' target {tmp}'
+
+ default_tc += f' noecn'
+
+ self._cmd(default_tc)
+
+ elif queue_type == 'random-detect':
+ default_tc += f' red'
+
+ self._cmd(default_tc)
+
+ elif queue_type == 'drop-tail':
+ default_tc += f' pfifo'
+
+ tmp = dict_search('queue_limit', config)
+ if tmp: default_tc += f' limit {tmp}'
+
+ self._cmd(default_tc)
+
+ def _rate_convert(self, rate) -> int:
+ rates = {
+ 'bit' : 1,
+ 'kbit' : 1000,
+ 'mbit' : 1000000,
+ 'gbit' : 1000000000,
+ 'tbit' : 1000000000000,
+ }
+
+ if rate == 'auto' or rate.endswith('%'):
+ speed = read_file(f'/sys/class/net/{self._interface}/speed')
+ if not speed.isnumeric():
+ Warning('Interface speed cannot be determined (assuming 10 Mbit/s)')
+ speed = 10
+ if rate.endswith('%'):
+ percent = rate.rstrip('%')
+ speed = int(speed) * int(percent) // 100
+ return int(speed) *1000000 # convert to MBit/s
+
+ rate_numeric = int(''.join([n for n in rate if n.isdigit()]))
+ rate_scale = ''.join([n for n in rate if not n.isdigit()])
+
+ if int(rate_numeric) <= 0:
+ raise ValueError(f'{rate_numeric} is not a valid bandwidth <= 0')
+
+ if rate_scale:
+ return int(rate_numeric * rates[rate_scale])
+ else:
+ # No suffix implies Kbps just as Cisco IOS
+ return int(rate_numeric * 1000)
+
+ def update(self, config, direction, priority=None):
+ """ method must be called from derived class after it has completed qdisc setup """
+
+ if 'class' in config:
+ for cls, cls_config in config['class'].items():
+ self._build_base_qdisc(cls_config, int(cls))
+
+ if 'match' in cls_config:
+ for match, match_config in cls_config['match'].items():
+ for af in ['ip', 'ipv6']:
+ # every match criteria has it's tc instance
+ filter_cmd = f'tc filter replace dev {self._interface} parent {self._parent:x}:'
+
+ if priority:
+ filter_cmd += f' prio {cls}'
+ elif 'priority' in cls_config:
+ prio = cls_config['priority']
+ filter_cmd += f' prio {prio}'
+
+ filter_cmd += ' protocol all u32'
+
+ tc_af = af
+ if af == 'ipv6':
+ tc_af = 'ip6'
+
+ if af in match_config:
+ tmp = dict_search(f'{af}.source.address', match_config)
+ if tmp: filter_cmd += f' match {tc_af} src {tmp}'
+
+ tmp = dict_search(f'{af}.source.port', match_config)
+ if tmp: filter_cmd += f' match {tc_af} sport {tmp} 0xffff'
+
+ tmp = dict_search(f'{af}.destination.address', match_config)
+ if tmp: filter_cmd += f' match {tc_af} dst {tmp}'
+
+ tmp = dict_search(f'{af}.destination.port', match_config)
+ if tmp: filter_cmd += f' match {tc_af} dport {tmp} 0xffff'
+
+ tmp = dict_search(f'{af}.protocol', match_config)
+ if tmp: filter_cmd += f' match {tc_af} protocol {tmp} 0xff'
+
+ # Will match against total length of an IPv4 packet and
+ # payload length of an IPv6 packet.
+ #
+ # IPv4 : match u16 0x0000 ~MAXLEN at 2
+ # IPv6 : match u16 0x0000 ~MAXLEN at 4
+ tmp = dict_search(f'{af}.max_length', match_config)
+ if tmp:
+ # We need the 16 bit two's complement of the maximum
+ # packet length
+ tmp = hex(0xffff & ~int(tmp))
+
+ if af == 'ip':
+ filter_cmd += f' match u16 0x0000 {tmp} at 2'
+ elif af == 'ipv6':
+ filter_cmd += f' match u16 0x0000 {tmp} at 4'
+
+ # We match against specific TCP flags - we assume the IPv4
+ # header length is 20 bytes and assume the IPv6 packet is
+ # not using extension headers (hence a ip header length of 40 bytes)
+ # TCP Flags are set on byte 13 of the TCP header.
+ # IPv4 : match u8 X X at 33
+ # IPv6 : match u8 X X at 53
+ # with X = 0x02 for SYN and X = 0x10 for ACK
+ tmp = dict_search(f'{af}.tcp', match_config)
+ if tmp:
+ mask = 0
+ if 'ack' in tmp:
+ mask |= 0x10
+ if 'syn' in tmp:
+ mask |= 0x02
+ mask = hex(mask)
+
+ if af == 'ip':
+ filter_cmd += f' match u8 {mask} {mask} at 33'
+ elif af == 'ipv6':
+ filter_cmd += f' match u8 {mask} {mask} at 53'
+
+ # The police block allows limiting of the byte or packet rate of
+ # traffic matched by the filter it is attached to.
+ # https://man7.org/linux/man-pages/man8/tc-police.8.html
+ if any(tmp in ['exceed', 'bandwidth', 'burst'] for tmp in cls_config):
+ filter_cmd += f' action police'
+
+ if 'exceed' in cls_config:
+ action = cls_config['exceed']
+ filter_cmd += f' conform-exceed {action}'
+ if 'not_exceed' in cls_config:
+ action = cls_config['not_exceed']
+ filter_cmd += f'/{action}'
+
+ if 'bandwidth' in cls_config:
+ rate = self._rate_convert(cls_config['bandwidth'])
+ filter_cmd += f' rate {rate}'
+
+ if 'burst' in cls_config:
+ burst = cls_config['burst']
+ filter_cmd += f' burst {burst}'
+
+ cls = int(cls)
+ filter_cmd += f' flowid {self._parent:x}:{cls:x}'
+ self._cmd(filter_cmd)
+
+ if 'default' in config:
+ if 'class' in config:
+ class_id_max = self._get_class_max_id(config)
+ default_cls_id = int(class_id_max) +1
+ self._build_base_qdisc(config['default'], default_cls_id)
+
+ filter_cmd = f'tc filter replace dev {self._interface} parent {self._parent:x}: '
+ filter_cmd += 'prio 255 protocol all basic'
+
+ # The police block allows limiting of the byte or packet rate of
+ # traffic matched by the filter it is attached to.
+ # https://man7.org/linux/man-pages/man8/tc-police.8.html
+ if any(tmp in ['exceed', 'bandwidth', 'burst'] for tmp in config['default']):
+ filter_cmd += f' action police'
+
+ if 'exceed' in config['default']:
+ action = config['default']['exceed']
+ filter_cmd += f' conform-exceed {action}'
+ if 'not_exceed' in config['default']:
+ action = config['default']['not_exceed']
+ filter_cmd += f'/{action}'
+
+ if 'bandwidth' in config['default']:
+ rate = self._rate_convert(config['default']['bandwidth'])
+ filter_cmd += f' rate {rate}'
+
+ if 'burst' in config['default']:
+ burst = config['default']['burst']
+ filter_cmd += f' burst {burst}'
+
+ if 'class' in config:
+ filter_cmd += f' flowid {self._parent:x}:{default_cls_id:x}'
+
+ self._cmd(filter_cmd)
+
diff --git a/python/vyos/qos/cake.py b/python/vyos/qos/cake.py
new file mode 100644
index 000000000..a89b1de1e
--- /dev/null
+++ b/python/vyos/qos/cake.py
@@ -0,0 +1,55 @@
+# Copyright 2022 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
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+from vyos.qos.base import QoSBase
+
+class CAKE(QoSBase):
+ _direction = ['egress']
+
+ # https://man7.org/linux/man-pages/man8/tc-cake.8.html
+ def update(self, config, direction):
+ tmp = f'tc qdisc add dev {self._interface} root handle 1: cake {direction}'
+ if 'bandwidth' in config:
+ bandwidth = self._rate_convert(config['bandwidth'])
+ tmp += f' bandwidth {bandwidth}'
+
+ if 'rtt' in config:
+ rtt = config['rtt']
+ tmp += f' rtt {rtt}ms'
+
+ if 'flow_isolation' in config:
+ if 'blind' in config['flow_isolation']:
+ tmp += f' flowblind'
+ if 'dst_host' in config['flow_isolation']:
+ tmp += f' dsthost'
+ if 'dual_dst_host' in config['flow_isolation']:
+ tmp += f' dual-dsthost'
+ if 'dual_src_host' in config['flow_isolation']:
+ tmp += f' dual-srchost'
+ if 'flow' in config['flow_isolation']:
+ tmp += f' flows'
+ if 'host' in config['flow_isolation']:
+ tmp += f' hosts'
+ if 'nat' in config['flow_isolation']:
+ tmp += f' nat'
+ if 'src_host' in config['flow_isolation']:
+ tmp += f' srchost '
+ else:
+ tmp += f' nonat'
+
+ self._cmd(tmp)
+
+ # call base class
+ super().update(config, direction)
diff --git a/python/vyos/qos/droptail.py b/python/vyos/qos/droptail.py
new file mode 100644
index 000000000..427d43d19
--- /dev/null
+++ b/python/vyos/qos/droptail.py
@@ -0,0 +1,28 @@
+# Copyright 2022 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
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+from vyos.qos.base import QoSBase
+
+class DropTail(QoSBase):
+ # https://man7.org/linux/man-pages/man8/tc-pfifo.8.html
+ def update(self, config, direction):
+ tmp = f'tc qdisc add dev {self._interface} root pfifo'
+ if 'queue_limit' in config:
+ limit = config["queue_limit"]
+ tmp += f' limit {limit}'
+ self._cmd(tmp)
+
+ # call base class
+ super().update(config, direction)
diff --git a/python/vyos/qos/fairqueue.py b/python/vyos/qos/fairqueue.py
new file mode 100644
index 000000000..f41d098fb
--- /dev/null
+++ b/python/vyos/qos/fairqueue.py
@@ -0,0 +1,31 @@
+# Copyright 2022 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
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+from vyos.qos.base import QoSBase
+
+class FairQueue(QoSBase):
+ # https://man7.org/linux/man-pages/man8/tc-sfq.8.html
+ def update(self, config, direction):
+ tmp = f'tc qdisc add dev {self._interface} root sfq'
+
+ if 'hash_interval' in config:
+ tmp += f' perturb {config["hash_interval"]}'
+ if 'queue_limit' in config:
+ tmp += f' limit {config["queue_limit"]}'
+
+ self._cmd(tmp)
+
+ # call base class
+ super().update(config, direction)
diff --git a/python/vyos/qos/fqcodel.py b/python/vyos/qos/fqcodel.py
new file mode 100644
index 000000000..cd2340aa2
--- /dev/null
+++ b/python/vyos/qos/fqcodel.py
@@ -0,0 +1,40 @@
+# Copyright 2022 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
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+from vyos.qos.base import QoSBase
+
+class FQCodel(QoSBase):
+ # https://man7.org/linux/man-pages/man8/tc-fq_codel.8.html
+ def update(self, config, direction):
+ tmp = f'tc qdisc add dev {self._interface} root fq_codel'
+
+ if 'codel_quantum' in config:
+ tmp += f' quantum {config["codel_quantum"]}'
+ if 'flows' in config:
+ tmp += f' flows {config["flows"]}'
+ if 'interval' in config:
+ interval = int(config['interval']) * 1000
+ tmp += f' interval {interval}'
+ if 'queue_limit' in config:
+ tmp += f' limit {config["queue_limit"]}'
+ if 'target' in config:
+ target = int(config['target']) * 1000
+ tmp += f' target {target}'
+
+ tmp += f' noecn'
+ self._cmd(tmp)
+
+ # call base class
+ super().update(config, direction)
diff --git a/python/vyos/qos/limiter.py b/python/vyos/qos/limiter.py
new file mode 100644
index 000000000..ace0c0b6c
--- /dev/null
+++ b/python/vyos/qos/limiter.py
@@ -0,0 +1,27 @@
+# Copyright 2022 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
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+from vyos.qos.base import QoSBase
+
+class Limiter(QoSBase):
+ _direction = ['ingress']
+
+ def update(self, config, direction):
+ tmp = f'tc qdisc add dev {self._interface} handle {self._parent:x}: {direction}'
+ self._cmd(tmp)
+
+ # base class must be called last
+ super().update(config, direction)
+
diff --git a/python/vyos/qos/netem.py b/python/vyos/qos/netem.py
new file mode 100644
index 000000000..8bdef300b
--- /dev/null
+++ b/python/vyos/qos/netem.py
@@ -0,0 +1,53 @@
+# Copyright 2022 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
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+from vyos.qos.base import QoSBase
+
+class NetEm(QoSBase):
+ # https://man7.org/linux/man-pages/man8/tc-netem.8.html
+ def update(self, config, direction):
+ tmp = f'tc qdisc add dev {self._interface} root netem'
+ if 'bandwidth' in config:
+ rate = self._rate_convert(config["bandwidth"])
+ tmp += f' rate {rate}'
+
+ if 'queue_limit' in config:
+ limit = config["queue_limit"]
+ tmp += f' limit {limit}'
+
+ if 'delay' in config:
+ delay = config["delay"]
+ tmp += f' delay {delay}ms'
+
+ if 'loss' in config:
+ drop = config["loss"]
+ tmp += f' drop {drop}%'
+
+ if 'corruption' in config:
+ corrupt = config["corruption"]
+ tmp += f' corrupt {corrupt}%'
+
+ if 'reordering' in config:
+ reorder = config["reordering"]
+ tmp += f' reorder {reorder}%'
+
+ if 'duplicate' in config:
+ duplicate = config["duplicate"]
+ tmp += f' duplicate {duplicate}%'
+
+ self._cmd(tmp)
+
+ # call base class
+ super().update(config, direction)
diff --git a/python/vyos/qos/priority.py b/python/vyos/qos/priority.py
new file mode 100644
index 000000000..6d4a60a43
--- /dev/null
+++ b/python/vyos/qos/priority.py
@@ -0,0 +1,41 @@
+# Copyright 2022 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
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+from vyos.qos.base import QoSBase
+from vyos.util import dict_search
+
+class Priority(QoSBase):
+ _parent = 1
+
+ # https://man7.org/linux/man-pages/man8/tc-prio.8.html
+ def update(self, config, direction):
+ if 'class' in config:
+ class_id_max = self._get_class_max_id(config)
+ bands = int(class_id_max) +1
+
+ tmp = f'tc qdisc add dev {self._interface} root handle {self._parent:x}: prio bands {bands} priomap ' \
+ f'{class_id_max} {class_id_max} {class_id_max} {class_id_max} ' \
+ f'{class_id_max} {class_id_max} {class_id_max} {class_id_max} ' \
+ f'{class_id_max} {class_id_max} {class_id_max} {class_id_max} ' \
+ f'{class_id_max} {class_id_max} {class_id_max} {class_id_max} '
+ self._cmd(tmp)
+
+ for cls in config['class']:
+ cls = int(cls)
+ tmp = f'tc qdisc add dev {self._interface} parent {self._parent:x}:{cls:x} pfifo'
+ self._cmd(tmp)
+
+ # base class must be called last
+ super().update(config, direction, priority=True)
diff --git a/python/vyos/qos/randomdetect.py b/python/vyos/qos/randomdetect.py
new file mode 100644
index 000000000..d7d84260f
--- /dev/null
+++ b/python/vyos/qos/randomdetect.py
@@ -0,0 +1,54 @@
+# Copyright 2022 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
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+from vyos.qos.base import QoSBase
+
+class RandomDetect(QoSBase):
+ _parent = 1
+
+ # https://man7.org/linux/man-pages/man8/tc.8.html
+ def update(self, config, direction):
+
+ tmp = f'tc qdisc add dev {self._interface} root handle {self._parent}:0 dsmark indices 8 set_tc_index'
+ self._cmd(tmp)
+
+ tmp = f'tc filter add dev {self._interface} parent {self._parent}:0 protocol ip prio 1 tcindex mask 0xe0 shift 5'
+ self._cmd(tmp)
+
+ # Generalized Random Early Detection
+ handle = self._parent +1
+ tmp = f'tc qdisc add dev {self._interface} parent {self._parent}:0 handle {handle}:0 gred setup DPs 8 default 0 grio'
+ self._cmd(tmp)
+
+ bandwidth = self._rate_convert(config['bandwidth'])
+
+ # set VQ (virtual queue) parameters
+ for precedence, precedence_config in config['precedence'].items():
+ precedence = int(precedence)
+ avg_pkt = int(precedence_config['average_packet'])
+ limit = int(precedence_config['queue_limit']) * avg_pkt
+ min_val = int(precedence_config['minimum_threshold']) * avg_pkt
+ max_val = int(precedence_config['maximum_threshold']) * avg_pkt
+
+ tmp = f'tc qdisc change dev {self._interface} handle {handle}:0 gred limit {limit} min {min_val} max {max_val} avpkt {avg_pkt} '
+
+ burst = (2 * int(precedence_config['minimum_threshold']) + int(precedence_config['maximum_threshold'])) // 3
+ probability = 1 / int(precedence_config['mark_probability'])
+ tmp += f'burst {burst} bandwidth {bandwidth} probability {probability} DP {precedence} prio {8 - precedence:x}'
+
+ self._cmd(tmp)
+
+ # call base class
+ super().update(config, direction)
diff --git a/python/vyos/qos/ratelimiter.py b/python/vyos/qos/ratelimiter.py
new file mode 100644
index 000000000..a4f80a1be
--- /dev/null
+++ b/python/vyos/qos/ratelimiter.py
@@ -0,0 +1,37 @@
+# Copyright 2022 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
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+from vyos.qos.base import QoSBase
+
+class RateLimiter(QoSBase):
+ # https://man7.org/linux/man-pages/man8/tc-tbf.8.html
+ def update(self, config, direction):
+ # call base class
+ super().update(config, direction)
+
+ tmp = f'tc qdisc add dev {self._interface} root tbf'
+ if 'bandwidth' in config:
+ rate = self._rate_convert(config['bandwidth'])
+ tmp += f' rate {rate}'
+
+ if 'burst' in config:
+ burst = config['burst']
+ tmp += f' burst {burst}'
+
+ if 'latency' in config:
+ latency = config['latency']
+ tmp += f' latency {latency}ms'
+
+ self._cmd(tmp)
diff --git a/python/vyos/qos/roundrobin.py b/python/vyos/qos/roundrobin.py
new file mode 100644
index 000000000..80814ddfb
--- /dev/null
+++ b/python/vyos/qos/roundrobin.py
@@ -0,0 +1,44 @@
+# Copyright 2022 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
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+from vyos.qos.base import QoSBase
+
+class RoundRobin(QoSBase):
+ _parent = 1
+
+ # https://man7.org/linux/man-pages/man8/tc-drr.8.html
+ def update(self, config, direction):
+ tmp = f'tc qdisc add dev {self._interface} root handle 1: drr'
+ self._cmd(tmp)
+
+ if 'class' in config:
+ for cls in config['class']:
+ cls = int(cls)
+ tmp = f'tc class replace dev {self._interface} parent 1:1 classid 1:{cls:x} drr'
+ self._cmd(tmp)
+
+ tmp = f'tc qdisc replace dev {self._interface} parent 1:{cls:x} pfifo'
+ self._cmd(tmp)
+
+ if 'default' in config:
+ class_id_max = self._get_class_max_id(config)
+ default_cls_id = int(class_id_max) +1
+
+ # class ID via CLI is in range 1-4095, thus 1000 hex = 4096
+ tmp = f'tc class replace dev {self._interface} parent 1:1 classid 1:{default_cls_id:x} drr'
+ self._cmd(tmp)
+
+ # call base class
+ super().update(config, direction, priority=True)
diff --git a/python/vyos/qos/trafficshaper.py b/python/vyos/qos/trafficshaper.py
new file mode 100644
index 000000000..f42f4d022
--- /dev/null
+++ b/python/vyos/qos/trafficshaper.py
@@ -0,0 +1,106 @@
+# Copyright 2022 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
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+from math import ceil
+from vyos.qos.base import QoSBase
+
+# Kernel limits on quantum (bytes)
+MAXQUANTUM = 200000
+MINQUANTUM = 1000
+
+class TrafficShaper(QoSBase):
+ _parent = 1
+
+ # https://man7.org/linux/man-pages/man8/tc-htb.8.html
+ def update(self, config, direction):
+ class_id_max = 0
+ if 'class' in config:
+ tmp = list(config['class'])
+ tmp.sort()
+ class_id_max = tmp[-1]
+
+ r2q = 10
+ # bandwidth is a mandatory CLI node
+ speed = self._rate_convert(config['bandwidth'])
+ speed_bps = int(speed) // 8
+
+ # need a bigger r2q if going fast than 16 mbits/sec
+ if (speed_bps // r2q) >= MAXQUANTUM: # integer division
+ r2q = ceil(speed_bps // MAXQUANTUM)
+ else:
+ # if there is a slow class then may need smaller value
+ if 'class' in config:
+ min_speed = speed_bps
+ for cls, cls_options in config['class'].items():
+ # find class with the lowest bandwidth used
+ if 'bandwidth' in cls_options:
+ bw_bps = int(self._rate_convert(cls_options['bandwidth'])) // 8 # bandwidth in bytes per second
+ if bw_bps < min_speed:
+ min_speed = bw_bps
+
+ while (r2q > 1) and (min_speed // r2q) < MINQUANTUM:
+ tmp = r2q -1
+ if (speed_bps // tmp) >= MAXQUANTUM:
+ break
+ r2q = tmp
+
+
+ default_minor_id = int(class_id_max) +1
+ tmp = f'tc qdisc replace dev {self._interface} root handle {self._parent:x}: htb r2q {r2q} default {default_minor_id:x}' # default is in hex
+ self._cmd(tmp)
+
+ tmp = f'tc class replace dev {self._interface} parent {self._parent:x}: classid {self._parent:x}:1 htb rate {speed}'
+ self._cmd(tmp)
+
+ if 'class' in config:
+ for cls, cls_config in config['class'].items():
+ # class id is used later on and passed as hex, thus this needs to be an int
+ cls = int(cls)
+
+ # bandwidth is a mandatory CLI node
+ rate = self._rate_convert(cls_config['bandwidth'])
+ burst = cls_config['burst']
+ quantum = cls_config['codel_quantum']
+
+ tmp = f'tc class replace dev {self._interface} parent {self._parent:x}:1 classid {self._parent:x}:{cls:x} htb rate {rate} burst {burst} quantum {quantum}'
+ if 'priority' in cls_config:
+ priority = cls_config['priority']
+ tmp += f' prio {priority}'
+ self._cmd(tmp)
+
+ tmp = f'tc qdisc replace dev {self._interface} parent {self._parent:x}:{cls:x} sfq'
+ self._cmd(tmp)
+
+ if 'default' in config:
+ rate = self._rate_convert(config['default']['bandwidth'])
+ burst = config['default']['burst']
+ quantum = config['default']['codel_quantum']
+ tmp = f'tc class replace dev {self._interface} parent {self._parent:x}:1 classid {self._parent:x}:{default_minor_id:x} htb rate {rate} burst {burst} quantum {quantum}'
+ if 'priority' in config['default']:
+ priority = config['default']['priority']
+ tmp += f' prio {priority}'
+ self._cmd(tmp)
+
+ tmp = f'tc qdisc replace dev {self._interface} parent {self._parent:x}:{default_minor_id:x} sfq'
+ self._cmd(tmp)
+
+ # call base class
+ super().update(config, direction)
+
+class TrafficShaperHFSC(TrafficShaper):
+ def update(self, config, direction):
+ # call base class
+ super().update(config, direction)
+
diff --git a/python/vyos/systemversions.py b/python/vyos/systemversions.py
deleted file mode 100644
index f2da76d4f..000000000
--- a/python/vyos/systemversions.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# Copyright 2019 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
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
-#
-# This library is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this library. If not, see <http://www.gnu.org/licenses/>.
-
-import os
-import re
-import sys
-import vyos.defaults
-from vyos.xml import component_version
-
-# legacy version, reading from the file names in
-# /opt/vyatta/etc/config-migrate/current
-def get_system_versions():
- """
- Get component versions from running system; critical failure if
- unable to read migration directory.
- """
- system_versions = {}
-
- try:
- version_info = os.listdir(vyos.defaults.directories['current'])
- except OSError as err:
- print("OS error: {}".format(err))
- sys.exit(1)
-
- for info in version_info:
- if re.match(r'[\w,-]+@\d+', info):
- pair = info.split('@')
- system_versions[pair[0]] = int(pair[1])
-
- return system_versions
-
-# read from xml cache
-def get_system_component_version():
- return component_version()
diff --git a/python/vyos/template.py b/python/vyos/template.py
index 2a4135f9e..15240f815 100644
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -193,6 +193,16 @@ def dot_colon_to_dash(text):
text = text.replace(".", "-")
return text
+@register_filter('generate_uuid4')
+def generate_uuid4(text):
+ """ Generate random unique ID
+ Example:
+ % uuid4()
+ UUID('958ddf6a-ef14-4e81-8cfb-afb12456d1c5')
+ """
+ from uuid import uuid4
+ return uuid4()
+
@register_filter('netmask_from_cidr')
def netmask_from_cidr(prefix):
""" Take CIDR prefix and convert the prefix length to a "subnet mask".
@@ -476,6 +486,8 @@ def get_esp_ike_cipher(group_config, ike_group=None):
continue
tmp = '{encryption}-{hash}'.format(**proposal)
+ if 'prf' in proposal:
+ tmp += '-' + proposal['prf']
if 'dh_group' in proposal:
tmp += '-' + pfs_lut[ 'dh-group' + proposal['dh_group'] ]
elif 'pfs' in group_config and group_config['pfs'] != 'disable':
diff --git a/python/vyos/util.py b/python/vyos/util.py
index 461df9a6e..66ded464d 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -348,9 +348,11 @@ def colon_separated_to_dict(data_string, uniquekeys=False):
l = l.strip()
if l:
match = re.match(key_value_re, l)
- if match:
+ if match and (len(match.groups()) == 2):
key = match.groups()[0].strip()
value = match.groups()[1].strip()
+ else:
+ raise ValueError(f"""Line "{l}" could not be parsed a colon-separated pair """, l)
if key in data.keys():
if uniquekeys:
raise ValueError("Data string has duplicate keys: {0}".format(key))
@@ -486,7 +488,7 @@ def is_listen_port_bind_service(port: int, service: str) -> bool:
Example:
% is_listen_port_bind_service(443, 'nginx')
True
- % is_listen_port_bind_service(443, 'ocservr-main')
+ % is_listen_port_bind_service(443, 'ocserv-main')
False
"""
from psutil import net_connections as connections
@@ -539,13 +541,16 @@ def seconds_to_human(s, separator=""):
return result
-def bytes_to_human(bytes, initial_exponent=0):
+def bytes_to_human(bytes, initial_exponent=0, precision=2):
""" Converts a value in bytes to a human-readable size string like 640 KB
The initial_exponent parameter is the exponent of 2,
e.g. 10 (1024) for kilobytes, 20 (1024 * 1024) for megabytes.
"""
+ if bytes == 0:
+ return "0 B"
+
from math import log2
bytes = bytes * (2**initial_exponent)
@@ -571,9 +576,40 @@ def bytes_to_human(bytes, initial_exponent=0):
# Add a new case when the first machine with petabyte RAM
# hits the market.
- size_string = "{0:.2f} {1}".format(value, suffix)
+ size_string = "{0:.{1}f} {2}".format(value, precision, suffix)
return size_string
+def human_to_bytes(value):
+ """ Converts a data amount with a unit suffix to bytes, like 2K to 2048 """
+
+ from re import match as re_match
+
+ res = re_match(r'^\s*(\d+(?:\.\d+)?)\s*([a-zA-Z]+)\s*$', value)
+
+ if not res:
+ raise ValueError(f"'{value}' is not a valid data amount")
+ else:
+ amount = float(res.group(1))
+ unit = res.group(2).lower()
+
+ if unit == 'b':
+ res = amount
+ elif (unit == 'k') or (unit == 'kb'):
+ res = amount * 1024
+ elif (unit == 'm') or (unit == 'mb'):
+ res = amount * 1024**2
+ elif (unit == 'g') or (unit == 'gb'):
+ res = amount * 1024**3
+ elif (unit == 't') or (unit == 'tb'):
+ res = amount * 1024**4
+ else:
+ raise ValueError(f"Unsupported data unit '{unit}'")
+
+ # There cannot be fractional bytes, so we convert them to integer.
+ # However, truncating causes problems with conversion back to human unit,
+ # so we round instead -- that seems to work well enough.
+ return round(res)
+
def get_cfg_group_id():
from grp import getgrnam
from vyos.defaults import cfg_group
@@ -1105,3 +1141,18 @@ def sysctl_write(name, value):
call(f'sysctl -wq {name}={value}')
return True
return False
+
+# approach follows a discussion in:
+# https://stackoverflow.com/questions/1175208/elegant-python-function-to-convert-camelcase-to-snake-case
+def camel_to_snake_case(name: str) -> str:
+ pattern = r'\d+|[A-Z]?[a-z]+|\W|[A-Z]{2,}(?=[A-Z][a-z]|\d|\W|$)'
+ words = re.findall(pattern, name)
+ return '_'.join(map(str.lower, words))
+
+def load_as_module(name: str, path: str):
+ import importlib.util
+
+ spec = importlib.util.spec_from_file_location(name, path)
+ mod = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(mod)
+ return mod