diff options
-rw-r--r-- | data/templates/high-availability/keepalived.conf.j2 | 3 | ||||
-rw-r--r-- | data/templates/pppoe/peer.j2 | 7 | ||||
-rw-r--r-- | debian/vyos-1x.install | 1 | ||||
-rw-r--r-- | interface-definitions/container.xml.in | 20 | ||||
-rw-r--r-- | interface-definitions/high-availability.xml.in | 19 | ||||
-rw-r--r-- | interface-definitions/interfaces-pppoe.xml.in | 13 | ||||
-rw-r--r-- | interface-definitions/system-config-mgmt.xml.in | 57 | ||||
-rw-r--r-- | op-mode-definitions/monitor-log.xml.in | 20 | ||||
-rw-r--r-- | op-mode-definitions/show-log.xml.in | 20 | ||||
-rw-r--r-- | op-mode-definitions/show-system.xml.in | 36 | ||||
-rw-r--r-- | python/setup.py | 5 | ||||
-rw-r--r-- | python/vyos/config_mgmt.py | 674 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_ha_vrrp.py | 9 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_interfaces_pppoe.py | 77 | ||||
-rwxr-xr-x | src/conf_mode/config_mgmt.py | 96 | ||||
-rwxr-xr-x | src/conf_mode/container.py | 16 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-pppoe.py | 3 | ||||
-rwxr-xr-x | src/op_mode/config_mgmt.py | 85 | ||||
-rwxr-xr-x | src/op_mode/lldp.py | 13 |
19 files changed, 1143 insertions, 31 deletions
diff --git a/data/templates/high-availability/keepalived.conf.j2 b/data/templates/high-availability/keepalived.conf.j2 index ebff52e1f..828636dc0 100644 --- a/data/templates/high-availability/keepalived.conf.j2 +++ b/data/templates/high-availability/keepalived.conf.j2 @@ -5,6 +5,9 @@ global_defs { dynamic_interfaces script_user root +{% if vrrp.global_parameters.startup_delay is vyos_defined %} + vrrp_startup_delay {{ vrrp.global_parameters.startup_delay }} +{% endif %} notify_fifo /run/keepalived/keepalived_notify_fifo notify_fifo_script /usr/libexec/vyos/system/keepalived-fifo.py } diff --git a/data/templates/pppoe/peer.j2 b/data/templates/pppoe/peer.j2 index 6221abb9b..f433a9b03 100644 --- a/data/templates/pppoe/peer.j2 +++ b/data/templates/pppoe/peer.j2 @@ -36,10 +36,13 @@ maxfail 0 plugin rp-pppoe.so {{ source_interface }} {% if access_concentrator is vyos_defined %} -rp_pppoe_ac '{{ access_concentrator }}' +pppoe-ac "{{ access_concentrator }}" {% endif %} {% if service_name is vyos_defined %} -rp_pppoe_service '{{ service_name }}' +pppoe-service "{{ service_name }}" +{% endif %} +{% if host_uniq is vyos_defined %} +pppoe-host-uniq "{{ host_uniq }}" {% endif %} persist diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install index a54b3b506..11b488b22 100644 --- a/debian/vyos-1x.install +++ b/debian/vyos-1x.install @@ -18,6 +18,7 @@ etc/vyos lib/ opt/ usr/sbin +usr/bin/config-mgmt usr/bin/initial-setup usr/bin/vyos-config-file-query usr/bin/vyos-config-to-commands diff --git a/interface-definitions/container.xml.in b/interface-definitions/container.xml.in index 4bac305d1..b61664125 100644 --- a/interface-definitions/container.xml.in +++ b/interface-definitions/container.xml.in @@ -274,6 +274,26 @@ </valueHelp> </properties> </leafNode> + <leafNode name="mode"> + <properties> + <help>Volume access mode ro/rw</help> + <completionHelp> + <list>ro rw</list> + </completionHelp> + <valueHelp> + <format>ro</format> + <description>Volume mounted into the container as read-only</description> + </valueHelp> + <valueHelp> + <format>rw</format> + <description>Volume mounted into the container as read-write</description> + </valueHelp> + <constraint> + <regex>(ro|rw)</regex> + </constraint> + </properties> + <defaultValue>rw</defaultValue> + </leafNode> </children> </tagNode> </children> diff --git a/interface-definitions/high-availability.xml.in b/interface-definitions/high-availability.xml.in index d67a142d1..37cb90a8d 100644 --- a/interface-definitions/high-availability.xml.in +++ b/interface-definitions/high-availability.xml.in @@ -11,6 +11,25 @@ <help>Virtual Router Redundancy Protocol settings</help> </properties> <children> + <node name="global-parameters"> + <properties> + <help>VRRP global parameters</help> + </properties> + <children> + <leafNode name="startup-delay"> + <properties> + <help>Time VRRP startup process (in seconds)</help> + <valueHelp> + <format>u32:1-600</format> + <description>Interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-600"/> + </constraint> + </properties> + </leafNode> + </children> + </node> <tagNode name="group"> <properties> <help>VRRP group</help> diff --git a/interface-definitions/interfaces-pppoe.xml.in b/interface-definitions/interfaces-pppoe.xml.in index 490f41471..c6fd7096b 100644 --- a/interface-definitions/interfaces-pppoe.xml.in +++ b/interface-definitions/interfaces-pppoe.xml.in @@ -37,6 +37,19 @@ <constraintErrorMessage>Timeout must be in range 0 to 86400</constraintErrorMessage> </properties> </leafNode> + <leafNode name="host-uniq"> + <properties> + <help>PPPoE RFC2516 host-uniq tag</help> + <valueHelp> + <format>txt</format> + <description>Host-uniq tag as byte string in HEX</description> + </valueHelp> + <constraint> + <regex>([a-fA-F0-9][a-fA-F0-9]){1,18}</regex> + </constraint> + <constraintErrorMessage>Host-uniq must be specified as hex-adecimal byte-string (even number of HEX characters)</constraintErrorMessage> + </properties> + </leafNode> <node name="ip"> <properties> <help>IPv4 routing parameters</help> diff --git a/interface-definitions/system-config-mgmt.xml.in b/interface-definitions/system-config-mgmt.xml.in new file mode 100644 index 000000000..91caed01a --- /dev/null +++ b/interface-definitions/system-config-mgmt.xml.in @@ -0,0 +1,57 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="system"> + <children> + <node name="config-management" owner="${vyos_conf_scripts_dir}/config_mgmt.py"> + <properties> + <help>Configuration management settings</help> + </properties> + <children> + <node name="commit-archive"> + <properties> + <help>Commit archive settings</help> + </properties> + <children> + <leafNode name="location"> + <properties> + <help>Commit archive location</help> + <valueHelp> + <format>uri</format> + <description>Uniform Resource Identifier</description> + </valueHelp> + <constraint> + <validator name="url --file-transport"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="source-address"> + <properties> + <help>Source address or interface for archive server connections</help> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + #include <include/constraint/interface-name.xml.in> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="commit-revisions"> + <properties> + <help>Commit revisions</help> + <valueHelp> + <format>u32:1-65535</format> + <description>Number of config backups to keep</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + <constraintErrorMessage>Number of revisions must be between 0 and 65535</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/monitor-log.xml.in b/op-mode-definitions/monitor-log.xml.in index b68047bb9..ec428a676 100644 --- a/op-mode-definitions/monitor-log.xml.in +++ b/op-mode-definitions/monitor-log.xml.in @@ -93,6 +93,12 @@ </properties> <command>journalctl --no-hostname --boot --follow --unit uacctd.service</command> </leafNode> + <leafNode name="ipoe-server"> + <properties> + <help>Monitor last lines of IPoE server log</help> + </properties> + <command>journalctl --no-hostname --boot --follow --unit accel-ppp@ipoe.service</command> + </leafNode> <leafNode name="kernel"> <properties> <help>Monitor last lines of Linux Kernel log</help> @@ -113,7 +119,7 @@ </leafNode> <node name="pppoe"> <properties> - <help>Monitor last lines of PPPoE log</help> + <help>Monitor last lines of PPPoE interface log</help> </properties> <command>journalctl --no-hostname --boot --follow --unit "ppp@pppoe*.service"</command> <children> @@ -121,13 +127,19 @@ <properties> <help>Monitor last lines of PPPoE log for specific interface</help> <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py -t pppoe</script> + <path>interfaces pppoe</path> </completionHelp> </properties> <command>journalctl --no-hostname --boot --follow --unit "ppp@$5.service"</command> </tagNode> </children> </node> + <leafNode name="pppoe-server"> + <properties> + <help>Monitor last lines of PPPoE server log</help> + </properties> + <command>journalctl --no-hostname --boot --follow --unit accel-ppp@pppoe.service</command> + </leafNode> <node name="protocol"> <properties> <help>Monitor log for Routing Protocol</help> @@ -211,7 +223,7 @@ <properties> <help>Monitor last lines of specific MACsec interface</help> <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py -t macsec</script> + <path>interfaces macsec</path> </completionHelp> </properties> <command>SRC=$(cli-shell-api returnValue interfaces macsec "$5" source-interface); journalctl --no-hostname --boot --follow --unit "wpa_supplicant-macsec@$SRC.service"</command> @@ -246,7 +258,7 @@ <properties> <help>Monitor last lines of SSTP client log for specific interface</help> <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py -t sstpc</script> + <path>interfaces sstpc</path> </completionHelp> </properties> <command>journalctl --no-hostname --boot --follow --unit "ppp@$5.service"</command> diff --git a/op-mode-definitions/show-log.xml.in b/op-mode-definitions/show-log.xml.in index 8114f7377..f5e5b1493 100644 --- a/op-mode-definitions/show-log.xml.in +++ b/op-mode-definitions/show-log.xml.in @@ -196,6 +196,12 @@ </tagNode> </children> </tagNode> + <leafNode name="ipoe-server"> + <properties> + <help>Show log for IPoE server</help> + </properties> + <command>journalctl --no-hostname --boot --unit accel-ppp@ipoe.service</command> + </leafNode> <leafNode name="kernel"> <properties> <help>Show log for Linux Kernel</help> @@ -236,7 +242,7 @@ <properties> <help>Show MACsec log on specific interface</help> <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py -t macsec</script> + <path>interfaces macsec</path> </completionHelp> </properties> <command>SRC=$(cli-shell-api returnValue interfaces macsec "$5" source-interface); journalctl --no-hostname --boot --unit "wpa_supplicant-macsec@$SRC.service"</command> @@ -262,7 +268,7 @@ </node> <node name="pppoe"> <properties> - <help>Show log for PPPoE</help> + <help>Show log for PPPoE interface</help> </properties> <command>journalctl --no-hostname --boot --unit "ppp@pppoe*.service"</command> <children> @@ -270,13 +276,19 @@ <properties> <help>Show PPPoE log on specific interface</help> <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py -t pppoe</script> + <path>interfaces pppoe</path> </completionHelp> </properties> <command>journalctl --no-hostname --boot --unit "ppp@$5.service"</command> </tagNode> </children> </node> + <leafNode name="pppoe-server"> + <properties> + <help>Show log for PPPoE server</help> + </properties> + <command>journalctl --no-hostname --boot --unit accel-ppp@pppoe.service</command> + </leafNode> <node name="protocol"> <properties> <help>Show log for Routing Protocol</help> @@ -378,7 +390,7 @@ <properties> <help>Show SSTP client log on specific interface</help> <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py -t sstpc</script> + <path>interfaces sstpc</path> </completionHelp> </properties> <command>journalctl --no-hostname --boot --unit "ppp@$5.service"</command> diff --git a/op-mode-definitions/show-system.xml.in b/op-mode-definitions/show-system.xml.in index 4a0e6c3b2..746601f43 100644 --- a/op-mode-definitions/show-system.xml.in +++ b/op-mode-definitions/show-system.xml.in @@ -7,6 +7,42 @@ <help>Show system information</help> </properties> <children> + <node name="commit"> + <properties> + <help>Show commit revision log</help> + </properties> + <command>${vyos_op_scripts_dir}/config_mgmt.py show_commit_log</command> + <children> + <tagNode name="diff"> + <properties> + <help>Show commit revision diff</help> + </properties> + <command>${vyos_op_scripts_dir}/config_mgmt.py show_commit_diff --rev "$5"</command> + </tagNode> + <tagNode name="file"> + <properties> + <help>Show commit revision file</help> + </properties> + <command>${vyos_op_scripts_dir}/config_mgmt.py show_commit_file --rev "$5"</command> + <children> + <tagNode name="compare"> + <properties> + <help>Compare config file revisions</help> + </properties> + <command>${vyos_op_scripts_dir}/config_mgmt.py show_commit_diff --rev "$5" --revb "$7"</command> + <children> + <leafNode name="commands"> + <properties> + <help>Compare config file revision commands</help> + </properties> + <command>${vyos_op_scripts_dir}/config_mgmt.py show_commit_diff --rev "$5" --revb "$7" --commands</command> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> <node name="connections"> <properties> <help>Show active network connections on the system</help> 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/config_mgmt.py b/python/vyos/config_mgmt.py new file mode 100644 index 000000000..8ec73ac28 --- /dev/null +++ b/python/vyos/config_mgmt.py @@ -0,0 +1,674 @@ +# 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' + + # 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.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)[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) + func() + 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/smoketest/scripts/cli/test_ha_vrrp.py b/smoketest/scripts/cli/test_ha_vrrp.py index 68905e447..f18a4ab86 100755 --- a/smoketest/scripts/cli/test_ha_vrrp.py +++ b/smoketest/scripts/cli/test_ha_vrrp.py @@ -87,11 +87,13 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase): advertise_interval = '77' priority = '123' preempt_delay = '400' + startup_delay = '120' for group in groups: vlan_id = group.lstrip('VLAN') vip = f'100.64.{vlan_id}.1/24' group_base = base_path + ['vrrp', 'group', group] + global_param_base = base_path + ['vrrp', 'global-parameters'] self.cli_set(['interfaces', 'ethernet', vrrp_interface, 'vif', vlan_id, 'address', inc_ip(vip, 1) + '/' + vip.split('/')[-1]]) @@ -110,6 +112,10 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase): self.cli_set(group_base + ['authentication', 'type', 'plaintext-password']) self.cli_set(group_base + ['authentication', 'password', f'{group}']) + # Global parameters + config = getConfig(f'global_defs') + self.cli_set(global_param_base + ['startup-delay', f'{startup_delay}']) + # commit changes self.cli_commit() @@ -131,6 +137,9 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase): # Authentication self.assertIn(f'auth_pass "{group}"', config) self.assertIn(f'auth_type PASS', config) + # Global parameters + config = getConfig(f'global_defs') + self.assertIn(f'vrrp_startup_delay {startup_delay}', config) def test_03_sync_group(self): sync_group = 'VyOS' diff --git a/smoketest/scripts/cli/test_interfaces_pppoe.py b/smoketest/scripts/cli/test_interfaces_pppoe.py index 8927121a8..08b7f2f46 100755 --- a/smoketest/scripts/cli/test_interfaces_pppoe.py +++ b/smoketest/scripts/cli/test_interfaces_pppoe.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2022 VyOS maintainers and contributors +# Copyright (C) 2019-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 @@ -57,8 +57,8 @@ class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase): def test_01_pppoe_client(self): # Check if PPPoE dialer can be configured and runs for interface in self._interfaces: - user = 'VyOS-user-' + interface - passwd = 'VyOS-passwd-' + interface + user = f'VyOS-user-{interface}' + passwd = f'VyOS-passwd-{interface}' mtu = '1400' self.cli_set(base_path + [interface, 'authentication', 'user', user]) @@ -76,23 +76,26 @@ class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase): # verify configuration file(s) for interface in self._interfaces: - user = 'VyOS-user-' + interface - password = 'VyOS-passwd-' + interface + user = f'VyOS-user-{interface}' + passwd = f'VyOS-passwd-{interface}' tmp = get_config_value(interface, 'mtu')[1] self.assertEqual(tmp, mtu) tmp = get_config_value(interface, 'user')[1].replace('"', '') self.assertEqual(tmp, user) tmp = get_config_value(interface, 'password')[1].replace('"', '') - self.assertEqual(tmp, password) + self.assertEqual(tmp, passwd) tmp = get_config_value(interface, 'ifname')[1] self.assertEqual(tmp, interface) def test_02_pppoe_client_disabled_interface(self): # Check if PPPoE Client can be disabled for interface in self._interfaces: - self.cli_set(base_path + [interface, 'authentication', 'user', 'vyos']) - self.cli_set(base_path + [interface, 'authentication', 'password', 'vyos']) + user = f'VyOS-user-{interface}' + passwd = f'VyOS-passwd-{interface}' + + self.cli_set(base_path + [interface, 'authentication', 'user', user]) + self.cli_set(base_path + [interface, 'authentication', 'password', passwd]) self.cli_set(base_path + [interface, 'source-interface', self._source_interface]) self.cli_set(base_path + [interface, 'disable']) @@ -117,7 +120,10 @@ class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase): def test_03_pppoe_authentication(self): # When username or password is set - so must be the other for interface in self._interfaces: - self.cli_set(base_path + [interface, 'authentication', 'user', 'vyos']) + user = f'VyOS-user-{interface}' + passwd = f'VyOS-passwd-{interface}' + + self.cli_set(base_path + [interface, 'authentication', 'user', user]) self.cli_set(base_path + [interface, 'source-interface', self._source_interface]) self.cli_set(base_path + [interface, 'ipv6', 'address', 'autoconf']) @@ -125,7 +131,7 @@ class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase): with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_set(base_path + [interface, 'authentication', 'password', 'vyos']) + self.cli_set(base_path + [interface, 'authentication', 'password', passwd]) self.cli_commit() @@ -136,8 +142,11 @@ class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase): sla_len = '8' for interface in self._interfaces: - self.cli_set(base_path + [interface, 'authentication', 'user', 'vyos']) - self.cli_set(base_path + [interface, 'authentication', 'password', 'vyos']) + user = f'VyOS-user-{interface}' + passwd = f'VyOS-passwd-{interface}' + + self.cli_set(base_path + [interface, 'authentication', 'user', user]) + self.cli_set(base_path + [interface, 'authentication', 'password', passwd]) self.cli_set(base_path + [interface, 'no-default-route']) self.cli_set(base_path + [interface, 'no-peer-dns']) self.cli_set(base_path + [interface, 'source-interface', self._source_interface]) @@ -149,18 +158,54 @@ class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase): self.cli_set(dhcpv6_pd_base + ['interface', self._source_interface, 'address', address]) self.cli_set(dhcpv6_pd_base + ['interface', self._source_interface, 'sla-id', sla_id]) - # commit changes - self.cli_commit() + # commit changes + self.cli_commit() + + for interface in self._interfaces: + user = f'VyOS-user-{interface}' + passwd = f'VyOS-passwd-{interface}' # verify "normal" PPPoE value - 1492 is default MTU tmp = get_config_value(interface, 'mtu')[1] self.assertEqual(tmp, '1492') tmp = get_config_value(interface, 'user')[1].replace('"', '') - self.assertEqual(tmp, 'vyos') + self.assertEqual(tmp, user) tmp = get_config_value(interface, 'password')[1].replace('"', '') - self.assertEqual(tmp, 'vyos') + self.assertEqual(tmp, passwd) tmp = get_config_value(interface, '+ipv6 ipv6cp-use-ipaddr') self.assertListEqual(tmp, ['+ipv6', 'ipv6cp-use-ipaddr']) + def test_05_pppoe_options(self): + # Check if PPPoE dialer can be configured with DHCPv6-PD + for interface in self._interfaces: + user = f'VyOS-user-{interface}' + passwd = f'VyOS-passwd-{interface}' + ac_name = f'AC{interface}' + service_name = f'SRV{interface}' + host_uniq = 'cafebeefBABE123456' + + self.cli_set(base_path + [interface, 'authentication', 'user', user]) + self.cli_set(base_path + [interface, 'authentication', 'password', passwd]) + self.cli_set(base_path + [interface, 'source-interface', self._source_interface]) + + self.cli_set(base_path + [interface, 'access-concentrator', ac_name]) + self.cli_set(base_path + [interface, 'service-name', service_name]) + self.cli_set(base_path + [interface, 'host-uniq', host_uniq]) + + # commit changes + self.cli_commit() + + for interface in self._interfaces: + ac_name = f'AC{interface}' + service_name = f'SRV{interface}' + host_uniq = 'cafebeefBABE123456' + + tmp = get_config_value(interface, 'pppoe-ac')[1] + self.assertEqual(tmp, f'"{ac_name}"') + tmp = get_config_value(interface, 'pppoe-service')[1] + self.assertEqual(tmp, f'"{service_name}"') + tmp = get_config_value(interface, 'pppoe-host-uniq')[1] + self.assertEqual(tmp, f'"{host_uniq}"') + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/config_mgmt.py b/src/conf_mode/config_mgmt.py new file mode 100755 index 000000000..c681a8405 --- /dev/null +++ b/src/conf_mode/config_mgmt.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 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 +# 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 os +import sys + +from vyos import ConfigError +from vyos.config import Config +from vyos.config_mgmt import ConfigMgmt +from vyos.config_mgmt import commit_post_hook_dir, commit_hooks + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + base = ['system', 'config-management'] + if not conf.exists(base): + return None + + mgmt = ConfigMgmt(config=conf) + + return mgmt + +def verify(_mgmt): + return + +def generate(mgmt): + if mgmt is None: + return + + mgmt.initialize_revision() + +def apply(mgmt): + if mgmt is None: + return + + locations = mgmt.locations + archive_target = os.path.join(commit_post_hook_dir, + commit_hooks['commit_archive']) + if locations: + try: + os.symlink('/usr/bin/config-mgmt', archive_target) + except FileExistsError: + pass + except OSError as exc: + raise ConfigError from exc + else: + try: + os.unlink(archive_target) + except FileNotFoundError: + pass + except OSError as exc: + raise ConfigError from exc + + revisions = mgmt.max_revisions + revision_target = os.path.join(commit_post_hook_dir, + commit_hooks['commit_revision']) + if revisions > 0: + try: + os.symlink('/usr/bin/config-mgmt', revision_target) + except FileExistsError: + pass + except OSError as exc: + raise ConfigError from exc + else: + try: + os.unlink(revision_target) + except FileNotFoundError: + pass + except OSError as exc: + raise ConfigError from exc + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index 7567444db..08861053d 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -75,6 +75,8 @@ def get_config(config=None): default_values = defaults(base + ['name']) if 'port' in default_values: del default_values['port'] + if 'volume' in default_values: + del default_values['volume'] for name in container['name']: container['name'][name] = dict_merge(default_values, container['name'][name]) @@ -85,6 +87,13 @@ def get_config(config=None): default_values = defaults(base + ['name', 'port']) container['name'][name]['port'][port] = dict_merge( default_values, container['name'][name]['port'][port]) + # XXX: T2665: we can not safely rely on the defaults() when there are + # tagNodes in place, it is better to blend in the defaults manually. + if 'volume' in container['name'][name]: + for volume in container['name'][name]['volume']: + default_values = defaults(base + ['name', 'volume']) + container['name'][name]['volume'][volume] = dict_merge( + default_values, container['name'][name]['volume'][volume]) # Delete container network, delete containers tmp = node_changed(conf, base + ['network']) @@ -245,7 +254,7 @@ def generate_run_arguments(name, container_config): env_opt = '' if 'environment' in container_config: for k, v in container_config['environment'].items(): - env_opt += f" -e \"{k}={v['value']}\"" + env_opt += f" --env \"{k}={v['value']}\"" # Publish ports port = '' @@ -255,7 +264,7 @@ def generate_run_arguments(name, container_config): protocol = container_config['port'][portmap]['protocol'] sport = container_config['port'][portmap]['source'] dport = container_config['port'][portmap]['destination'] - port += f' -p {sport}:{dport}/{protocol}' + port += f' --publish {sport}:{dport}/{protocol}' # Bind volume volume = '' @@ -263,7 +272,8 @@ def generate_run_arguments(name, container_config): for vol, vol_config in container_config['volume'].items(): svol = vol_config['source'] dvol = vol_config['destination'] - volume += f' -v {svol}:{dvol}' + mode = vol_config['mode'] + volume += f' --volume {svol}:{dvol}:{mode}' container_base_cmd = f'--detach --interactive --tty --replace {cap_add} ' \ f'--memory {memory}m --shm-size {shared_memory}m --memory-swap 0 --restart {restart} ' \ diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py index ee4defa0d..5f0b76f90 100755 --- a/src/conf_mode/interfaces-pppoe.py +++ b/src/conf_mode/interfaces-pppoe.py @@ -54,7 +54,8 @@ def get_config(config=None): # All parameters that can be changed on-the-fly (like interface description) # should not lead to a reconnect! for options in ['access-concentrator', 'connect-on-demand', 'service-name', - 'source-interface', 'vrf', 'no-default-route', 'authentication']: + 'source-interface', 'vrf', 'no-default-route', + 'authentication', 'host_uniq']: if is_node_changed(conf, base + [ifname, options]): pppoe.update({'shutdown_required': {}}) # bail out early - no need to further process other nodes diff --git a/src/op_mode/config_mgmt.py b/src/op_mode/config_mgmt.py new file mode 100755 index 000000000..0ffb1468b --- /dev/null +++ b/src/op_mode/config_mgmt.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 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 +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import typing + +import vyos.opmode +from vyos.config_mgmt import ConfigMgmt + +def show_commit_diff(raw: bool, rev: int, revb: typing.Optional[int], + commands: bool): + config_mgmt = ConfigMgmt() + config_diff = config_mgmt.show_commit_diff(rev, revb, commands) + + if raw: + revb = (rev+1) if revb is None else revb + if commands: + d = {f'config_command_diff_{revb}_{rev}': config_diff} + else: + d = {f'config_file_diff_{revb}_{rev}': config_diff} + return d + + return config_diff + +def show_commit_file(raw: bool, rev: int): + config_mgmt = ConfigMgmt() + config_file = config_mgmt.show_commit_file(rev) + + if raw: + d = {f'config_revision_{rev}': config_file} + return d + + return config_file + +def show_commit_log(raw: bool): + config_mgmt = ConfigMgmt() + + msg = '' + if config_mgmt.max_revisions == 0: + msg = ('commit-revisions is not configured;\n' + 'commit log is empty or stale:\n\n') + + data = config_mgmt.get_raw_log_data() + if raw: + return data + + out = config_mgmt.format_log_data(data) + out = msg + out + + return out + +def show_commit_log_brief(raw: bool): + # used internally for completion help for 'rollback' + # option 'raw' will return same as 'show_commit_log' + config_mgmt = ConfigMgmt() + + data = config_mgmt.get_raw_log_data() + if raw: + return data + + out = config_mgmt.format_log_data_brief(data) + + return out + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/lldp.py b/src/op_mode/lldp.py index dc2b1e0b5..1a1b94783 100755 --- a/src/op_mode/lldp.py +++ b/src/op_mode/lldp.py @@ -61,7 +61,14 @@ def _get_raw_data(interface=None, detail=False): def _get_formatted_output(raw_data): data_entries = [] - for neighbor in dict_search('lldp.interface', raw_data): + tmp = dict_search('lldp.interface', raw_data) + if not tmp: + return None + # One can not always ensure that "interface" is of type list, add safeguard. + # E.G. Juniper Networks, Inc. ex2300-c-12t only has a dict, not a list of dicts + if isinstance(tmp, dict): + tmp = [tmp] + for neighbor in tmp: for local_if, values in neighbor.items(): tmp = [] @@ -80,6 +87,10 @@ def _get_formatted_output(raw_data): # Capabilities cap = '' capabilities = jmespath.search('chassis.[*][0][0].capability', values) + # One can not always ensure that "capability" is of type list, add + # safeguard. E.G. Unify US-24-250W only has a dict, not a list of dicts + if isinstance(capabilities, dict): + capabilities = [capabilities] if capabilities: for capability in capabilities: if capability['enabled']: |