summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Estabrook <jestabro@vyos.io>2024-10-08 16:57:25 -0500
committerGitHub <noreply@github.com>2024-10-08 16:57:25 -0500
commit3bc900722892dba525b7cd6bada4e6327b01fffe (patch)
tree2dfb03e3f60c7d18bfda90380df80c4b4d298bf3
parent7d4264365e487d37115cff0633b25e4b2012a126 (diff)
parentdfba19b3e89f47690c0abdee5f6410278237122e (diff)
downloadvyos-1x-3bc900722892dba525b7cd6bada4e6327b01fffe.tar.gz
vyos-1x-3bc900722892dba525b7cd6bada4e6327b01fffe.zip
Merge pull request #4128 from jestabro/commit-confirm-soft-rollback
config-mgmt: T5976: add option for commit-confirm to use 'soft' rollback
-rw-r--r--interface-definitions/system_config-management.xml.in27
-rw-r--r--python/vyos/config_mgmt.py351
-rw-r--r--python/vyos/configsession.py87
-rwxr-xr-xsrc/conf_mode/system_config-management.py20
-rwxr-xr-xsrc/helpers/commit-confirm-notify.py48
5 files changed, 375 insertions, 158 deletions
diff --git a/interface-definitions/system_config-management.xml.in b/interface-definitions/system_config-management.xml.in
index e666633b7..a23d44aea 100644
--- a/interface-definitions/system_config-management.xml.in
+++ b/interface-definitions/system_config-management.xml.in
@@ -67,6 +67,33 @@
<constraintErrorMessage>Number of revisions must be between 0 and 65535</constraintErrorMessage>
</properties>
</leafNode>
+ <node name="commit-confirm">
+ <properties>
+ <help>Commit confirm options</help>
+ </properties>
+ <children>
+ <leafNode name="action">
+ <properties>
+ <help>Commit confirm revert action</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>
</node>
</children>
diff --git a/python/vyos/config_mgmt.py b/python/vyos/config_mgmt.py
index d518737ca..1c2b70fdf 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
@@ -49,8 +51,10 @@ config_json = '/run/vyatta/config/config.json'
# 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'}
+commit_hooks = {
+ 'commit_revision': '01vyos-commit-revision',
+ 'commit_archive': '02vyos-commit-archive',
+}
DEFAULT_TIME_MINUTES = 10
timer_name = 'commit-confirm'
@@ -72,6 +76,7 @@ formatter = logging.Formatter('%(funcName)s: %(levelname)s:%(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
+
def save_config(target, json_out=None):
if json_out is None:
cmd = f'{SAVE_CONFIG} {target}'
@@ -81,6 +86,7 @@ def save_config(target, json_out=None):
if rc != 0:
logger.critical(f'save config failed: {out}')
+
def unsaved_commits(allow_missing_config=False) -> bool:
if get_full_version_data()['boot_via'] == 'livecd':
return False
@@ -92,6 +98,7 @@ def unsaved_commits(allow_missing_config=False) -> bool:
os.unlink(tmp_save)
return ret
+
def get_file_revision(rev: int):
revision = os.path.join(archive_dir, f'config.boot.{rev}.gz')
try:
@@ -102,12 +109,15 @@ def get_file_revision(rev: int):
return ''
return r
+
def get_config_tree_revision(rev: int):
c = get_file_revision(rev)
return ConfigTree(c)
+
def is_node_revised(path: list = [], rev1: int = 1, rev2: int = 0) -> bool:
from vyos.configtree import DiffTree
+
left = get_config_tree_revision(rev1)
right = get_config_tree_revision(rev2)
diff_tree = DiffTree(left, right)
@@ -115,9 +125,11 @@ def is_node_revised(path: list = [], rev1: int = 1, rev2: int = 0) -> bool:
return True
return False
+
class ConfigMgmtError(Exception):
pass
+
class ConfigMgmt:
def __init__(self, session_env=None, config=None):
if session_env:
@@ -128,15 +140,20 @@ class ConfigMgmt:
if config is None:
config = Config()
- d = config.get_config_dict(['system', 'config-management'],
- key_mangling=('-', '_'),
- get_first_key=True)
+ d = config.get_config_dict(
+ ['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.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']):
@@ -156,51 +173,73 @@ class ConfigMgmt:
# a call to compare without args is edit_level aware
edit_level = os.getenv('VYATTA_EDIT_LEVEL', '')
- self.edit_path = [l for l in edit_level.split('/') if l]
+ self.edit_path = [l for l in edit_level.split('/') if l] # noqa: E741
self.active_config = config._running_config
self.working_config = config._session_config
# 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
+ def commit_confirm(
+ self, minutes: int = DEFAULT_TIME_MINUTES, no_prompt: bool = False
+ ) -> Tuple[str, int]:
+ """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
-Proceed ?'''
+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'.
+ def confirm(self) -> Tuple[str, int]:
+ """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'):
@@ -224,12 +263,15 @@ 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]:
- """Reboot to saved config, dropping commits from 'commit-confirm'.
- """
+ 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
@@ -239,13 +281,39 @@ Proceed ?'''
return '', 0
- def rollback(self, rev: int, no_prompt: bool=False) -> Tuple[str,int]:
- """Reboot to config revision 'rev'.
- """
+ 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)
+
+ message = '[commit-confirm] Reverting to previous config now'
+ os.system('wall -n ' + message)
+
+ 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 = ''
if not self._check_revision_number(rev):
- msg = f'Invalid revision number {rev}: must be 0 < rev < {self.num_revisions}'
+ msg = (
+ f'Invalid revision number {rev}: must be 0 < rev < {self.num_revisions}'
+ )
return msg, 1
prompt_str = 'Proceed with reboot ?'
@@ -274,12 +342,13 @@ Proceed ?'''
return msg, 0
def rollback_soft(self, rev: int):
- """Rollback without reboot (rollback-soft)
- """
+ """Rollback without reboot (rollback-soft)"""
msg = ''
if not self._check_revision_number(rev):
- msg = f'Invalid revision number {rev}: must be 0 < rev < {self.num_revisions}'
+ msg = (
+ f'Invalid revision number {rev}: must be 0 < rev < {self.num_revisions}'
+ )
return msg, 1
rollback_ct = self._get_config_tree_revision(rev)
@@ -292,9 +361,13 @@ Proceed ?'''
return msg, 0
- def compare(self, saved: bool=False, commands: bool=False,
- rev1: Optional[int]=None,
- rev2: Optional[int]=None) -> Tuple[str,int]:
+ 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.
@@ -335,7 +408,7 @@ Proceed ?'''
return msg, 0
- def wrap_compare(self, options) -> Tuple[str,int]:
+ def wrap_compare(self, options) -> Tuple[str, int]:
"""Interface to vyatta-cfg-run: args collected as 'options' to parse
for compare.
"""
@@ -343,7 +416,7 @@ Proceed ?'''
r1 = None
r2 = None
if 'commands' in options:
- cmnds=True
+ cmnds = True
options.remove('commands')
for i in options:
if not i.isnumeric():
@@ -358,8 +431,7 @@ Proceed ?'''
# Initialization and post-commit hooks for conf-mode
#
def initialize_revision(self):
- """Initialize config archive, logrotate conf, and commit log.
- """
+ """Initialize config archive, logrotate conf, and commit log."""
mask = os.umask(0o002)
os.makedirs(archive_dir, exist_ok=True)
json_dir = os.path.dirname(config_json)
@@ -371,8 +443,7 @@ Proceed ?'''
self._add_logrotate_conf()
- if (not os.path.exists(commit_log_file) or
- self._get_number_of_revisions() == 0):
+ if not os.path.exists(commit_log_file) or self._get_number_of_revisions() == 0:
user = self._get_user()
via = 'init'
comment = ''
@@ -399,8 +470,7 @@ Proceed ?'''
self._update_archive()
def commit_archive(self):
- """Upload config to remote archive.
- """
+ """Upload config to remote archive."""
from vyos.remote import upload
hostname = self.hostname
@@ -410,20 +480,23 @@ Proceed ?'''
source_address = self.source_address
if self.effective_locations:
- print("Archiving config...")
+ print('Archiving config...')
for location in self.effective_locations:
url = urlsplit(location)
- _, _, netloc = url.netloc.rpartition("@")
+ _, _, netloc = url.netloc.rpartition('@')
redacted_location = urlunsplit(url._replace(netloc=netloc))
- print(f" {redacted_location}", end=" ", flush=True)
- upload(archive_config_file, f'{location}/{remote_file}',
- source_host=source_address)
+ print(f' {redacted_location}', end=' ', flush=True)
+ 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]
+ keys: [timestamp, user, commit_via, commit_comment]
"""
log = self._get_log_entries()
res_l = []
@@ -435,20 +508,20 @@ Proceed ?'''
@staticmethod
def format_log_data(data: list) -> str:
- """Return formatted log data as 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")
+ for l_no, l_val in enumerate(data):
+ time_d = datetime.fromtimestamp(int(l_val['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']}"])
+ res_l.append(
+ [l_no, time_str, f"by {l_val['user']}", f"via {l_val['commit_via']}"]
+ )
- if l['commit_comment'] != 'commit': # default comment
- res_l.append([None, l['commit_comment']])
+ if l_val['commit_comment'] != 'commit': # default comment
+ res_l.append([None, l_val['commit_comment']])
- ret = tabulate(res_l, tablefmt="plain")
+ ret = tabulate(res_l, tablefmt='plain')
return ret
@staticmethod
@@ -459,23 +532,25 @@ Proceed ?'''
'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")
+ for l_no, l_val in enumerate(data):
+ time_d = datetime.fromtimestamp(int(l_val['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']}"])
+ res_l.append(
+ ['\t', l_no, time_str, f"{l_val['user']}", f"by {l_val['commit_via']}"]
+ )
- ret = tabulate(res_l, tablefmt="plain")
+ ret = tabulate(res_l, tablefmt='plain')
return ret
- def show_commit_diff(self, rev: int, rev2: Optional[int]=None,
- commands: bool=False) -> str:
+ 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))
+ out, _ = self.compare(commands=commands, rev1=rev, rev2=(rev + 1))
return out
out, _ = self.compare(commands=commands, rev1=rev, rev2=rev2)
@@ -519,8 +594,9 @@ Proceed ?'''
conf_file.chmod(0o644)
def _archive_active_config(self) -> bool:
- save_to_tmp = (boot_configuration_complete() or not
- os.path.isfile(archive_config_file))
+ save_to_tmp = boot_configuration_complete() or not os.path.isfile(
+ archive_config_file
+ )
mask = os.umask(0o113)
ext = os.getpid()
@@ -560,15 +636,14 @@ Proceed ?'''
@staticmethod
def _update_archive():
- cmd = f"sudo logrotate -f -s {logrotate_state} {logrotate_conf}"
+ 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
- """
+ """Return lines of commit log as list of strings"""
entries = []
if os.path.exists(commit_log_file):
with open(commit_log_file) as f:
@@ -577,8 +652,8 @@ Proceed ?'''
return entries
def _get_number_of_revisions(self) -> int:
- l = self._get_log_entries()
- return len(l)
+ log_entries = self._get_log_entries()
+ return len(log_entries)
def _check_revision_number(self, rev: int) -> bool:
self.num_revisions = self._get_number_of_revisions()
@@ -599,9 +674,14 @@ Proceed ?'''
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]:
+ 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
@@ -647,12 +727,12 @@ Proceed ?'''
logger.critical(f'Invalid log format {line}')
return {}
- timestamp, user, commit_via, commit_comment = (
- tuple(line.strip().strip('|').split('|')))
+ 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]))
+ d = dict(zip(keys, [user, commit_via, commit_comment, timestamp]))
return d
@@ -662,17 +742,28 @@ 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)
- def _add_log_entry(self, user: str='', commit_via: str='',
- commit_comment: str='', timestamp: Optional[int]=None):
+ 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)
+ 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)
@@ -687,6 +778,7 @@ Proceed ?'''
os.umask(mask)
+
# entry_point for console script
#
def run():
@@ -706,43 +798,54 @@ def run():
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")
-
- rollback_soft = subparsers.add_parser('rollback_soft',
- help="Rollback to earlier config")
- rollback_soft.add_argument('--rev', type=int,
- help="Revision number for rollback")
-
- 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")
+ 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 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')
+ rollback.add_argument(
+ '-y', dest='no_prompt', action='store_true', help='Excute without prompt'
+ )
+
+ rollback_soft = subparsers.add_parser(
+ 'rollback_soft', help='Rollback to earlier config'
+ )
+ rollback_soft.add_argument('--rev', type=int, help='Revision number for rollback')
+
+ 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())
diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py
index 7d51b94e4..9c56d246a 100644
--- a/python/vyos/configsession.py
+++ b/python/vyos/configsession.py
@@ -32,15 +32,34 @@ SHOW_CONFIG = ['/bin/cli-shell-api', 'showConfig']
LOAD_CONFIG = ['/bin/cli-shell-api', 'loadFile']
MIGRATE_LOAD_CONFIG = ['/usr/libexec/vyos/vyos-load-config.py']
SAVE_CONFIG = ['/usr/libexec/vyos/vyos-save-config.py']
-INSTALL_IMAGE = ['/usr/libexec/vyos/op_mode/image_installer.py',
- '--action', 'add', '--no-prompt', '--image-path']
+INSTALL_IMAGE = [
+ '/usr/libexec/vyos/op_mode/image_installer.py',
+ '--action',
+ 'add',
+ '--no-prompt',
+ '--image-path',
+]
IMPORT_PKI = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'import']
-IMPORT_PKI_NO_PROMPT = ['/usr/libexec/vyos/op_mode/pki.py',
- '--action', 'import', '--no-prompt']
-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']
+IMPORT_PKI_NO_PROMPT = [
+ '/usr/libexec/vyos/op_mode/pki.py',
+ '--action',
+ 'import',
+ '--no-prompt',
+]
+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']
@@ -50,7 +69,8 @@ OP_CMD_ADD = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'add']
OP_CMD_DELETE = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'delete']
# Default "commit via" string
-APP = "vyos-http-api"
+APP = 'vyos-http-api'
+
# When started as a service rather than from a user shell,
# the process lacks the VyOS-specific environment that comes
@@ -61,7 +81,7 @@ def inject_vyos_env(env):
env['VYATTA_USER_LEVEL_DIR'] = '/opt/vyatta/etc/shell/level/admin'
env['VYATTA_PROCESS_CLIENT'] = 'gui2_rest'
env['VYOS_HEADLESS_CLIENT'] = 'vyos_http_api'
- env['vyatta_bindir']= '/opt/vyatta/bin'
+ env['vyatta_bindir'] = '/opt/vyatta/bin'
env['vyatta_cfg_templates'] = '/opt/vyatta/share/vyatta-cfg/templates'
env['vyatta_configdir'] = directories['vyos_configdir']
env['vyatta_datadir'] = '/opt/vyatta/share'
@@ -78,7 +98,7 @@ def inject_vyos_env(env):
env['vyos_configdir'] = directories['vyos_configdir']
env['vyos_conf_scripts_dir'] = '/usr/libexec/vyos/conf_mode'
env['vyos_datadir'] = '/opt/vyatta/share'
- env['vyos_datarootdir']= '/opt/vyatta/share'
+ env['vyos_datarootdir'] = '/opt/vyatta/share'
env['vyos_libdir'] = '/opt/vyatta/lib'
env['vyos_libexec_dir'] = '/usr/libexec/vyos'
env['vyos_op_scripts_dir'] = '/usr/libexec/vyos/op_mode'
@@ -102,6 +122,7 @@ class ConfigSession(object):
"""
The write API of VyOS.
"""
+
def __init__(self, session_id, app=APP):
"""
Creates a new config session.
@@ -116,7 +137,9 @@ class ConfigSession(object):
and used the PID for the session identifier.
"""
- env_str = subprocess.check_output([CLI_SHELL_API, 'getSessionEnv', str(session_id)])
+ env_str = subprocess.check_output(
+ [CLI_SHELL_API, 'getSessionEnv', str(session_id)]
+ )
self.__session_id = session_id
# Extract actual variables from the chunk of shell it outputs
@@ -129,20 +152,39 @@ class ConfigSession(object):
session_env[k] = v
self.__session_env = session_env
- self.__session_env["COMMIT_VIA"] = app
+ self.__session_env['COMMIT_VIA'] = app
self.__run_command([CLI_SHELL_API, 'setupSession'])
def __del__(self):
try:
- output = subprocess.check_output([CLI_SHELL_API, 'teardownSession'], env=self.__session_env).decode().strip()
+ output = (
+ subprocess.check_output(
+ [CLI_SHELL_API, 'teardownSession'], env=self.__session_env
+ )
+ .decode()
+ .strip()
+ )
if output:
- print("cli-shell-api teardownSession output for sesion {0}: {1}".format(self.__session_id, output), file=sys.stderr)
+ print(
+ 'cli-shell-api teardownSession output for sesion {0}: {1}'.format(
+ self.__session_id, output
+ ),
+ file=sys.stderr,
+ )
except Exception as e:
- print("Could not tear down session {0}: {1}".format(self.__session_id, e), file=sys.stderr)
+ print(
+ 'Could not tear down session {0}: {1}'.format(self.__session_id, e),
+ file=sys.stderr,
+ )
def __run_command(self, cmd_list):
- p = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=self.__session_env)
+ p = subprocess.Popen(
+ cmd_list,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ env=self.__session_env,
+ )
(stdout_data, stderr_data) = p.communicate()
output = stdout_data.decode()
result = p.wait()
@@ -204,7 +246,7 @@ class ConfigSession(object):
def comment(self, path, value=None):
if not value:
- value = [""]
+ value = ['']
else:
value = [value]
self.__run_command([COMMENT] + path + value)
@@ -226,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 c681a8405..8de4e5342 100755
--- a/src/conf_mode/system_config-management.py
+++ b/src/conf_mode/system_config-management.py
@@ -22,6 +22,7 @@ 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
@@ -36,22 +37,29 @@ def get_config(config=None):
return mgmt
-def verify(_mgmt):
+
+def verify(mgmt):
+ d = mgmt.config_dict
+ confirm = d.get('commit_confirm', {})
+ if confirm.get('action', '') == 'reload' and 'commit_revisions' not in d:
+ raise ConfigError('commit-confirm reload requires non-zero commit-revisions')
+
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'])
+ archive_target = os.path.join(commit_post_hook_dir, commit_hooks['commit_archive'])
if locations:
try:
os.symlink('/usr/bin/config-mgmt', archive_target)
@@ -68,8 +76,9 @@ def apply(mgmt):
raise ConfigError from exc
revisions = mgmt.max_revisions
- revision_target = os.path.join(commit_post_hook_dir,
- commit_hooks['commit_revision'])
+ 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)
@@ -85,6 +94,7 @@ def apply(mgmt):
except OSError as exc:
raise ConfigError from exc
+
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/helpers/commit-confirm-notify.py b/src/helpers/commit-confirm-notify.py
index 8d7626c78..af6167651 100755
--- a/src/helpers/commit-confirm-notify.py
+++ b/src/helpers/commit-confirm-notify.py
@@ -2,30 +2,56 @@
import os
import sys
import time
+from argparse import ArgumentParser
# Minutes before reboot to trigger notification.
intervals = [1, 5, 15, 60]
-def notify(interval):
- s = "" if interval == 1 else "s"
+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, 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__":
+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)
- minutes -= (minutes - interval)
+ notify(interval, reboot=reboot)
+ minutes -= minutes - interval
exit(0)