3434 MergeConfigException ,
3535 ConnectionClosedException ,
3636 CommandErrorException ,
37+ CommitConfirmException ,
3738)
3839from napalm .base .helpers import (
3940 canonical_interface_name ,
@@ -502,15 +503,38 @@ def commit_config(self, message="", revert_in=None):
502503
503504 If merge operation, perform copy <file> running-config.
504505 """
505- if revert_in is not None :
506- raise NotImplementedError (
507- "Commit confirm has not been implemented on this platform."
508- )
506+ CISCO_TIMER_MIN = 1
507+ CISCO_TIMER_MAX = 120
508+ ARCHIVE_DISABLED_MESSAGE = (
509+ "For Cisco devices, revert_in requires 'archive' feature to be enabled."
510+ )
511+ revert_in_min = None
512+
509513 if message :
510514 raise NotImplementedError (
511515 "Commit message not implemented for this platform"
512516 )
513517
518+ if revert_in is not None :
519+ if not self ._check_archive_feature ():
520+ raise CommitConfirmException (ARCHIVE_DISABLED_MESSAGE )
521+ elif not CISCO_TIMER_MIN * 60 <= revert_in <= CISCO_TIMER_MAX * 60 :
522+ msg = (
523+ "For Cisco IOS devices revert_in is rounded down to the nearest minute,"
524+ "pass revert_in as a multiple of 60 between {} and {}" .format (
525+ CISCO_TIMER_MIN * 60 , CISCO_TIMER_MAX * 60
526+ )
527+ )
528+ raise CommitConfirmException (msg )
529+ else :
530+ revert_in_min = int (revert_in / 60 )
531+
532+ if self .has_pending_commit ():
533+ raise CommandErrorException (
534+ "Configuration session already in progress, cannot "
535+ "perform configuration actions"
536+ )
537+
514538 # Always generate a rollback config on commit
515539 self ._gen_rollback_cfg ()
516540
@@ -520,8 +544,16 @@ def commit_config(self, message="", revert_in=None):
520544 cfg_file = self ._gen_full_path (filename )
521545 if not self ._check_file_exists (cfg_file ):
522546 raise ReplaceConfigException ("Candidate config file does not exist" )
523- if self .auto_rollback_on_error :
547+ if revert_in_min and self .auto_rollback_on_error :
548+ cmd = "configure replace {} force revert trigger error timer {}" .format (
549+ cfg_file , revert_in_min
550+ )
551+ elif self .auto_rollback_on_error :
524552 cmd = "configure replace {} force revert trigger error" .format (cfg_file )
553+ elif revert_in_min :
554+ cmd = "configure replace {} force revert timer {}" .format (
555+ cfg_file , revert_in_min
556+ )
525557 else :
526558 cmd = "configure replace {} force" .format (cfg_file )
527559 output = self ._commit_handler (cmd )
@@ -534,14 +566,29 @@ def commit_config(self, message="", revert_in=None):
534566 msg = "Candidate config could not be applied\n {}" .format (output )
535567 raise ReplaceConfigException (msg )
536568 elif "%Please turn config archive on" in output :
537- msg = "napalm-ios replace() requires Cisco 'archive' feature to be enabled."
538- raise ReplaceConfigException (msg )
569+ if revert_in_min :
570+ raise CommitConfirmException (ARCHIVE_DISABLED_MESSAGE )
571+ else :
572+ msg = "napalm-ios replace() requires Cisco 'archive' feature to be enabled"
573+ raise ReplaceConfigException (msg )
539574 else :
540575 # Merge operation
541576 filename = self .merge_cfg
542577 cfg_file = self ._gen_full_path (filename )
543578 if not self ._check_file_exists (cfg_file ):
544579 raise MergeConfigException ("Merge source config file does not exist" )
580+ if revert_in_min is not None :
581+ # Enter config mode with a revert timer and exit config mode
582+ try :
583+ self .device .config_mode (
584+ config_command = "configure terminal revert timer {}" .format (
585+ revert_in_min
586+ )
587+ )
588+ self .device .exit_config_mode ()
589+ except ValueError :
590+ raise MergeConfigException (ARCHIVE_DISABLED_MESSAGE )
591+
545592 cmd = "copy {} running-config" .format (cfg_file )
546593 output = self ._commit_handler (cmd )
547594 if "Invalid input detected" in output :
@@ -553,8 +600,61 @@ def commit_config(self, message="", revert_in=None):
553600 # After a commit - we no longer know whether this is configured or not.
554601 self .prompt_quiet_configured = None
555602
556- # Save config to startup (both replace and merge)
557- output += self .device .save_config ()
603+ if revert_in_min is None :
604+ # Save config to startup (both replace and merge)
605+ output += self .device .save_config ()
606+
607+ def _check_archive_feature (self ):
608+ cmd = "show archive"
609+ output = self .device .send_command (cmd )
610+ if "Archive feature not enabled" in output :
611+ return False
612+ return True
613+
614+ def has_pending_commit (self ):
615+ pending_commits = self ._get_pending_commits ()
616+ return bool (pending_commits )
617+
618+ def _get_pending_commits (self ):
619+ if self ._check_archive_feature ():
620+ cmd = "show archive config rollback timer"
621+ output = self .device .send_command (cmd )
622+ else :
623+ return {}
624+ if "No Rollback Confirmed Change pending" in output :
625+ return {}
626+ match_strings = r"|" .join (
627+ [
628+ r"Time configured.*?: (?P<configured>.*)" ,
629+ r"Timer type: (?P<type>.*)" ,
630+ r"Timer value: (?P<timer>.*)" ,
631+ r"User: (?P<user>.*)" ,
632+ ]
633+ )
634+ keys = ["configured" , "type" , "timer" , "user" ]
635+ matches = re .finditer (match_strings , output )
636+ pending_commits = {}
637+ for match in matches :
638+ for key in keys :
639+ if match .groupdict ().get (key ):
640+ pending_commits .update ({key : match .groupdict ().get (key )})
641+
642+ return pending_commits
643+
644+ def confirm_commit (self ):
645+ """Send final commit to confirm an in-proces commit that requires confirmation."""
646+ if self .has_pending_commit ():
647+ pending = self ._get_pending_commits ()
648+ if pending .get ("user" ) == self .username :
649+ self .device .send_command ("configure confirm" )
650+ self .device .save_config ()
651+ else :
652+ raise CommitConfirmException (
653+ "Configuration session active but not owned by"
654+ " {} cannot confirm commit" .format (self .username )
655+ )
656+ else :
657+ raise CommitConfirmException ("No pending configuration" )
558658
559659 def discard_config (self ):
560660 """Discard loaded candidate configurations."""
@@ -572,18 +672,29 @@ def _discard_config(self):
572672
573673 def rollback (self ):
574674 """Rollback configuration to filename or to self.rollback_cfg file."""
575- filename = self .rollback_cfg
576- cfg_file = self ._gen_full_path (filename )
577- if not self ._check_file_exists (cfg_file ):
578- raise ReplaceConfigException ("Rollback config file does not exist" )
579- cmd = "configure replace {} force" .format (cfg_file )
580- self ._commit_handler (cmd )
581-
582- # After a rollback - we no longer know whether this is configured or not.
583- self .prompt_quiet_configured = None
675+ if self .has_pending_commit ():
676+ if self ._get_pending_commits ().get ("user" ) == self .username :
677+ cmd = "configure revert now"
678+ self ._commit_handler (cmd )
679+ self .device .save_config ()
680+ else :
681+ raise CommitConfirmException (
682+ "Configuration session active but not owned by {} "
683+ "cannot rollback" .format (self .username )
684+ )
685+ else :
686+ filename = self .rollback_cfg
687+ cfg_file = self ._gen_full_path (filename )
688+ if not self ._check_file_exists (cfg_file ):
689+ raise ReplaceConfigException ("Rollback config file does not exist" )
690+ cmd = "configure replace {} force" .format (cfg_file )
691+ self ._commit_handler (cmd )
692+
693+ # After a rollback - we no longer know whether this is configured or not.
694+ self .prompt_quiet_configured = None
584695
585- # Save config to startup
586- self .device .save_config ()
696+ # Save config to startup
697+ self .device .save_config ()
587698
588699 def _inline_tcl_xfer (
589700 self , source_file = None , source_config = None , dest_file = None , file_system = None
0 commit comments