Skip to content

Commit ec15b3d

Browse files
authored
Merge pull request #1688 from napalm-automation/develop
Merge forward from develop to master
2 parents bfe9778 + ef8326c commit ec15b3d

22 files changed

+2460
-51
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/base/constants.py

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
}
7070
LLDP_CAPAB_TRANFORM_TABLE = {
7171
"o": "other",
72+
"n/a": "other",
7273
"p": "repeater",
7374
"b": "bridge",
7475
"w": "wlan-access-point",

napalm/eos/eos.py

+23-7
Original file line numberDiff line numberDiff line change
@@ -846,16 +846,32 @@ def extract_temperature_data(data):
846846
# Matches either of
847847
# Mem: 3844356k total, 3763184k used, 81172k free, 16732k buffers ( 4.16 > )
848848
# KiB Mem: 32472080 total, 5697604 used, 26774476 free, 372052 buffers ( 4.16 < )
849+
# MiB Mem : 3889.2 total, 150.3 free, 1104.5 used, 2634.4 buff/cache (4.27 < )
849850
mem_regex = (
850-
r"[^\d]*(?P<total>\d+)[k\s]+total,"
851-
r"\s+(?P<used>\d+)[k\s]+used,"
852-
r"\s+(?P<free>\d+)[k\s]+free,.*"
851+
r"^(?:(?P<unit>\S+)\s+)?Mem\s*:"
852+
r"\s+(?P<total>[0-9.]+)[k\s]+total,"
853+
r"(?:\s+(?P<used1>[0-9.]+)[k\s]+used,)?"
854+
r"\s+(?P<free>[0-9.]+)[k\s]+free,"
855+
r"(?:\s+(?P<used2>[0-9.]+)[k\s]+used,)?"
856+
r".*"
853857
)
854858
m = re.match(mem_regex, cpu_lines[3])
855-
environment_counters["memory"] = {
856-
"available_ram": int(m.group("total")),
857-
"used_ram": int(m.group("used")),
858-
}
859+
860+
def _parse_memory(unit, total, used):
861+
if unit == "MiB":
862+
return {
863+
"available_ram": int(float(total) * 1024),
864+
"used_ram": int(float(used) * 1024),
865+
}
866+
return {
867+
"available_ram": int(total),
868+
"used_ram": int(used),
869+
}
870+
871+
environment_counters["memory"] = _parse_memory(
872+
m.group("unit"), m.group("total"), m.group("used1") or m.group("used2")
873+
)
874+
859875
return environment_counters
860876

861877
def _transform_lldp_capab(self, capabilities):

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

napalm/ios/utils/textfsm_templates/show_lldp_neighbors_detail.tpl

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Value REMOTE_SYSTEM_CAPAB (.*)
88
Value REMOTE_SYSTEM_ENABLE_CAPAB (.*)
99

1010
Start
11-
^Local Intf\s*?[:-]\s+${LOCAL_INTERFACE}
11+
^Local Int(?:er)?f(?:ace)?\s*?[:-]\s+${LOCAL_INTERFACE}
1212
^Chassis id\s*?[:-]\s+${REMOTE_CHASSIS_ID}
1313
^Port id\s*?[:-]\s+${REMOTE_PORT}
1414
^Port Description\s*?[:-]\s+${REMOTE_PORT_DESCRIPTION}

napalm/iosxr/iosxr.py

+53-8
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from napalm.pyIOSXR.exceptions import ConnectError
3030
from napalm.pyIOSXR.exceptions import TimeoutError
3131
from napalm.pyIOSXR.exceptions import InvalidInputError
32+
from napalm.pyIOSXR.exceptions import XMLCLIError
3233

3334
# import NAPALM base
3435
import napalm.base.helpers
@@ -165,16 +166,60 @@ def get_facts(self):
165166
"interface_list": [],
166167
}
167168

