summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--interface-definitions/system_config-management.xml.in20
-rw-r--r--python/vyos/config_mgmt.py83
-rw-r--r--python/vyos/configsession.py9
-rwxr-xr-xsrc/conf_mode/system_config-management.py6
-rwxr-xr-xsrc/helpers/commit-confirm-notify.py42
5 files changed, 137 insertions, 23 deletions
diff --git a/interface-definitions/system_config-management.xml.in b/interface-definitions/system_config-management.xml.in
index e666633b7..b8fb6cdb5 100644
--- a/interface-definitions/system_config-management.xml.in
+++ b/interface-definitions/system_config-management.xml.in
@@ -67,6 +67,26 @@
<constraintErrorMessage>Number of revisions must be between 0 and 65535</constraintErrorMessage>
</properties>
</leafNode>
+ <leafNode name="commit-confirm">
+ <properties>
+ <help>Commit confirm rollback type if no confirmation</help>
+ <completionHelp>
+ <list>reload reboot</list>
+ </completionHelp>
+ <valueHelp>
+ <format>reload</format>
+ <description>Reload previous configuration if not confirmed</description>
+ </valueHelp>
+ <valueHelp>
+ <format>reboot</format>
+ <description>Reboot to saved configuration if not confirmed</description>
+ </valueHelp>
+ <constraint>
+ <regex>(reload|reboot)</regex>
+ </constraint>
+ </properties>
+ <defaultValue>reboot</defaultValue>
+ </leafNode>
</children>
</node>
</children>
diff --git a/python/vyos/config_mgmt.py b/python/vyos/config_mgmt.py
index 920a19fec..851ac2134 100644
--- a/python/vyos/config_mgmt.py
+++ b/python/vyos/config_mgmt.py
@@ -33,6 +33,8 @@ from urllib.parse import urlunsplit
from vyos.config import Config
from vyos.configtree import ConfigTree
from vyos.configtree import ConfigTreeError
+from vyos.configsession import ConfigSession
+from vyos.configsession import ConfigSessionError
from vyos.configtree import show_diff
from vyos.load_config import load
from vyos.load_config import LoadConfigError
@@ -139,13 +141,19 @@ class ConfigMgmt:
config = Config()
d = config.get_config_dict(
- ['system', 'config-management'], key_mangling=('-', '_'), get_first_key=True
+ ['system', 'config-management'],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ with_defaults=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', '')
+ self.reboot_unconfirmed = bool(d.get('commit_confirm') == 'reboot')
+ self.config_dict = d
+
if config.exists(['system', 'host-name']):
self.hostname = config.return_value(['system', 'host-name'])
if config.exists(['system', 'domain-name']):
@@ -175,42 +183,63 @@ class ConfigMgmt:
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
+ """Commit with reload/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 unsaved_commits():
+ if self.reboot_unconfirmed and unsaved_commits():
W = '\nYou should save previous commits before commit-confirm !\n'
else:
W = ''
- prompt_str = f"""
+ if self.reboot_unconfirmed:
+ prompt_str = f"""
commit-confirm will automatically reboot in {minutes} minutes unless changes
-are confirmed.\n
+are confirmed.
+Proceed ?"""
+ else:
+ prompt_str = f"""
+commit-confirm will automatically reload previous config in {minutes} minutes
+unless changes are confirmed.
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"'
+ if self.reboot_unconfirmed:
+ action = 'sg vyattacfg "/usr/bin/config-mgmt revert"'
+ else:
+ action = 'sg vyattacfg "/usr/bin/config-mgmt revert_soft"'
+
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}'
+ if self.reboot_unconfirmed:
+ cmd = (
+ f'sudo -b /usr/libexec/vyos/commit-confirm-notify.py --reboot {minutes}'
+ )
+ else:
+ 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'
+ if self.reboot_unconfirmed:
+ msg = f'Initialized commit-confirm; {minutes} minutes to confirm before reboot'
+ else:
+ msg = f'Initialized commit-confirm; {minutes} minutes to confirm before reload'
+
return msg, 0
def confirm(self) -> Tuple[str, int]:
- """Do not reboot to saved config following 'commit-confirm'.
+ """Do not reboot/reload to saved/completed config following 'commit-confirm'.
Update commit log and archive.
"""
if not is_systemd_service_active(f'{timer_name}.timer'):
@@ -234,7 +263,11 @@ Proceed ?"""
self._add_log_entry(**entry)
self._update_archive()
- msg = 'Reboot timer stopped'
+ if self.reboot_unconfirmed:
+ msg = 'Reboot timer stopped'
+ else:
+ msg = 'Reload timer stopped'
+
return msg, 0
def revert(self) -> Tuple[str, int]:
@@ -248,6 +281,28 @@ Proceed ?"""
return '', 0
+ def revert_soft(self) -> Tuple[str, int]:
+ """Reload last revision, dropping commits from 'commit-confirm'."""
+ _ = self._read_tmp_log_entry()
+
+ # commits under commit-confirm are not added to revision list unless
+ # confirmed, hence a soft revert is to revision 0
+ revert_ct = self._get_config_tree_revision(0)
+
+ mask = os.umask(0o002)
+ session = ConfigSession(os.getpid(), app='config-mgmt')
+
+ try:
+ session.load_explicit(revert_ct)
+ session.commit()
+ except ConfigSessionError as e:
+ raise ConfigMgmtError(e) from e
+ finally:
+ os.umask(mask)
+ del session
+
+ return '', 0
+
def rollback(self, rev: int, no_prompt: bool = False) -> Tuple[str, int]:
"""Reboot to config revision 'rev'."""
msg = ''
@@ -684,7 +739,10 @@ Proceed ?"""
entry = f.read()
os.unlink(tmp_log_entry)
except OSError as e:
- logger.critical(f'error on file {tmp_log_entry}: {e}')
+ logger.info(f'error on file {tmp_log_entry}: {e}')
+ # fail gracefully in corner case:
+ # delete commit-revisions; commit-confirm
+ return {}
return self._get_log_entry(entry)
@@ -752,7 +810,8 @@ def run():
)
subparsers.add_parser('confirm', help='Confirm commit')
- subparsers.add_parser('revert', help='Revert commit-confirm')
+ subparsers.add_parser('revert', help='Revert commit-confirm with reboot')
+ subparsers.add_parser('revert_soft', help='Revert commit-confirm with reload')
rollback = subparsers.add_parser('rollback', help='Rollback to earlier config')
rollback.add_argument('--rev', type=int, help='Revision number for rollback')
diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py
index c0d3c7ecb..9c56d246a 100644
--- a/python/vyos/configsession.py
+++ b/python/vyos/configsession.py
@@ -268,6 +268,15 @@ class ConfigSession(object):
out = self.__run_command(LOAD_CONFIG + [file_path])
return out
+ def load_explicit(self, file_path):
+ from vyos.load_config import load
+ from vyos.load_config import LoadConfigError
+
+ try:
+ load(file_path, switch='explicit')
+ except LoadConfigError as e:
+ raise ConfigSessionError(e) from e
+
def migrate_and_load_config(self, file_path):
out = self.__run_command(MIGRATE_LOAD_CONFIG + [file_path])
return out
diff --git a/src/conf_mode/system_config-management.py b/src/conf_mode/system_config-management.py
index 99f25bef6..a1ee136cd 100755
--- a/src/conf_mode/system_config-management.py
+++ b/src/conf_mode/system_config-management.py
@@ -38,7 +38,11 @@ def get_config(config=None):
return mgmt
-def verify(_mgmt):
+def verify(mgmt):
+ d = mgmt.config_dict
+ if d.get('commit_confirm', '') == 'reload' and 'commit_revisions' not in d:
+ raise ConfigError('commit-confirm reload requires non-zero commit-revisions')
+
return
diff --git a/src/helpers/commit-confirm-notify.py b/src/helpers/commit-confirm-notify.py
index 69dda5112..af6167651 100755
--- a/src/helpers/commit-confirm-notify.py
+++ b/src/helpers/commit-confirm-notify.py
@@ -2,34 +2,56 @@
import os
import sys
import time
+from argparse import ArgumentParser
# Minutes before reboot to trigger notification.
intervals = [1, 5, 15, 60]
+parser = ArgumentParser()
+parser.add_argument(
+ 'minutes', type=int, help='minutes before rollback to trigger notification'
+)
+parser.add_argument(
+ '--reboot', action='store_true', help="use 'soft' rollback instead of reboot"
+)
-def notify(interval):
+
+def notify(interval, reboot=False):
s = '' if interval == 1 else 's'
time.sleep((minutes - interval) * 60)
- message = (
- '"[commit-confirm] System is going to reboot in '
- f'{interval} minute{s} to rollback the last commit.\n'
- 'Confirm your changes to cancel the reboot."'
- )
- os.system('wall -n ' + message)
+ if reboot:
+ message = (
+ '"[commit-confirm] System will reboot in '
+ f'{interval} minute{s}\nto rollback the last commit.\n'
+ 'Confirm your changes to cancel the reboot."'
+ )
+ os.system('wall -n ' + message)
+ else:
+ message = (
+ '"[commit-confirm] System will reload previous config in '
+ f'{interval} minute{s}\nto rollback the last commit.\n'
+ 'Confirm your changes to cancel the reload."'
+ )
+ os.system('wall -n ' + message)
if __name__ == '__main__':
# Must be run as root to call wall(1) without a banner.
- if len(sys.argv) != 2 or os.getuid() != 0:
+ if os.getuid() != 0:
print('This script requires superuser privileges.', file=sys.stderr)
exit(1)
- minutes = int(sys.argv[1])
+
+ args = parser.parse_args()
+
+ minutes = args.minutes
+ reboot = args.reboot
+
# Drop the argument from the list so that the notification
# doesn't kick in immediately.
if minutes in intervals:
intervals.remove(minutes)
for interval in sorted(intervals, reverse=True):
if minutes >= interval:
- notify(interval)
+ notify(interval, reboot=reboot)
minutes -= minutes - interval
exit(0)