34
34
MergeConfigException ,
35
35
ConnectionClosedException ,
36
36
CommandErrorException ,
37
+ CommitConfirmException ,
37
38
)
38
39
from napalm .base .helpers import (
39
40
canonical_interface_name ,
@@ -502,15 +503,38 @@ def commit_config(self, message="", revert_in=None):
502
503
503
504
If merge operation, perform copy <file> running-config.
504
505
"""
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
+
509
513
if message :
510
514
raise NotImplementedError (
511
515
"Commit message not implemented for this platform"
512
516
)
513
517
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
+
514
538
# Always generate a rollback config on commit
515
539
self ._gen_rollback_cfg ()
516
540
@@ -520,8 +544,16 @@ def commit_config(self, message="", revert_in=None):
520
544
cfg_file = self ._gen_full_path (filename )
521
545
if not self ._check_file_exists (cfg_file ):
522
546
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 :
524
552
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
+ )
525
557
else :
526
558
cmd = "configure replace {} force" .format (cfg_file )
527
559
output = self ._commit_handler (cmd )
@@ -534,14 +566,29 @@ def commit_config(self, message="", revert_in=None):
534
566
msg = "Candidate config could not be applied\n {}" .format (output )
535
567
raise ReplaceConfigException (msg )
536
568
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 )
539
574
else :
540
575
# Merge operation
541
576
filename = self .merge_cfg
542
577
cfg_file = self ._gen_full_path (filename )
543
578
if not self ._check_file_exists (cfg_file ):
544
579
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
+
545
592
cmd = "copy {} running-config" .format (cfg_file )
546
593
output = self ._commit_handler (cmd )
547
594
if "Invalid input detected" in output :
@@ -553,8 +600,61 @@ def commit_config(self, message="", revert_in=None):
553
600
# After a commit - we no longer know whether this is configured or not.
554
601
self .prompt_quiet_configured = None
555
602
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" )
558
658
559
659
def discard_config (self ):
560
660
"""Discard loaded candidate configurations."""
@@ -572,18 +672,29 @@ def _discard_config(self):
572
672
573
673
def rollback (self ):
574
674
"""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
584
695
585
- # Save config to startup
586
- self .device .save_config ()
696
+ # Save config to startup
697
+ self .device .save_config ()
587
698
588
699
def _inline_tcl_xfer (
589
700
self , source_file = None , source_config = None , dest_file = None , file_system = None
0 commit comments