168-
facts_rpc_request = (
169-
"<Get><Operational><SystemTime/><PlatformInventory><RackTable>"
170-
"<Rack><Naming><Name>0</Name></Naming>"
171-
"<Attributes><BasicInfo/></Attributes>"
172-
"</Rack></RackTable></PlatformInventory></Operational></Get>"
173-
)
169+
facts_rpc_request = """
170+
<Get>
171+
<Operational>
172+
<SystemTime/>
173+
<PlatformInventory>
174+
<RackTable>
175+
<Rack>
176+
<Naming>
177+
<Name>0</Name>
178+
</Naming>
179+
<Attributes>
180+
<BasicInfo/>
181+
</Attributes>
182+
</Rack>
183+
</RackTable>
184+
</PlatformInventory>
185+
</Operational>
186+
</Get>
187+
"""
188+
189+
# IOS-XR 7.3.3 and possibly other 7.X versions have this located in
190+
# different location in the XML tree
191+
facts_rpc_request_alt = """
192+
<Get>
193+
<Operational>
194+
<SystemTime/>
195+
<Inventory>
196+
<Entities>
197+
<Entity>
198+
<Naming>
199+
<Name>Rack 0</Name>
200+
</Naming>
201+
<Attributes>
202+
<InvBasicBag></InvBasicBag>
203+
</Attributes>
204+
</Entity>
205+
</Entities>
206+
</Inventory>
207+
</Operational>
208+
</Get>
209+
"""
174210

175-
facts_rpc_reply = ETREE.fromstring(self.device.make_rpc_call(facts_rpc_request))
176211
system_time_xpath = ".//SystemTime/Uptime"
177-
platform_attr_xpath = ".//RackTable/Rack/Attributes/BasicInfo"
212+
try:
213+
facts_rpc_reply = ETREE.fromstring(
214+
self.device.make_rpc_call(facts_rpc_request)
215+
)
216+
platform_attr_xpath = ".//RackTable/Rack/Attributes/BasicInfo"
217+
except XMLCLIError:
218+
facts_rpc_reply = ETREE.fromstring(
219+
self.device.make_rpc_call(facts_rpc_request_alt)
220+
)
221+
platform_attr_xpath = ".//Entities/Entity/Attributes/InvBasicBag"
222+
178223
system_time_tree = facts_rpc_reply.xpath(system_time_xpath)[0]
179224
try:
180225
platform_attr_tree = facts_rpc_reply.xpath(platform_attr_xpath)[0]

napalm/junos/junos.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ def _get_pending_commits(self):
360360
commit_comment = commit_comment_element.text
361361

362362
sys_uptime_info = self.device.rpc.get_system_uptime_information()
363-
current_time_element = sys_uptime_info.find("./current-time/date-time")
363+
current_time_element = sys_uptime_info.find(".//current-time/date-time")
364364
current_time = int(current_time_element.attrib["seconds"])
365365

366366
# Msg from Jnpr: 'commit confirmed, rollback in 5mins'

requirements-dev.txt

+9-9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
black==22.3.0
1+
black==22.6.0
22
coveralls==3.3.1
33
ddt==1.5.0
44
flake8-import-order==0.18.1
@@ -7,11 +7,11 @@ pytest-cov==3.0.0
77
pytest-json==0.4.0
88
pylama==8.2.1
99
mock==4.0.3
10-
tox==3.25.0
11-
mypy==0.960
12-
types-requests==2.27.30
13-
types-six==1.16.15
14-
types-setuptools==57.4.17
15-
types-PyYAML==6.0.8
16-
ttp==0.8.4
17-
ttp_templates==0.1.4
10+
tox==3.25.1
11+
mypy==0.961
12+
types-requests==2.28.0
13+
types-six==1.16.17
14+
types-setuptools==57.4.18
15+
types-PyYAML==6.0.9
16+
ttp==0.9.0
17+
ttp_templates==0.3.0

requirements.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ cffi>=1.11.3
33
paramiko>=2.6.0
44
requests>=2.7.0
55
future
6-
textfsm
6+
textfsm<=1.1.2
77
jinja2
88
netaddr
99
pyYAML
@@ -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)