diff options
Diffstat (limited to 'python')
-rw-r--r-- | python/vyos/base.py | 2 | ||||
-rw-r--r-- | python/vyos/compose_config.py | 84 | ||||
-rw-r--r-- | python/vyos/config_mgmt.py | 2 | ||||
-rw-r--r-- | python/vyos/configsession.py | 6 | ||||
-rw-r--r-- | python/vyos/configtree.py | 10 | ||||
-rw-r--r-- | python/vyos/defaults.py | 3 | ||||
-rw-r--r-- | python/vyos/ifconfig/interface.py | 12 | ||||
-rw-r--r-- | python/vyos/ifconfig/vxlan.py | 7 | ||||
-rw-r--r-- | python/vyos/nat.py | 6 | ||||
-rw-r--r-- | python/vyos/qos/base.py | 11 | ||||
-rw-r--r-- | python/vyos/system/image.py | 10 | ||||
-rw-r--r-- | python/vyos/utils/io.py | 2 | ||||
-rw-r--r-- | python/vyos/version.py | 12 |
13 files changed, 142 insertions, 25 deletions
diff --git a/python/vyos/base.py b/python/vyos/base.py index 9b93cb2f2..054b1d837 100644 --- a/python/vyos/base.py +++ b/python/vyos/base.py @@ -41,7 +41,7 @@ class BaseWarning: isfirstmessage = False initial_indent = self.standardindent print(f'{mes}') - print('') + print('', flush=True) class Warning(): diff --git a/python/vyos/compose_config.py b/python/vyos/compose_config.py new file mode 100644 index 000000000..efa28babe --- /dev/null +++ b/python/vyos/compose_config.py @@ -0,0 +1,84 @@ +# Copyright 2024 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/>. + +"""This module allows iterating over function calls to modify an existing +config. +""" + +from pathlib import Path +from typing import TypeAlias, Union, Callable + +from vyos.configtree import ConfigTree +from vyos.configtree import deep_copy as ct_deep_copy +from vyos.utils.system import load_as_module + +ConfigObj: TypeAlias = Union[str, ConfigTree] + +class ComposeConfigError(Exception): + """Raised when an error occurs modifying a config object. + """ + +class ComposeConfig: + """Apply function to config tree: for iteration over functions or files. + """ + def __init__(self, config_obj: ConfigObj, checkpoint_file=None): + if isinstance(config_obj, ConfigTree): + self.config_tree = config_obj + else: + self.config_tree = ConfigTree(config_obj) + + self.checkpoint = self.config_tree + self.checkpoint_file = checkpoint_file + + def apply_func(self, func: Callable): + """Apply the function to the config tree. + """ + if not callable(func): + raise ComposeConfigError(f'{func.__name__} is not callable') + + if self.checkpoint_file is not None: + self.checkpoint = ct_deep_copy(self.config_tree) + + try: + func(self.config_tree) + except Exception as e: + self.config_tree = self.checkpoint + raise ComposeConfigError(e) from e + + def apply_file(self, func_file: str, func_name: str): + """Apply named function from file. + """ + try: + mod_name = Path(func_file).stem.replace('-', '_') + mod = load_as_module(mod_name, func_file) + func = getattr(mod, func_name) + except Exception as e: + raise ComposeConfigError(f'Error with {func_file}: {e}') from e + + try: + self.apply_func(func) + except ComposeConfigError as e: + raise ComposeConfigError(f'Error in {func_file}: {e}') from e + + def to_string(self, with_version=False) -> str: + """Return the rendered config tree. + """ + return self.config_tree.to_string(no_version=not with_version) + + def write(self, config_file: str, with_version=False): + """Write the config tree to a file. + """ + config_str = self.to_string(with_version=with_version) + Path(config_file).write_text(config_str) diff --git a/python/vyos/config_mgmt.py b/python/vyos/config_mgmt.py index fc51d781c..70b6ea203 100644 --- a/python/vyos/config_mgmt.py +++ b/python/vyos/config_mgmt.py @@ -283,6 +283,8 @@ Proceed ?''' rollback_ct = self._get_config_tree_revision(rev) try: load(rollback_ct, switch='explicit') + print('Rollback diff has been applied.') + print('Use "compare" to review the changes or "commit" to apply them.') except LoadConfigError as e: raise ConfigMgmtError(e) from e diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index ab7a631bb..beec6010b 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -34,6 +34,8 @@ INSTALL_IMAGE = ['/usr/libexec/vyos/op_mode/image_installer.py', '--action', 'add', '--no-prompt', '--image-path'] REMOVE_IMAGE = ['/usr/libexec/vyos/op_mode/image_manager.py', '--action', 'delete', '--no-prompt', '--image-name'] +SET_DEFAULT_IMAGE = ['/usr/libexec/vyos/op_mode/image_manager.py', + '--action', 'set', '--no-prompt', '--image-name'] 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'] @@ -235,6 +237,10 @@ class ConfigSession(object): out = self.__run_command(REMOVE_IMAGE + [name]) return out + def set_default_image(self, name): + out = self.__run_command(SET_DEFAULT_IMAGE + [name]) + return out + def generate(self, path): out = self.__run_command(GENERATE + path) return out diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py index e4b282d72..afd6e030b 100644 --- a/python/vyos/configtree.py +++ b/python/vyos/configtree.py @@ -175,9 +175,11 @@ class ConfigTree(object): def get_version_string(self): return self.__version - def to_string(self, ordered_values=False): + def to_string(self, ordered_values=False, no_version=False): config_string = self.__to_string(self.__config, ordered_values).decode() config_string = unescape_backslash(config_string) + if no_version: + return config_string config_string = "{0}\n{1}".format(config_string, self.__version) return config_string @@ -482,3 +484,9 @@ class DiffTree: add = self.add.to_commands() delete = self.delete.to_commands(op="delete") return delete + "\n" + add + +def deep_copy(config_tree: ConfigTree) -> ConfigTree: + """An inelegant, but reasonably fast, copy; replace with backend copy + """ + D = DiffTree(None, config_tree) + return D.add diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index 64145a42e..e7cd69a8b 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -25,6 +25,7 @@ directories = { 'services' : f'{base_dir}/services', 'config' : '/opt/vyatta/etc/config', 'migrate' : '/opt/vyatta/etc/config-migrate/migrate', + 'activate' : f'{base_dir}/activate', 'log' : '/var/log/vyatta', 'templates' : '/usr/share/vyos/templates/', 'certbot' : '/config/auth/letsencrypt', @@ -46,3 +47,5 @@ cfg_vintage = 'vyos' commit_lock = '/opt/vyatta/config/.lock' component_version_json = os.path.join(directories['data'], 'component-versions.json') + +config_default = os.path.join(directories['data'], 'config.boot.default') diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index f0897bc21..117479ade 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -42,6 +42,7 @@ from vyos.utils.process import is_systemd_service_active from vyos.utils.process import run from vyos.template import is_ipv4 from vyos.template import is_ipv6 +from vyos.utils.file import read_file from vyos.utils.network import is_intf_addr_assigned from vyos.utils.network import is_ipv6_link_local from vyos.utils.assertion import assert_boolean @@ -1356,12 +1357,13 @@ class Interface(Control): if enable and 'disable' not in self.config: if dict_search('dhcp_options.host_name', self.config) == None: # read configured system hostname. - # maybe change to vyos hostd client ??? + # maybe change to vyos-hostsd client ??? hostname = 'vyos' - with open('/etc/hostname', 'r') as f: - hostname = f.read().rstrip('\n') - tmp = {'dhcp_options' : { 'host_name' : hostname}} - self.config = dict_merge(tmp, self.config) + hostname_file = '/etc/hostname' + if os.path.isfile(hostname_file): + hostname = read_file(hostname_file) + tmp = {'dhcp_options' : { 'host_name' : hostname}} + self.config = dict_merge(tmp, self.config) render(systemd_override_file, 'dhcp-client/override.conf.j2', self.config) render(dhclient_config_file, 'dhcp-client/ipv4.j2', self.config) diff --git a/python/vyos/ifconfig/vxlan.py b/python/vyos/ifconfig/vxlan.py index bdb48e303..918aea202 100644 --- a/python/vyos/ifconfig/vxlan.py +++ b/python/vyos/ifconfig/vxlan.py @@ -138,10 +138,13 @@ class VXLANIf(Interface): raise ValueError('Value out of range') if 'vlan_to_vni_removed' in self.config: - cur_vni_filter = get_vxlan_vni_filter(self.ifname) + cur_vni_filter = None + if dict_search('parameters.vni_filter', self.config) != None: + cur_vni_filter = get_vxlan_vni_filter(self.ifname) + for vlan, vlan_config in self.config['vlan_to_vni_removed'].items(): # If VNI filtering is enabled, remove matching VNI filter - if dict_search('parameters.vni_filter', self.config) != None: + if cur_vni_filter != None: vni = vlan_config['vni'] if vni in cur_vni_filter: self._cmd(f'bridge vni delete dev {self.ifname} vni {vni}') diff --git a/python/vyos/nat.py b/python/vyos/nat.py index 2ada29add..e54548788 100644 --- a/python/vyos/nat.py +++ b/python/vyos/nat.py @@ -300,12 +300,12 @@ def parse_nat_static_rule(rule_conf, rule_id, nat_type): output.append('counter') - if translation_str: - output.append(translation_str) - if 'log' in rule_conf: output.append(f'log prefix "[{log_prefix}{log_suffix}]"') + if translation_str: + output.append(translation_str) + output.append(f'comment "{log_prefix}"') return " ".join(output) diff --git a/python/vyos/qos/base.py b/python/vyos/qos/base.py index 87927ba9d..98e486e42 100644 --- a/python/vyos/qos/base.py +++ b/python/vyos/qos/base.py @@ -247,9 +247,15 @@ class QoSBase: filter_cmd_base += ' protocol all' if 'match' in cls_config: - is_filtered = False + has_filter = False for index, (match, match_config) in enumerate(cls_config['match'].items(), start=1): filter_cmd = filter_cmd_base + if not has_filter: + for key in ['mark', 'vif', 'ip', 'ipv6']: + if key in match_config: + has_filter = True + break + if self.qostype == 'shaper' and 'prio ' not in filter_cmd: filter_cmd += f' prio {index}' if 'mark' in match_config: @@ -332,13 +338,12 @@ class QoSBase: cls = int(cls) filter_cmd += f' flowid {self._parent:x}:{cls:x}' self._cmd(filter_cmd) - is_filtered = True vlan_expression = "match.*.vif" match_vlan = jmespath.search(vlan_expression, cls_config) if any(tmp in ['exceed', 'bandwidth', 'burst'] for tmp in cls_config) \ - and is_filtered: + and has_filter: # For "vif" "basic match" is used instead of "action police" T5961 if not match_vlan: filter_cmd += f' action police' diff --git a/python/vyos/system/image.py b/python/vyos/system/image.py index ba9a6dfa7..aae52e770 100644 --- a/python/vyos/system/image.py +++ b/python/vyos/system/image.py @@ -18,8 +18,9 @@ from re import compile as re_compile from functools import wraps from tempfile import TemporaryDirectory from typing import TypedDict +from json import loads -from vyos import version +from vyos.defaults import directories from vyos.system import disk, grub # Define variables @@ -201,9 +202,12 @@ def get_running_image() -> str: if running_image_result: running_image: str = running_image_result.groupdict().get( 'image_version', '') - # we need to have a fallback for live systems + # we need to have a fallback for live systems: + # explicit read from version file if not running_image: - running_image: str = version.get_version() + json_data: str = Path(directories['data']).joinpath('version.json').read_text() + dict_data: dict = loads(json_data) + running_image: str = dict_data['version'] return running_image diff --git a/python/vyos/utils/io.py b/python/vyos/utils/io.py index a8c430f28..205210b66 100644 --- a/python/vyos/utils/io.py +++ b/python/vyos/utils/io.py @@ -72,6 +72,8 @@ def ask_yes_no(question, default=False) -> bool: stdout.write("Please respond with yes/y or no/n\n") except EOFError: stdout.write("\nPlease respond with yes/y or no/n\n") + except KeyboardInterrupt: + return False def is_interactive(): """Try to determine if the routine was called from an interactive shell.""" diff --git a/python/vyos/version.py b/python/vyos/version.py index b5ed2705b..86e96d0ec 100644 --- a/python/vyos/version.py +++ b/python/vyos/version.py @@ -33,11 +33,11 @@ import os import requests import vyos.defaults +from vyos.system.image import is_live_boot from vyos.utils.file import read_file from vyos.utils.file import read_json from vyos.utils.process import popen -from vyos.utils.process import run from vyos.utils.process import DEVNULL version_file = os.path.join(vyos.defaults.directories['data'], 'version.json') @@ -81,16 +81,14 @@ def get_full_version_data(fname=version_file): else: version_data['system_type'] = f"{hypervisor} guest" - # Get boot type, it can be livecd, installed image, or, possible, a system installed - # via legacy "install system" mechanism + # Get boot type, it can be livecd or installed image # In installed images, the squashfs image file is named after its image version, # while on livecd it's just "filesystem.squashfs", that's how we tell a livecd boot # from an installed image - boot_via = "installed image" - if run(""" grep -e '^overlay.*/filesystem.squashfs' /proc/mounts >/dev/null""") == 0: + if is_live_boot(): boot_via = "livecd" - elif run(""" grep '^overlay /' /proc/mounts >/dev/null """) != 0: - boot_via = "legacy non-image installation" + else: + boot_via = "installed image" version_data['boot_via'] = boot_via # Get hardware details from DMI |