Skip to content

Commit 9f41701

Browse files
authored
Merge branch 'develop' into xr_getfacts
2 parents b8acac0 + d2f9fc5 commit 9f41701

File tree

3 files changed

+133
-21
lines changed

3 files changed

+133
-21
lines changed

docs/support/index.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ _ EOS Junos IOS-XR (NETCONF) IOS-XR (XML-Agent)
3838
===================== ========== ===== ================ ================== ============== ==============
3939
**Config. replace** Yes Yes Yes Yes Yes Yes
4040
**Config. merge** Yes Yes Yes Yes Yes Yes
41-
**Commit Confirm** Yes Yes No No No No
41+
**Commit Confirm** Yes Yes No No No Yes
4242
**Compare config** Yes Yes Yes Yes [#c1]_ Yes [#c4]_ Yes
4343
**Atomic Changes** Yes Yes Yes Yes Yes/No [#c5]_ Yes/No [#c5]_
4444
**Rollback** Yes [#c2]_ Yes Yes Yes Yes/No [#c5]_ Yes

napalm/ios/ios.py

+131-20
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
MergeConfigException,
3535
ConnectionClosedException,
3636
CommandErrorException,
37+
CommitConfirmException,
3738
)
3839
from 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

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ ncclient
1616
ttp
1717
ttp_templates
1818
netutils>=1.0.0
19+
typing-extensions>=4.3.0

0 commit comments

Comments
 (0)