summaryrefslogtreecommitdiff
path: root/python/vyos
diff options
context:
space:
mode:
Diffstat (limited to 'python/vyos')
-rw-r--r--python/vyos/base.py2
-rw-r--r--python/vyos/compose_config.py84
-rw-r--r--python/vyos/config_mgmt.py2
-rw-r--r--python/vyos/configsession.py6
-rw-r--r--python/vyos/configtree.py10
-rw-r--r--python/vyos/defaults.py3
-rw-r--r--python/vyos/ifconfig/interface.py12
-rw-r--r--python/vyos/ifconfig/vxlan.py7
-rw-r--r--python/vyos/nat.py6
-rw-r--r--python/vyos/qos/base.py11
-rw-r--r--python/vyos/system/image.py10
-rw-r--r--python/vyos/utils/io.py2
-rw-r--r--python/vyos/version.py12
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