diff options
| author | John Estabrook <jestabro@vyos.io> | 2024-10-08 16:57:25 -0500 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-10-08 16:57:25 -0500 | 
| commit | 3bc900722892dba525b7cd6bada4e6327b01fffe (patch) | |
| tree | 2dfb03e3f60c7d18bfda90380df80c4b4d298bf3 | |
| parent | 7d4264365e487d37115cff0633b25e4b2012a126 (diff) | |
| parent | dfba19b3e89f47690c0abdee5f6410278237122e (diff) | |
| download | vyos-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.in | 27 | ||||
| -rw-r--r-- | python/vyos/config_mgmt.py | 351 | ||||
| -rw-r--r-- | python/vyos/configsession.py | 87 | ||||
| -rwxr-xr-x | src/conf_mode/system_config-management.py | 20 | ||||
| -rwxr-xr-x | src/helpers/commit-confirm-notify.py | 48 | 
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) | 
