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 #57 from napalm-automation/develop
Browse files Browse the repository at this point in the history
Release 0.3.0
  • Loading branch information
ktbyers authored Oct 22, 2016
2 parents 9031fde + e636f2d commit 1b0a744
Show file tree
Hide file tree
Showing 11 changed files with 118 additions and 72 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,6 @@ env
*.swp

test/unit/test_devices.py

test/unit/TestIOSDriverKB.py
#test/unit/ios/*.conf
#test/unit/ios/*.diff
33 changes: 18 additions & 15 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ language: python
python:
- 2.7
install:
- pip install -r requirements.txt
- pip install -r requirements-dev.txt
- pip install .
deploy:
provider: pypi
Expand All @@ -13,19 +13,22 @@ deploy:
tags: true
branch: master
script:
- pylama .
- cd test/unit
- nosetests -v TestIOSDriver:TestGetterIOSDriver.test_get_arp_table
- nosetests -v TestIOSDriver:TestGetterIOSDriver.test_get_bgp_neighbors
- nosetests -v TestIOSDriver:TestGetterIOSDriver.test_get_environment
- nosetests -v TestIOSDriver:TestGetterIOSDriver.test_get_facts
- nosetests -v TestIOSDriver:TestGetterIOSDriver.test_get_interfaces
- nosetests -v TestIOSDriver:TestGetterIOSDriver.test_get_interfaces_counters
- nosetests -v TestIOSDriver:TestGetterIOSDriver.test_get_interfaces_ip
- nosetests -v TestIOSDriver:TestGetterIOSDriver.test_get_lldp_neighbors
- nosetests -v TestIOSDriver:TestGetterIOSDriver.test_get_lldp_neighbors_detail
- nosetests -v TestIOSDriver:TestGetterIOSDriver.test_get_mac_address_table
- nosetests -v TestIOSDriver:TestGetterIOSDriver.test_get_ntp_stats
- nosetests -v TestIOSDriver:TestGetterIOSDriver.test_get_snmp_information
- nosetests -v TestIOSDriver:TestGetterIOSDriver.test_ios_only_bgp_time_conversion
- nosetests -v TestIOSDriver:TestGetterIOSDriver.test_ping
- nosetests --with-coverage --cover-package napalm_ios -v TestIOSDriver:TestGetterIOSDriver.test_get_arp_table
- nosetests --with-coverage --cover-package napalm_ios -v TestIOSDriver:TestGetterIOSDriver.test_get_bgp_neighbors
- nosetests --with-coverage --cover-package napalm_ios -v TestIOSDriver:TestGetterIOSDriver.test_get_environment
- nosetests --with-coverage --cover-package napalm_ios -v TestIOSDriver:TestGetterIOSDriver.test_get_facts
- nosetests --with-coverage --cover-package napalm_ios -v TestIOSDriver:TestGetterIOSDriver.test_get_interfaces
- nosetests --with-coverage --cover-package napalm_ios -v TestIOSDriver:TestGetterIOSDriver.test_get_interfaces_counters
- nosetests --with-coverage --cover-package napalm_ios -v TestIOSDriver:TestGetterIOSDriver.test_get_interfaces_ip
- nosetests --with-coverage --cover-package napalm_ios -v TestIOSDriver:TestGetterIOSDriver.test_get_lldp_neighbors
- nosetests --with-coverage --cover-package napalm_ios -v TestIOSDriver:TestGetterIOSDriver.test_get_lldp_neighbors_detail
- nosetests --with-coverage --cover-package napalm_ios -v TestIOSDriver:TestGetterIOSDriver.test_get_mac_address_table
- nosetests --with-coverage --cover-package napalm_ios -v TestIOSDriver:TestGetterIOSDriver.test_get_ntp_stats
- nosetests --with-coverage --cover-package napalm_ios -v TestIOSDriver:TestGetterIOSDriver.test_get_snmp_information
- nosetests --with-coverage --cover-package napalm_ios -v TestIOSDriver:TestGetterIOSDriver.test_ios_only_bgp_time_conversion
- nosetests --with-coverage --cover-package napalm_ios -v TestIOSDriver:TestGetterIOSDriver.test_ping
- cd ../..
- coverage combine test/unit/.coverage
after_success: coveralls
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[![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)

# napalm-ios
10 changes: 9 additions & 1 deletion napalm_ios/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,12 @@
# the License.

"""napalm_ios package."""
from ios import IOSDriver
import pkg_resources
from napalm_ios.ios import IOSDriver

try:
__version__ = pkg_resources.get_distribution('napalm-ios').version
except pkg_resources.DistributionNotFound:
__version__ = "Not installed"

__all__ = ['IOSDriver']
106 changes: 66 additions & 40 deletions napalm_ios/ios.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
from __future__ import print_function

import re
from datetime import datetime

from netmiko import ConnectHandler, FileTransfer
from napalm_base.base import NetworkDriver
Expand All @@ -40,28 +39,52 @@ def __init__(self, hostname, username, password, timeout=60, optional_args=None)
self.username = username
self.password = password
self.timeout = timeout

# Retrieve file names
self.candidate_cfg = optional_args.get('candidate_cfg', 'candidate_config.txt')
self.merge_cfg = optional_args.get('merge_cfg', 'merge_config.txt')
self.rollback_cfg = optional_args.get('rollback_cfg', 'rollback_config.txt')

# None will cause autodetection of dest_file_system
self.dest_file_system = optional_args.get('dest_file_system', None)
self.global_delay_factor = optional_args.get('global_delay_factor', .5)
self.port = optional_args.get('port', 22)
self.auto_rollback_on_error = optional_args.get('auto_rollback_on_error', True)

# Netmiko possible arguments
netmiko_argument_map = {
'port': None,
'secret': '',
'verbose': False,
'global_delay_factor': 1,
'use_keys': False,
'key_file': None,
'ssh_strict': False,
'system_host_keys': False,
'alt_host_keys': False,
'alt_key_file': '',
'ssh_config_file': None,
}

# Build dict of any optional Netmiko args
self.netmiko_optional_args = {}
for k, v in netmiko_argument_map.items():
try:
self.netmiko_optional_args[k] = optional_args[k]
except KeyError:
pass
self.global_delay_factor = optional_args.get('global_delay_factor', 1)
self.port = optional_args.get('port', 22)

self.device = None
self.config_replace = False
self.interface_map = {}

def open(self):
"""Open a connection to the device."""
self.device = ConnectHandler(device_type='cisco_ios',
ip=self.hostname,
port=self.port,
host=self.hostname,
username=self.username,
password=self.password,
global_delay_factor=self.global_delay_factor,
verbose=False)
**self.netmiko_optional_args)
if not self.dest_file_system:
try:
self.dest_file_system = self.device._autodetect_fs()
Expand Down Expand Up @@ -112,7 +135,8 @@ def load_merge_candidate(self, filename=None, config=None):
@staticmethod
def normalize_compare_config(diff):
"""Filter out strings that should not show up in the diff."""
ignore_strings = ['Contextual Config Diffs', 'No changes were found', 'file prompt quiet', 'ntp clock-period']
ignore_strings = ['Contextual Config Diffs', 'No changes were found',
'file prompt quiet', 'ntp clock-period']

