diff --git a/napalm/base/base.py b/napalm/base/base.py index c7c6ee347..f5e2121b7 100644 --- a/napalm/base/base.py +++ b/napalm/base/base.py @@ -178,22 +178,32 @@ def compare_config(self): """ raise NotImplementedError - def commit_config(self): + def commit_config(self, confirmed=None, message=None): """ Commits the changes requested by the method load_replace_candidate or load_merge_candidate. """ raise NotImplementedError - def discard_config(self): + def commit_confirm(self): + """Confirm pending commit.""" + raise NotImplementedError + + def commit_confirm_revert(self): + """Abort pending commit confirm immediately and rollback to previous state""" + raise NotImplementedError + + def has_pending_commit(self): """ - Discards the configuration loaded into the candidate. + :return A boolean indicating whether any pending commit confirms exist. """ raise NotImplementedError + def discard_config(self): + """Discards the configuration loaded into the candidate.""" + raise NotImplementedError + def rollback(self): - """ - If changes were made, revert changes to the original state. - """ + """If changes were made, revert changes to the original state.""" raise NotImplementedError def get_facts(self): diff --git a/napalm/base/test/base.py b/napalm/base/test/base.py index 1f991412c..0b5fa8c92 100644 --- a/napalm/base/test/base.py +++ b/napalm/base/test/base.py @@ -69,6 +69,23 @@ def test_replacing_and_committing_config(self): self.assertEqual(len(diff), 0) + def test_replacing_and_committing_config_with_confirm(self): + try: + self.device.load_replace_candidate(filename='%s/new_good.conf' % self.vendor) + self.device.commit_config(confirmed=5) + self.device.commit_confirm() + except NotImplementedError: + raise SkipTest() + + # The diff should be empty as the configuration has been committed already + diff = self.device.compare_config() + + # Reverting changes + self.device.load_replace_candidate(filename='%s/initial.conf' % self.vendor) + self.device.commit_config() + + self.assertEqual(len(diff), 0) + def test_replacing_config_with_typo(self): result = False try: diff --git a/napalm/eos/eos.py b/napalm/eos/eos.py index 1fc4dd7c4..a49a89c74 100644 --- a/napalm/eos/eos.py +++ b/napalm/eos/eos.py @@ -256,8 +256,11 @@ def compare_config(self): return result.strip() - def commit_config(self): + def commit_config(self, confirmed=None, message=None): """Implementation of NAPALM method commit_config.""" + if confirmed is not None: + raise NotImplementedError + commands = [] commands.append('copy startup-config flash:rollback-0') commands.append('configure session {}'.format(self.config_session)) diff --git a/napalm/ios/ios.py b/napalm/ios/ios.py index f542b8640..6b4ac6e73 100644 --- a/napalm/ios/ios.py +++ b/napalm/ios/ios.py @@ -420,9 +420,27 @@ def _commit_hostname_handler(self, cmd): self.device.set_base_prompt() return output - def commit_config(self): + def commit_confirm(self): + """Confirm pending commit.""" + cmd = "configure confirm" + self.device.send_command_expect(cmd) + + def commit_confirm_revert(self): + """Immediately revert pending commit confirm.""" + cmd = "configure revert now" + self.device.send_command_expect(cmd) + + def has_pending_commit(self): + """ + :return A boolean indicating whether any pending commit confirms exist. """ - If replacement operation, perform 'configure replace' for the entire config. + cmd = "show archive config rollback timer" + search_pattern = r"No Rollback Confirmed Change pending" + output = self.device.send_command_expect(cmd) + return not bool(re.search(search_pattern, output, flags=re.I)) + + def commit_config(self, confirmed=None, message=None): + """If replacement operation, perform 'configure replace' for the entire config. If merge operation, perform copy running-config. """ @@ -435,10 +453,24 @@ def commit_config(self): cfg_file = self._gen_full_path(filename) if not self._check_file_exists(cfg_file): raise ReplaceConfigException("Candidate config file does not exist") - if self.auto_rollback_on_error: - cmd = 'configure replace {} force revert trigger error'.format(cfg_file) - else: - cmd = 'configure replace {} force'.format(cfg_file) + if not self.auto_rollback_on_error: + raise NotImplementedError("auto_rollback_on_error attribute has been removed") + + cmd = 'configure replace {} force revert trigger error'.format(cfg_file) + if confirmed is not None: + confirmed = int(confirmed) + # Ensure within Cisco IOS thresholds (minutes) + CISCO_LOW_THRESHOLD = 1 + CISCO_HIGH_THRESHOLD = 120 + if CISCO_LOW_THRESHOLD <= confirmed <= CISCO_HIGH_THRESHOLD: + cmd = 'configure replace flash:/candidate_config.txt force ' \ + 'revert trigger error timer {}'.format(confirmed) + else: + msg = "Invalid value for 'confirmed'; Cisco IOS requires a value " \ + "between: {}-{} minutes".format(CISCO_LOW_THRESHOLD, + CISCO_HIGH_THRESHOLD) + raise ValueError(msg) + output = self._commit_hostname_handler(cmd) if ('original configuration has been successfully restored' in output) or \ ('error' in output.lower()) or \ diff --git a/napalm/iosxr/iosxr.py b/napalm/iosxr/iosxr.py index c2643b18d..979783a6f 100644 --- a/napalm/iosxr/iosxr.py +++ b/napalm/iosxr/iosxr.py @@ -145,7 +145,10 @@ def compare_config(self): else: return self.device.compare_config().strip() - def commit_config(self): + def commit_config(self, confirmed=None, message=None): + if confirmed is not None: + raise NotImplementedError + if self.replace: self.device.commit_replace_config() else: diff --git a/napalm/junos/junos.py b/napalm/junos/junos.py index 0c32bb253..15692048e 100644 --- a/napalm/junos/junos.py +++ b/napalm/junos/junos.py @@ -234,8 +234,11 @@ def compare_config(self): else: return diff.strip() - def commit_config(self): + def commit_config(self, confirmed=None, message=None): """Commit configuration.""" + if confirmed is not None: + raise NotImplementedError + self.device.cu.commit(ignore_warning=self.ignore_warning) if not self.config_lock: self._unlock() diff --git a/napalm/nxos/nxos.py b/napalm/nxos/nxos.py index cdffdf74c..516de345f 100644 --- a/napalm/nxos/nxos.py +++ b/napalm/nxos/nxos.py @@ -323,7 +323,10 @@ def _load_config(self): raise ReplaceConfigException(rollback_result['msg']) return True - def commit_config(self): + def commit_config(self, confirmed=None, message=None): + if confirmed is not None: + raise NotImplementedError + if self.loaded: self.backup_file = 'config_' + str(datetime.now()).replace(' ', '_') self._save_config(self.backup_file) diff --git a/napalm/nxos_ssh/nxos_ssh.py b/napalm/nxos_ssh/nxos_ssh.py index d67e08331..3baf14968 100644 --- a/napalm/nxos_ssh/nxos_ssh.py +++ b/napalm/nxos_ssh/nxos_ssh.py @@ -672,7 +672,10 @@ def _load_cfg_from_checkpoint(self): return False return True - def commit_config(self): + def commit_config(self, confirmed=None, message=None): + if confirmed is not None: + raise NotImplementedError + if self.loaded: self.backup_file = 'config_' + str(datetime.now()).replace(' ', '_') # Create Checkpoint from current running-config