Skip to content
This repository has been archived by the owner on Jan 16, 2019. It is now read-only.

Commit

Permalink
Merge pull request #214 from napalm-automation/develop
Browse files Browse the repository at this point in the history
napalm-ios release 0.8.1
  • Loading branch information
ktbyers committed Oct 9, 2017
2 parents 13f83f9 + 8cf5d60 commit bd7b9b0
Show file tree
Hide file tree
Showing 25 changed files with 678 additions and 160 deletions.
1 change: 0 additions & 1 deletion README.md
@@ -1,5 +1,4 @@
[![PyPI](https://img.shields.io/pypi/v/napalm-ios.svg)](https://pypi.python.org/pypi/napalm-ios)
[![PyPI](https://img.shields.io/pypi/dm/napalm-ios.svg)](https://pypi.python.org/pypi/napalm-ios)
[![Build Status](https://travis-ci.org/napalm-automation/napalm-ios.svg?branch=master)](https://travis-ci.org/napalm-automation/napalm-ios)
[![Coverage Status](https://coveralls.io/repos/github/napalm-automation/napalm-ios/badge.svg?branch=master)](https://coveralls.io/github/napalm-automation/napalm-ios)

Expand Down
212 changes: 100 additions & 112 deletions napalm_ios/ios.py
Expand Up @@ -363,7 +363,10 @@ def compare_config(self):
# merge
cmd = 'show archive config incremental-diffs {} ignorecase'.format(new_file_full)
diff = self.device.send_command_expect(cmd)
if '% Invalid' not in diff:
if 'error code 5' in diff or 'returned error 5' in diff:
diff = "You have encountered the obscure 'error 5' message. This generally " \
"means you need to add an 'end' statement to the end of your merge changes."
elif '% Invalid' not in diff:
diff = self._normalize_merge_diff_incr(diff)
else:
cmd = 'more {}'.format(new_file_full)
Expand All @@ -374,17 +377,13 @@ def compare_config(self):

def _commit_hostname_handler(self, cmd):
"""Special handler for hostname change on commit operation."""
try:
current_prompt = self.device.find_prompt()
# Wait 12 seconds for output to come back (.2 * 60)
output = self.device.send_command_expect(cmd, delay_factor=.2, max_loops=60)
except IOError:
# Check if hostname change
if current_prompt == self.device.find_prompt():
raise
else:
self.device.set_base_prompt()
output = ''
current_prompt = self.device.find_prompt().strip()
terminating_char = current_prompt[-1]
pattern = r"[>#{}]\s*$".format(terminating_char)
# Look exclusively for trailing pattern that includes '#' and '>'
output = self.device.send_command_expect(cmd, expect_string=pattern)
# Reset base prompt in case hostname changed
self.device.set_base_prompt()
return output

def commit_config(self):
Expand All @@ -407,9 +406,9 @@ def commit_config(self):
else:
cmd = 'configure replace {} force'.format(cfg_file)
output = self._commit_hostname_handler(cmd)
if ('Failed to apply command' in output) or \
('original configuration has been successfully restored' in output) or \
('Error' in output):
if ('original configuration has been successfully restored' in output) or \
('error' in output.lower()) or \
('failed' in output.lower()):
msg = "Candidate config could not be applied\n{}".format(output)
raise ReplaceConfigException(msg)
elif '%Please turn config archive on' in output:
Expand Down Expand Up @@ -861,7 +860,7 @@ def get_facts(self):
# default values.
vendor = u'Cisco'
uptime = -1
serial_number, fqdn, os_version, hostname = (u'Unknown', u'Unknown', u'Unknown', u'Unknown')
serial_number, fqdn, os_version, hostname, domain_name = ('Unknown',) * 5

# obtain output from device
show_ver = self._send_command('show version')
Expand Down Expand Up @@ -965,18 +964,26 @@ def get_interfaces(self):
interface_dict = {}
for line in output.splitlines():

interface_regex = r"^(\S+?)\s+is\s+(.+?),\s+line\s+protocol\s+is\s+(\S+)"
if re.search(interface_regex, line):
interface_match = re.search(interface_regex, line)
interface = interface_match.groups()[0]
status = interface_match.groups()[1]
protocol = interface_match.groups()[2]

if 'admin' in status:
is_enabled = False
else:
is_enabled = True
is_up = bool('up' in protocol)
interface_regex_1 = r"^(\S+?)\s+is\s+(.+?),\s+line\s+protocol\s+is\s+(\S+)"
interface_regex_2 = r"^(\S+)\s+is\s+(up|down)"
for pattern in (interface_regex_1, interface_regex_2):
interface_match = re.search(pattern, line)
if interface_match:
interface = interface_match.group(1)
status = interface_match.group(2)
try:
protocol = interface_match.group(3)
except IndexError:
protocol = ''
if 'admin' in status.lower():
is_enabled = False
else:
is_enabled = True
if protocol:
is_up = bool('up' in protocol)
else:
is_up = bool('up' in status)
break

mac_addr_regex = r"^\s+Hardware.+address\s+is\s+({})".format(MAC_REGEX)
if re.search(mac_addr_regex, line):
Expand Down Expand Up @@ -1028,7 +1035,7 @@ def get_interfaces_ip(self):
'ipv6': { u'1::1': { 'prefix_length': 64},
u'2001:DB8:1::1': { 'prefix_length': 64},
u'2::': { 'prefix_length': 64},
u'FE80::3': { 'prefix_length': u'N/A'}}},
u'FE80::3': { 'prefix_length': 10}}},
u'Tunnel0': { 'ipv4': { u'10.63.100.9': { 'prefix_length': 24}}},
u'Tunnel1': { 'ipv4': { u'10.63.101.9': { 'prefix_length': 24}}},
u'Vlan100': { 'ipv4': { u'10.40.0.1': { 'prefix_length': 24},
Expand All @@ -1038,86 +1045,50 @@ def get_interfaces_ip(self):
"""
interfaces = {}

command = 'show ip interface brief'
output = self._send_command(command)
for line in output.splitlines():
if 'Interface' in line and 'Status' in line:
continue
fields = line.split()
if len(fields) >= 3:
interface = fields[0]
else:
raise ValueError("Unexpected response from the router")
interfaces.update({interface: {}})

# Parse IP Address and Subnet Mask from Interfaces
for interface in interfaces:
show_command = "show run interface {0}".format(interface)
interface_output = self._send_command(show_command)
for line in interface_output.splitlines():
if 'ip address ' in line and 'no ip address' not in line:
fields = line.split()
if len(fields) == 3:
# Check for 'ip address dhcp', convert to ip address and mask
if fields[2] == 'dhcp':
cmd = "show interface {} | in Internet address is".format(interface)
show_int = self._send_command(cmd)
if not show_int:
continue
int_fields = show_int.split()
ip_address, subnet = int_fields[3].split(r'/')
interfaces[interface]['ipv4'] = {ip_address: {}}
try:
val = {'prefix_length': int(subnet)}
except ValueError:
val = {'prefix_length': u'N/A'}
interfaces[interface]['ipv4'][ip_address] = val
elif len(fields) in [4, 5]:
# Check for 'ip address 10.10.10.1 255.255.255.0'
# Check for 'ip address 10.10.11.1 255.255.255.0 secondary'
if 'ipv4' not in interfaces[interface].keys():
interfaces[interface].update({'ipv4': {}})
ip_address = fields[2]

try:
subnet = sum([bin(int(x)).count('1') for x in fields[3].split('.')])
except ValueError:
subnet = u'N/A'

ip_dict = {'prefix_length': subnet}
interfaces[interface]['ipv4'].update({ip_address: {}})
interfaces[interface]['ipv4'][ip_address].update(ip_dict)
else:
raise ValueError(u"Unexpected Response from the device")

# Check IPv6
if 'ipv6 address ' in line:
fields = line.split()
ip_address = fields[2]
if 'ipv6' not in interfaces[interface].keys():
interfaces[interface].update({'ipv6': {}})

try:
if r'/' in ip_address:
# check for 'ipv6 address 1::1/64'
ip_address, subnet = ip_address.split(r'/')
interfaces[interface]['ipv6'].update({ip_address: {}})
ip_dict = {'prefix_length': int(subnet)}
else:
# check for 'ipv6 address FE80::3 link-local'
interfaces[interface]['ipv6'].update({ip_address: {}})
ip_dict = {'prefix_length': u'N/A'}
command = 'show ip interface'
show_ip_interface = self._send_command(command)
command = 'show ipv6 interface'
show_ipv6_interface = self._send_command(command)

interfaces[interface]['ipv6'][ip_address].update(ip_dict)
except AttributeError:
raise ValueError(u"Unexpected Response from the device")
INTERNET_ADDRESS = r'\s+(?:Internet address is|Secondary address)'
INTERNET_ADDRESS += r' (?P<ip>{})/(?P<prefix>\d+)'.format(IPV4_ADDR_REGEX)
LINK_LOCAL_ADDRESS = r'\s+IPv6 is enabled, link-local address is (?P<ip>[a-fA-F0-9:]+)'
GLOBAL_ADDRESS = r'\s+(?P<ip>[a-fA-F0-9:]+), subnet is (?:[a-fA-F0-9:]+)/(?P<prefix>\d+)'

# remove keys with no data
new_interfaces = {}
for k, val in interfaces.items():
if val:
new_interfaces[k] = val
return new_interfaces
interfaces = {}
for line in show_ip_interface.splitlines():
if(len(line.strip()) == 0):
continue
if(line[0] != ' '):
ipv4 = {}
interface_name = line.split()[0]
m = re.match(INTERNET_ADDRESS, line)
if m:
ip, prefix = m.groups()
ipv4.update({ip: {"prefix_length": int(prefix)}})
interfaces[interface_name] = {'ipv4': ipv4}

for line in show_ipv6_interface.splitlines():
if(len(line.strip()) == 0):
continue
if(line[0] != ' '):
ifname = line.split()[0]
ipv6 = {}
if ifname not in interfaces:
interfaces[ifname] = {'ipv6': ipv6}
else:
interfaces[ifname].update({'ipv6': ipv6})
m = re.match(LINK_LOCAL_ADDRESS, line)
if m:
ip = m.group(1)
ipv6.update({ip: {"prefix_length": 10}})
m = re.match(GLOBAL_ADDRESS, line)
if m:
ip, prefix = m.groups()
ipv6.update({ip: {"prefix_length": int(prefix)}})

# Interface without ipv6 doesn't appears in show ipv6 interface
return interfaces

@staticmethod
def bgp_time_conversion(bgp_uptime):
Expand Down Expand Up @@ -1554,13 +1525,14 @@ def get_environment(self):
"""
Get environment facts.
power, fan, temperature are currently not implemented
power and fan are currently not implemented
cpu is using 1-minute average
cpu hard-coded to cpu0 (i.e. only a single CPU)
"""
environment = {}
cpu_cmd = 'show proc cpu'
mem_cmd = 'show memory statistics'
temp_cmd = 'show env temperature status'

output = self._send_command(cpu_cmd)
environment.setdefault('cpu', {})
Expand All @@ -1586,14 +1558,30 @@ def get_environment(self):
environment['memory']['used_ram'] = used_mem
environment['memory']['available_ram'] = free_mem

# Initialize 'power', 'fan', and 'temperature' to default values (not implemented)
environment.setdefault('temperature', {})
# The 'show env temperature status' is not ubiquitous in Cisco IOS
output = self._send_command(temp_cmd)
if '% Invalid' not in output:
for line in output.splitlines():
if 'System Temperature Value' in line:
system_temp = float(line.split(':')[1].split()[0])
elif 'Yellow Threshold' in line:
system_temp_alert = float(line.split(':')[1].split()[0])
elif 'Red Threshold' in line:
system_temp_crit = float(line.split(':')[1].split()[0])
env_value = {'is_alert': system_temp >= system_temp_alert,
'is_critical': system_temp >= system_temp_crit, 'temperature': system_temp}
environment['temperature']['system'] = env_value
else:
env_value = {'is_alert': False, 'is_critical': False, 'temperature': -1.0}
environment['temperature']['invalid'] = env_value

# Initialize 'power' and 'fan' to default values (not implemented)
environment.setdefault('power', {})
environment['power']['invalid'] = {'status': True, 'output': -1.0, 'capacity': -1.0}
environment.setdefault('fans', {})
environment['fans']['invalid'] = {'status': True}
environment.setdefault('temperature', {})
env_value = {'is_alert': False, 'is_critical': False, 'temperature': -1.0}
environment['temperature']['invalid'] = env_value

return environment

def get_arp_table(self):
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
@@ -1,2 +1,2 @@
napalm_base>=0.25.0
netmiko>=1.4.2
netmiko>=1.4.3
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -11,7 +11,7 @@

setup(
name="napalm-ios",
version="0.8.0",
version="0.8.1",
packages=find_packages(),
author="Kirk Byers",
author_email="ktbyers@twb-tech.com",
Expand Down
Expand Up @@ -5,9 +5,9 @@
}
},
"temperature": {
"invalid": {
"system": {
"is_alert": false,
"temperature": -1.0,
"temperature": 42,
"is_critical": false
}
},
Expand Down
@@ -0,0 +1,4 @@
System Temperature Value: 42 Degree Celsius
System Temperature State: GREEN
Yellow Threshold : 88 Degree Celsius
Red Threshold : 93 Degree Celsius
@@ -0,0 +1,30 @@
{
"fans": {
"invalid": {
"status": true
}
},
"temperature": {
"invalid": {
"is_alert": false,
"temperature": -1.0,
"is_critical": false
}
},
"cpu": {
"0": {
"%usage": 6.0
}
},
"power": {
"invalid": {
"status": true,
"output": -1.0,
"capacity": -1.0
}
},
"memory": {
"available_ram": 4826256,
"used_ram": 20873476
}
}
@@ -0,0 +1,3 @@
^
% Invalid input detected at '^' marker.

@@ -0,0 +1,4 @@
Head Total(b) Used(b) Free(b) Lowest(b) Largest(b)
Processor 277B88C 21505428 18498044 3007384 2136352 2061756
I/O 2C00000 4194304 2375432 1818872 1581372 1817552
Driver te 18C0000 1048576 44 1048532 1048532 1048532

0 comments on commit bd7b9b0

Please sign in to comment.