summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitattributes5
-rw-r--r--.vscode/settings.json2
-rw-r--r--python/vyos/config_mgmt.py7
-rw-r--r--python/vyos/configtree.py3
-rw-r--r--python/vyos/load_config.py200
-rw-r--r--python/vyos/utils/io.py9
-rwxr-xr-xsrc/migration-scripts/nat/6-to-78
-rwxr-xr-xsrc/op_mode/dhcp.py10
-rwxr-xr-xsrc/op_mode/image_installer.py3
-rwxr-xr-xsrc/op_mode/nat.py6
-rwxr-xr-xsrc/validators/bgp-large-community-list8
11 files changed, 233 insertions, 28 deletions
diff --git a/.gitattributes b/.gitattributes
index 624e00f03..1d79e0804 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -73,8 +73,3 @@
.gitkeep export-ignore
*.pyc binary export-ignore
*.pyo binary export-ignore
-
-#
-# Enable syntax highlighting for VSCode settings
-#
-.vscode/settings.json linguist-language=jsonc
diff --git a/.vscode/settings.json b/.vscode/settings.json
index ab3af6cf1..27ed4c296 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -10,7 +10,7 @@
"*.xml.in": "xml",
},
"[jinja]": {
- "editor.wordBasedSuggestions": false
+ "editor.wordBasedSuggestions": "off"
},
// https://code.visualstudio.com/docs/python/settings-reference
"python.analysis.extraPaths": [
diff --git a/python/vyos/config_mgmt.py b/python/vyos/config_mgmt.py
index 950f14d4f..fd0fa7a75 100644
--- a/python/vyos/config_mgmt.py
+++ b/python/vyos/config_mgmt.py
@@ -125,6 +125,7 @@ class ConfigMgmt:
get_first_key=True)
self.max_revisions = int(d.get('commit_revisions', 0))
+ self.num_revisions = 0
self.locations = d.get('commit_archive', {}).get('location', [])
self.source_address = d.get('commit_archive',
{}).get('source_address', '')
@@ -233,7 +234,7 @@ Proceed ?'''
msg = ''
if not self._check_revision_number(rev):
- msg = f'Invalid revision number {rev}: must be 0 < rev < {self.max_revisions}'
+ msg = f'Invalid revision number {rev}: must be 0 < rev < {self.num_revisions}'
return msg, 1
prompt_str = 'Proceed with reboot ?'
@@ -560,8 +561,8 @@ Proceed ?'''
return len(l)
def _check_revision_number(self, rev: int) -> bool:
- maxrev = self._get_number_of_revisions()
- if not 0 <= rev < maxrev:
+ self.num_revisions = self._get_number_of_revisions()
+ if not 0 <= rev < self.num_revisions:
return False
return True
diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py
index 09cfd43d3..d048901f0 100644
--- a/python/vyos/configtree.py
+++ b/python/vyos/configtree.py
@@ -160,6 +160,9 @@ class ConfigTree(object):
def _get_config(self):
return self.__config
+ def get_version_string(self):
+ return self.__version
+
def to_string(self, ordered_values=False):
config_string = self.__to_string(self.__config, ordered_values).decode()
config_string = "{0}\n{1}".format(config_string, self.__version)
diff --git a/python/vyos/load_config.py b/python/vyos/load_config.py
new file mode 100644
index 000000000..af563614d
--- /dev/null
+++ b/python/vyos/load_config.py
@@ -0,0 +1,200 @@
+# 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/>.
+
+"""This module abstracts the loading of a config file into the running
+config. It provides several varieties of loading a config file, from the
+legacy version to the developing versions, as a means of offering
+alternatives for competing use cases, and a base for profiling the
+performance of each.
+"""
+
+import sys
+from pathlib import Path
+from tempfile import NamedTemporaryFile
+from typing import Union, Literal, TypeAlias, get_type_hints, get_args
+
+from vyos.config import Config
+from vyos.configtree import ConfigTree, DiffTree
+from vyos.configsource import ConfigSourceSession, VyOSError
+from vyos.component_version import from_string as version_from_string
+from vyos.component_version import from_system as version_from_system
+from vyos.migrator import Migrator, VirtualMigrator, MigratorError
+from vyos.utils.process import popen, DEVNULL
+
+Variety: TypeAlias = Literal['explicit', 'batch', 'tree', 'legacy']
+ConfigObj: TypeAlias = Union[str, ConfigTree]
+
+thismod = sys.modules[__name__]
+
+class LoadConfigError(Exception):
+ """Raised when an error occurs loading a config file.
+ """
+
+# utility functions
+
+def get_running_config(config: Config) -> ConfigTree:
+ return config.get_config_tree(effective=True)
+
+def get_proposed_config(config_file: str = None) -> ConfigTree:
+ config_str = Path(config_file).read_text()
+ return ConfigTree(config_str)
+
+def migration_needed(config_obj: ConfigObj) -> bool:
+ """Check if a migration is needed for the config object.
+ """
+ if not isinstance(config_obj, ConfigTree):
+ atree = get_proposed_config(config_obj)
+ else:
+ atree = config_obj
+ version_str = atree.get_version_string()
+ if not version_str:
+ return True
+ aversion = version_from_string(version_str.splitlines()[1])
+ bversion = version_from_system()
+ return aversion != bversion
+
+def check_session(strict: bool, switch: Variety) -> None:
+ """Check if we are in a config session, with no uncommitted changes, if
+ strict. This is not needed for legacy load, as these checks are
+ implicit.
+ """
+
+ if switch == 'legacy':
+ return
+
+ context = ConfigSourceSession()
+
+ if not context.in_session():
+ raise LoadConfigError('not in a config session')
+
+ if strict and context.session_changed():
+ raise LoadConfigError('commit or discard changes before loading config')
+
+# methods to call for each variety
+
+# explicit
+def diff_to_commands(ctree: ConfigTree, ntree: ConfigTree) -> list:
+ """Calculate the diff between the current and proposed config."""
+ # Calculate the diff between the current and new config tree
+ commands = DiffTree(ctree, ntree).to_commands()
+ # on an empty set of 'add' or 'delete' commands, to_commands
+ # returns '\n'; prune below
+ command_list = commands.splitlines()
+ command_list = [c for c in command_list if c]
+ return command_list
+
+def set_commands(cmds: list) -> None:
+ """Set commands in the config session."""
+ if not cmds:
+ print('no commands to set')
+ return
+ error_out = []
+ for op in cmds:
+ out, rc = popen(f'/opt/vyatta/sbin/my_{op}', shell=True, stderr=DEVNULL)
+ if rc != 0:
+ error_out.append(out)
+ continue
+ if error_out:
+ out = '\n'.join(error_out)
+ raise LoadConfigError(out)
+
+# legacy
+class LoadConfig(ConfigSourceSession):
+ """A subclass for calling 'loadFile'.
+ """
+ def load_config(self, file_name):
+ return self._run(['/bin/cli-shell-api','loadFile', file_name])
+
+# end methods to call for each variety
+
+def migrate(config_obj: ConfigObj) -> ConfigObj:
+ """Migrate a config object to the current version.
+ """
+ if isinstance(config_obj, ConfigTree):
+ config_file = NamedTemporaryFile(delete=False).name
+ Path(config_file).write_text(config_obj.to_string())
+ else:
+ config_file = config_obj
+
+ virtual_migration = VirtualMigrator(config_file)
+ migration = Migrator(config_file)
+ try:
+ virtual_migration.run()
+ migration.run()
+ except MigratorError as e:
+ raise LoadConfigError(e) from e
+ else:
+ if isinstance(config_obj, ConfigTree):
+ return ConfigTree(Path(config_file).read_text())
+ return config_file
+ finally:
+ if isinstance(config_obj, ConfigTree):
+ Path(config_file).unlink()
+
+def load_explicit(config_obj: ConfigObj):
+ """Explicit load from file or configtree.
+ """
+ config = Config()
+ ctree = get_running_config(config)
+ if isinstance(config_obj, ConfigTree):
+ ntree = config_obj
+ else:
+ ntree = get_proposed_config(config_obj)
+ # Calculate the diff between the current and proposed config
+ cmds = diff_to_commands(ctree, ntree)
+ # Set the commands in the config session
+ set_commands(cmds)
+
+def load_batch(config_obj: ConfigObj):
+ # requires legacy backend patch
+ raise NotImplementedError('batch loading not implemented')
+
+def load_tree(config_obj: ConfigObj):
+ # requires vyconf backend patch
+ raise NotImplementedError('tree loading not implemented')
+
+def load_legacy(config_obj: ConfigObj):
+ """Legacy load from file or configtree.
+ """
+ if isinstance(config_obj, ConfigTree):
+ config_file = NamedTemporaryFile(delete=False).name
+ Path(config_file).write_text(config_obj.to_string())
+ else:
+ config_file = config_obj
+
+ config = LoadConfig()
+
+ try:
+ config.load_config(config_file)
+ except VyOSError as e:
+ raise LoadConfigError(e) from e
+ finally:
+ if isinstance(config_obj, ConfigTree):
+ Path(config_file).unlink()
+
+def load(config_obj: ConfigObj, strict: bool = True,
+ switch: Variety = 'legacy'):
+ type_hints = get_type_hints(load)
+ switch_choice = get_args(type_hints['switch'])
+ if switch not in switch_choice:
+ raise ValueError(f'invalid switch: {switch}')
+
+ check_session(strict, switch)
+
+ if migration_needed(config_obj):
+ config_obj = migrate(config_obj)
+
+ func = getattr(thismod, f'load_{switch}')
+ func(config_obj)
diff --git a/python/vyos/utils/io.py b/python/vyos/utils/io.py
index 74099b502..0afaf695c 100644
--- a/python/vyos/utils/io.py
+++ b/python/vyos/utils/io.py
@@ -26,13 +26,18 @@ def print_error(str='', end='\n'):
sys.stderr.write(end)
sys.stderr.flush()
-def ask_input(question, default='', numeric_only=False, valid_responses=[]):
+def ask_input(question, default='', numeric_only=False, valid_responses=[],
+ no_echo=False):
+ from getpass import getpass
question_out = question
if default:
question_out += f' (Default: {default})'
response = ''
while True:
- response = input(question_out + ' ').strip()
+ if not no_echo:
+ response = input(question_out + ' ').strip()
+ else:
+ response = getpass(question_out + ' ').strip()
if not response and default:
return default
if numeric_only:
diff --git a/src/migration-scripts/nat/6-to-7 b/src/migration-scripts/nat/6-to-7
index b5f6328ef..a2e735394 100755
--- a/src/migration-scripts/nat/6-to-7
+++ b/src/migration-scripts/nat/6-to-7
@@ -21,6 +21,7 @@
# to
# 'set nat [source|destination] rule X [inbound-interface|outbound interface] name <iface>'
# 'set nat [source|destination] rule X [inbound-interface|outbound interface] group <iface_group>'
+# Also remove command if interface == any
from sys import argv,exit
from vyos.configtree import ConfigTree
@@ -56,8 +57,11 @@ for direction in ['source', 'destination']:
if config.exists(base + [iface]):
if config.exists(base + [iface, 'interface-name']):
tmp = config.return_value(base + [iface, 'interface-name'])
- config.delete(base + [iface, 'interface-name'])
- config.set(base + [iface, 'name'], value=tmp)
+ if tmp != 'any':
+ config.delete(base + [iface, 'interface-name'])
+ config.set(base + [iface, 'name'], value=tmp)
+ else:
+ config.delete(base + [iface])
try:
with open(file_name, 'w') as f:
diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py
index bd2c522ca..a9271ea79 100755
--- a/src/op_mode/dhcp.py
+++ b/src/op_mode/dhcp.py
@@ -102,11 +102,11 @@ def _get_raw_server_leases(family='inet', pool=None, sorted=None, state=[], orig
if family == 'inet':
data_lease['mac'] = lease['hwaddr']
- data_lease['start'] = lease['start_timestamp']
+ data_lease['start'] = lease['start_timestamp'].timestamp()
data_lease['hostname'] = lease['hostname']
if family == 'inet6':
- data_lease['last_communication'] = lease['start_timestamp']
+ data_lease['last_communication'] = lease['start_timestamp'].timestamp()
data_lease['iaid_duid'] = _format_hex_string(lease['duid'])
lease_types_long = {'0': 'non-temporary', '1': 'temporary', '2': 'prefix delegation'}
data_lease['type'] = lease_types_long[lease['lease_type']]
@@ -123,7 +123,7 @@ def _get_raw_server_leases(family='inet', pool=None, sorted=None, state=[], orig
# Do not add old leases
if data_lease['remaining'] != '' and data_lease['pool'] in pool and data_lease['state'] != 'free':
- if not state or data_lease['state'] in state:
+ if not state or state == 'all' or data_lease['state'] in state:
data.append(data_lease)
# deduplicate
@@ -151,7 +151,7 @@ def _get_formatted_server_leases(raw_data, family='inet'):
ipaddr = lease.get('ip')
hw_addr = lease.get('mac')
state = lease.get('state')
- start = lease.get('start').timestamp()
+ start = lease.get('start')
start = _utc_to_local(start).strftime('%Y/%m/%d %H:%M:%S')
end = lease.get('end')
end = _utc_to_local(end).strftime('%Y/%m/%d %H:%M:%S') if end else '-'
@@ -168,7 +168,7 @@ def _get_formatted_server_leases(raw_data, family='inet'):
for lease in raw_data:
ipaddr = lease.get('ip')
state = lease.get('state')
- start = lease.get('last_communication').timestamp()
+ start = lease.get('last_communication')
start = _utc_to_local(start).strftime('%Y/%m/%d %H:%M:%S')
end = lease.get('end')
end = _utc_to_local(end).strftime('%Y/%m/%d %H:%M:%S')
diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py
index b3e6e518c..9452c5e28 100755
--- a/src/op_mode/image_installer.py
+++ b/src/op_mode/image_installer.py
@@ -611,7 +611,8 @@ def install_image() -> None:
print(MSG_WARN_IMAGE_NAME_WRONG)
# ask for password
- user_password: str = ask_input(MSG_INPUT_PASSWORD, default='vyos')
+ user_password: str = ask_input(MSG_INPUT_PASSWORD, default='vyos',
+ no_echo=True)
# ask for default console
console_type: str = ask_input(MSG_INPUT_CONSOLE_TYPE,
diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py
index 71a40c0e1..2bc7e24fe 100755
--- a/src/op_mode/nat.py
+++ b/src/op_mode/nat.py
@@ -28,9 +28,6 @@ from vyos.configquery import ConfigTreeQuery
from vyos.utils.process import cmd
from vyos.utils.dict import dict_search
-base = 'nat'
-unconf_message = 'NAT is not configured'
-
ArgDirection = typing.Literal['source', 'destination']
ArgFamily = typing.Literal['inet', 'inet6']
@@ -293,8 +290,9 @@ def _verify(func):
@wraps(func)
def _wrapper(*args, **kwargs):
config = ConfigTreeQuery()
+ base = 'nat66' if 'inet6' in sys.argv[1:] else 'nat'
if not config.exists(base):
- raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
+ raise vyos.opmode.UnconfiguredSubsystem(f'{base.upper()} is not configured')
return func(*args, **kwargs)
return _wrapper
diff --git a/src/validators/bgp-large-community-list b/src/validators/bgp-large-community-list
index 80112dfdc..9ba5b27eb 100755
--- a/src/validators/bgp-large-community-list
+++ b/src/validators/bgp-large-community-list
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# Copyright (C) 2021-2023 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
@@ -17,9 +17,8 @@
import re
import sys
-from vyos.template import is_ipv4
-
pattern = '(.*):(.*):(.*)'
+allowedChars = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '+', '*', '?', '^', '$', '(', ')', '[', ']', '{', '}', '|', '\\', ':', '-' }
if __name__ == '__main__':
if len(sys.argv) != 2:
@@ -29,8 +28,7 @@ if __name__ == '__main__':
if not len(value) == 3:
sys.exit(1)
- if not (re.match(pattern, sys.argv[1]) and
- (is_ipv4(value[0]) or value[0].isdigit()) and (value[1].isdigit() or value[1] == '*')):
+ if not (re.match(pattern, sys.argv[1]) and set(sys.argv[1]).issubset(allowedChars)):
sys.exit(1)
sys.exit(0)