new_list = []
for line in diff.splitlines():
Expand Down Expand Up @@ -190,7 +214,6 @@ def commit_config(self, filename=None):
If merge operation, perform copy <file> running-config.
"""
debug = False
# Always generate a rollback config on commit
self._gen_rollback_cfg()

Expand Down Expand Up @@ -257,7 +280,6 @@ def scp_file(self, source_file, dest_file, file_system):
"""
# Will automaticall enable SCP on remote device
enable_scp = True
debug = False

with FileTransfer(self.device,
source_file=source_file,
Expand Down Expand Up @@ -422,10 +444,12 @@ def pad_list_entries(my_list, list_length):
system_capabilities = re.findall(r"System Capabilities: (.+)", output)
enabled_capabilities = re.findall(r"Enabled Capabilities: (.+)", output)
remote_address = re.findall(r"Management Addresses:\n IP: (.+)", output)

if not remote_address:
remote_address = re.findall(r"Management Addresses:\n Other: (.+)", output)
number_entries = len(port_id)
lldp_fields = [port_id, port_description, chassis_id, system_name, system_description,
system_capabilities, enabled_capabilities, remote_address]

# Check length of each list
for test_list in lldp_fields:
if len(test_list) > number_entries:
Expand All @@ -437,8 +461,9 @@ def pad_list_entries(my_list, list_length):
# Standardize the fields
port_id, port_description, chassis_id, system_name, system_description, \
system_capabilities, enabled_capabilities, remote_address = lldp_fields
standardized_fields = zip(port_id, port_description, chassis_id, system_name, system_description,
system_capabilities, enabled_capabilities, remote_address)
standardized_fields = zip(port_id, port_description, chassis_id, system_name,
system_description, system_capabilities,
enabled_capabilities, remote_address)

lldp.setdefault(local_port, [])
for entry in standardized_fields:
Expand Down Expand Up @@ -510,7 +535,11 @@ def get_facts(self):
serial_number = serial_number.strip()

if re.search(r"Cisco IOS Software", line):
_, os_version = line.split("Cisco IOS Software, ")
try:
_, os_version = line.split("Cisco IOS Software, ")
except ValueError:
# Handle 'Cisco IOS Software [Denali],'
_, os_version = re.split(r"Cisco IOS Software \[.*?\], ", line)
os_version = os_version.strip()
elif re.search(r"IOS (tm).+Software", line):
_, os_version = line.split("IOS (tm) ")
Expand Down Expand Up @@ -714,15 +743,16 @@ def get_interfaces_ip(self):
if len(fields) == 3:
# Check for 'ip address dhcp', convert to ip address and mask
if fields[2] == 'dhcp':
show_command = "show interface {0} | in Internet address is".format(interface)
show_int = self.device.send_command(show_command)
cmd = "show interface {} | in Internet address is".format(interface)
show_int = self.device.send_command(cmd)
int_fields = show_int.split()
ip_address, subnet = int_fields[3].split(r'/')
interfaces[interface]['ipv4'] = {ip_address: {}}
try:
interfaces[interface]['ipv4'][ip_address] = {'prefix_length': int(subnet)}
val = {'prefix_length': int(subnet)}
except ValueError:
interfaces[interface]['ipv4'][ip_address] = {'prefix_length': u'N/A'}
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'
Expand Down Expand Up @@ -1073,7 +1103,8 @@ def get_environment(self):
environment.setdefault('fans', {})
environment['fans']['invalid'] = {'status': True}
environment.setdefault('temperature', {})
environment['temperature']['invalid'] = {'is_alert': False, 'is_critical': False, 'temperature': -1.0}
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 Expand Up @@ -1136,7 +1167,8 @@ def get_arp_table(self):

def cli(self, commands=None):
"""
Execute a list of commands and return the output in a dictionary format using the command as the key.
Execute a list of commands and return the output in a dictionary format using the command
as the key.
Example input:
['show clock', 'show calendar']
Expand Down Expand Up @@ -1198,10 +1230,9 @@ def get_ntp_stats(self):
return ntp_stats

def get_mac_address_table(self):

"""
Returns a lists of dictionaries. Each dictionary represents an entry in the MAC Address Table,
having the following keys
Returns a lists of dictionaries. Each dictionary represents an entry in the MAC Address
Table, having the following keys
* mac (string)
* interface (string)
* vlan (int)
Expand All @@ -1210,31 +1241,26 @@ def get_mac_address_table(self):
* moves (int)
* last_move (float)
"""

mac_address_table = []
command = 'show mac-address-table'
output = self.device.send_command(command)
output = output.strip().split('\n')

# Skip the first two lines which are headers
output = output[2:-1]

output = output[2:]
for line in output:
if len(line) == 0:
return mac_address_table
elif len(line.split()) == 4:
mac, mac_type, vlan, interface = line.split()

if mac_type.lower() in ['self', 'static']:
static = True
else:
static = False

if mac_type.lower() in ['dynamic']:
active = True
else:
active = False

entry = {
'mac': mac,
'interface': interface,
Expand All @@ -1244,11 +1270,9 @@ def get_mac_address_table(self):
'moves': -1,
'last_move': -1.0
}

mac_address_table.append(entry)
else:
raise ValueError("Unexpected output from: {}".format(line.split()))

return mac_address_table

def get_snmp_information(self):
Expand All @@ -1267,10 +1291,13 @@ def get_snmp_information(self):
'location': u'123 Anytown USA Rack 404'}
"""

# default values

snmp_dict = {}
snmp_dict = {
'chassis_id': u'unknown',
'community': {},
'contact': u'unknown',
'location': u'unknown'
}
command = 'show run | include snmp-server'
output = self.device.send_command(command)
for line in output.splitlines():
Expand All @@ -1280,26 +1307,25 @@ def get_snmp_information(self):
if 'community' not in snmp_dict.keys():
snmp_dict.update({'community': {}})
snmp_dict['community'].update({name: {}})

try:
snmp_dict['community'][name].update({'mode': fields[3].lower()})
except IndexError:
snmp_dict['community'][name].update({'mode': u'N/A'})

try:
snmp_dict['community'][name].update({'acl': fields[4]})
except IndexError:
snmp_dict['community'][name].update({'acl': u'N/A'})

elif 'snmp-server location' in line:
snmp_dict['location'] = ' '.join(fields[2:])
elif 'snmp-server contact' in line:
snmp_dict['contact'] = ' '.join(fields[2:])
elif 'snmp-server chassis-id' in line:
snmp_dict['chassis_id'] = ' '.join(fields[2:])
else:
raise ValueError("Unexpected Response from the device")

# If SNMP Chassis wasn't found; obtain using direct command
if snmp_dict['chassis_id'] == 'unknown':
command = 'show snmp chassis'
snmp_chassis = self.device.send_command(command)
snmp_dict['chassis_id'] = snmp_chassis
return snmp_dict

def ping(self, destination, source='', ttl=255, timeout=2, size=100, count=5):
Expand Down
6 changes: 0 additions & 6 deletions pylama.ini

This file was deleted.

8 changes: 8 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
coveralls
pytest
pytest-cov
pytest-json
pytest-pythonpath
pylama
flake8-import-order
-r requirements.txt
5 changes: 2 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
napalm-base
netaddr
netmiko>=0.5.0
napalm_base==0.17.0
netmiko>=1.0.0
6 changes: 6 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[pylama]
linters = mccabe,pep8,pyflakes
ignore = D203,C901

[pylama:pep8]
max_line_length = 100
3 changes: 1 addition & 2 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""setup.py file."""

import uuid

from setuptools import setup, find_packages
Expand All @@ -12,7 +11,7 @@

setup(
name="napalm-ios",
version="0.2.0",
version="0.3.0",
packages=find_packages(),
author="David Barroso",
author_email="[email protected]",
Expand Down
Loading

0 comments on commit 1b0a744

Please sign in to comment.