diff --git a/.gitignore b/.gitignore index 7b231ee..d3d0498 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,7 @@ env test/unit/test_devices.py test/unit/TestIOSDriverKB.py +test/unit/TestIOSDriverKB.py_safe #test/unit/ios/*.conf #test/unit/ios/*.diff test/unit/ios/cleanup.sh diff --git a/.travis.yml b/.travis.yml index c774c32..5bda2a7 100755 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,8 @@ python: - 3.4 - 3.5 install: -- pip install -r requirements-dev.txt -- pip install . +- pip install tox-travis +- pip install coveralls deploy: provider: pypi user: dbarroso @@ -15,8 +15,8 @@ deploy: tags: true branch: master script: -- py.test --cov-report= --cov=napalm_ios test/ -- pylama . +- tox + after_success: - coveralls - if [ $TRAVIS_TAG ]; then curl -X POST https://readthedocs.org/build/napalm; fi diff --git a/napalm_ios/ios.py b/napalm_ios/ios.py index e3048d6..44d7513 100755 --- a/napalm_ios/ios.py +++ b/napalm_ios/ios.py @@ -17,8 +17,11 @@ from __future__ import unicode_literals import re +import os +import uuid +import tempfile -from netmiko import ConnectHandler, FileTransfer +from netmiko import ConnectHandler, FileTransfer, InLineTransfer from netmiko import __version__ as netmiko_version from napalm_base.base import NetworkDriver from napalm_base.exceptions import ReplaceConfigException, MergeConfigException @@ -60,6 +63,7 @@ def __init__(self, hostname, username, password, timeout=60, optional_args=None) 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') + self.inline_transfer = optional_args.get('inline_transfer', False) # None will cause autodetection of dest_file_system self.dest_file_system = optional_args.get('dest_file_system', None) @@ -142,6 +146,53 @@ def is_alive(self): 'is_alive': self.device.remote_conn.transport.is_active() } + @staticmethod + def _create_tmp_file(config): + """Write temp file and for use with inline config and SCP.""" + tmp_dir = tempfile.gettempdir() + rand_fname = py23_compat.text_type(uuid.uuid4()) + filename = os.path.join(tmp_dir, rand_fname) + with open(filename, 'wt') as fobj: + fobj.write(config) + return filename + + def _load_candidate_wrapper(self, source_file=None, source_config=None, dest_file=None, + file_system=None): + """ + Transfer file to remote device for either merge or replace operations + + Returns (return_status, msg) + """ + return_status = False + msg = '' + if source_file and source_config: + raise ValueError("Cannot simultaneously set source_file and source_config") + + if source_config: + if self.inline_transfer: + (return_status, msg) = self._inline_tcl_xfer(source_config=source_config, + dest_file=dest_file, + file_system=file_system) + else: + # Use SCP + tmp_file = self._create_tmp_file(source_config) + (return_status, msg) = self._scp_file(source_file=tmp_file, dest_file=dest_file, + file_system=file_system) + if tmp_file and os.path.isfile(tmp_file): + os.remove(tmp_file) + if source_file: + if self.inline_transfer: + (return_status, msg) = self._inline_tcl_xfer(source_file=source_file, + dest_file=dest_file, + file_system=file_system) + else: + (return_status, msg) = self._scp_file(source_file=source_file, dest_file=dest_file, + file_system=file_system) + if not return_status: + if msg == '': + msg = "Transfer to remote device failed" + return (return_status, msg) + def load_replace_candidate(self, filename=None, config=None): """ SCP file to device filesystem, defaults to candidate_config. @@ -149,16 +200,12 @@ def load_replace_candidate(self, filename=None, config=None): Return None or raise exception """ self.config_replace = True - if config: - raise NotImplementedError - if filename: - (return_status, msg) = self._scp_file(source_file=filename, - dest_file=self.candidate_cfg, - file_system=self.dest_file_system) - if not return_status: - if msg == '': - msg = "SCP transfer to remote device failed" - raise ReplaceConfigException(msg) + return_status, msg = self._load_candidate_wrapper(source_file=filename, + source_config=config, + dest_file=self.candidate_cfg, + file_system=self.dest_file_system) + if not return_status: + raise ReplaceConfigException(msg) def load_merge_candidate(self, filename=None, config=None): """ @@ -167,16 +214,12 @@ def load_merge_candidate(self, filename=None, config=None): Merge configuration in: copy running-config """ self.config_replace = False - if config: - raise NotImplementedError - if filename: - (return_status, msg) = self._scp_file(source_file=filename, - dest_file=self.merge_cfg, - file_system=self.dest_file_system) - if not return_status: - if msg == '': - msg = "SCP transfer to remote device failed" - raise MergeConfigException(msg) + return_status, msg = self._load_candidate_wrapper(source_file=filename, + source_config=config, + dest_file=self.merge_cfg, + file_system=self.dest_file_system) + if not return_status: + raise MergeConfigException(msg) @staticmethod def _normalize_compare_config(diff): @@ -193,6 +236,42 @@ def _normalize_compare_config(diff): new_list.append(line) return "\n".join(new_list) + @staticmethod + def _normalize_merge_diff_incr(diff): + """Make the compare config output look better. + + Cisco IOS incremental-diff output + + No changes: + !List of Commands: + end + !No changes were found + """ + new_diff = [] + + changes_found = False + for line in diff.splitlines(): + if re.search(r'order-dependent line.*re-ordered', line): + changes_found = True + elif 'No changes were found' in line: + # IOS in the re-order case still claims "No changes were found" + if not changes_found: + return '' + else: + continue + + if line.strip() == 'end': + continue + elif 'List of Commands' in line: + continue + # Filter blank lines and prepend +sign + elif line.strip(): + if re.search(r"^no\s+", line.strip()): + new_diff.append('-' + line) + else: + new_diff.append('+' + line) + return "\n".join(new_diff) + @staticmethod def _normalize_merge_diff(diff): """Make compare_config() for merge look similar to replace config diff.""" @@ -202,8 +281,7 @@ def _normalize_merge_diff(diff): if line.strip(): new_diff.append('+' + line) if new_diff: - new_diff.insert(0, '! Cisco IOS does not support true compare_config() for merge: ' - 'echo merge file.') + new_diff.insert(0, '! incremental-diff failed; falling back to echo of merge file') else: new_diff.append('! No changes specified in merge file.') return "\n".join(new_diff) @@ -231,9 +309,16 @@ def compare_config(self): diff = self.device.send_command_expect(cmd) diff = self._normalize_compare_config(diff) else: - cmd = 'more {}'.format(new_file_full) + # merge + cmd = 'show archive config incremental-diffs {} ignorecase'.format(new_file_full) diff = self.device.send_command_expect(cmd) - diff = self._normalize_merge_diff(diff) + if '% Invalid' not in diff: + diff = self._normalize_merge_diff_incr(diff) + else: + cmd = 'more {}'.format(new_file_full) + diff = self.device.send_command_expect(cmd) + diff = self._normalize_merge_diff(diff) + return diff.strip() def _commit_hostname_handler(self, cmd): @@ -310,6 +395,23 @@ def rollback(self): cmd = 'configure replace {} force'.format(cfg_file) self.device.send_command_expect(cmd) + def _inline_tcl_xfer(self, source_file=None, source_config=None, dest_file=None, + file_system=None): + """ + Use Netmiko InlineFileTransfer (TCL) to transfer file or config to remote device. + + Return (status, msg) + status = boolean + msg = details on what happened + """ + if source_file: + return self._xfer_file(source_file=source_file, dest_file=dest_file, + file_system=file_system, TransferClass=InLineTransfer) + if source_config: + return self._xfer_file(source_config=source_config, dest_file=dest_file, + file_system=file_system, TransferClass=InLineTransfer) + raise ValueError("File source not specified for transfer.") + def _scp_file(self, source_file, dest_file, file_system): """ SCP file to remote device. @@ -318,30 +420,53 @@ def _scp_file(self, source_file, dest_file, file_system): status = boolean msg = details on what happened """ - # Will automaticall enable SCP on remote device - enable_scp = True + return self._xfer_file(source_file=source_file, dest_file=dest_file, + file_system=file_system, TransferClass=FileTransfer) + + def _xfer_file(self, source_file=None, source_config=None, dest_file=None, file_system=None, + TransferClass=FileTransfer): + """Transfer file to remote device. - with FileTransfer(self.device, - source_file=source_file, - dest_file=dest_file, - file_system=file_system) as scp_transfer: + By default, this will use Secure Copy if self.inline_transfer is set, then will use + Netmiko InlineTransfer method to transfer inline using either SSH or telnet (plus TCL + onbox). + + Return (status, msg) + status = boolean + msg = details on what happened + """ + if not source_file and not source_config: + raise ValueError("File source not specified for transfer.") + if not dest_file or not file_system: + raise ValueError("Destination file or file system not specified.") + + if source_file: + kwargs = dict(ssh_conn=self.device, source_file=source_file, dest_file=dest_file, + direction='put', file_system=file_system) + elif source_config: + kwargs = dict(ssh_conn=self.device, source_config=source_config, dest_file=dest_file, + direction='put', file_system=file_system) + enable_scp = True + if self.inline_transfer: + enable_scp = False + with TransferClass(**kwargs) as transfer: # Check if file already exists and has correct MD5 - if scp_transfer.check_file_exists() and scp_transfer.compare_md5(): + if transfer.check_file_exists() and transfer.compare_md5(): msg = "File already exists and has correct MD5: no SCP needed" return (True, msg) - if not scp_transfer.verify_space_available(): + if not transfer.verify_space_available(): msg = "Insufficient space available on remote device" return (False, msg) if enable_scp: - scp_transfer.enable_scp() + transfer.enable_scp() # Transfer file - scp_transfer.transfer_file() + transfer.transfer_file() # Compares MD5 between local-remote files - if scp_transfer.verify_file(): + if transfer.verify_file(): msg = "File successfully transferred to remote device" return (True, msg) else: @@ -485,16 +610,15 @@ def get_lldp_neighbors_detail(self, interface=''): return {} local_port = interface - port_id = re.findall(r"Port id: (.+)", output) - port_description = re.findall(r"Port Description: (.+)", output) - chassis_id = re.findall(r"Chassis id: (.+)", output) - system_name = re.findall(r"System Name: (.+)", output) - system_description = re.findall(r"System Description: \n(.+)", output) - 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) + port_id = re.findall(r"Port id:\s+(.+)", output) + port_description = re.findall(r"Port Description(?:\s\-|:)\s+(.+)", output) + chassis_id = re.findall(r"Chassis id:\s+(.+)", output) + system_name = re.findall(r"System Name:\s+(.+)", output) + system_description = re.findall(r"System Description:\s*\n(.+)", output) + system_capabilities = re.findall(r"System Capabilities:\s+(.+)", output) + enabled_capabilities = re.findall(r"Enabled Capabilities:\s+(.+)", output) + remote_address = re.findall(r"Management Addresses:\n\s+(?:IP|Other)(?::\s+?)(.+)", + output) number_entries = len(port_id) lldp_fields = [port_id, port_description, chassis_id, system_name, system_description, system_capabilities, enabled_capabilities, remote_address] @@ -1361,15 +1485,16 @@ def get_mac_address_table(self): RE_MACTABLE_6500_2 = r"^{}\s+{}\s+".format(VLAN_REGEX, MAC_REGEX) # 6 fields RE_MACTABLE_4500 = r"^{}\s+{}\s+".format(VLAN_REGEX, MAC_REGEX) # 5 fields RE_MACTABLE_2960_1 = r"^All\s+{}".format(MAC_REGEX) - RE_MACTABLE_2960_2 = r"^{}\s+{}\s+".format(VLAN_REGEX, MAC_REGEX) # 4 fields + RE_MACTABLE_GEN_1 = r"^{}\s+{}\s+".format(VLAN_REGEX, MAC_REGEX) # 4 fields (2960/4500) def process_mac_fields(vlan, mac, mac_type, interface): """Return proper data for mac address fields.""" - if mac_type.lower() in ['self', 'static']: + if mac_type.lower() in ['self', 'static', 'system']: static = True if vlan.lower() == 'all': vlan = 0 - if interface.lower() == 'cpu': + if interface.lower() == 'cpu' or re.search(r'router', interface.lower()) or \ + re.search(r'switch', interface.lower()): interface = '' else: static = False @@ -1394,12 +1519,18 @@ def process_mac_fields(vlan, mac, mac_type, interface): # Skip the header lines output = re.split(r'^----.*', output, flags=re.M)[1:] output = "\n".join(output).strip() + # Strip any leading astericks + output = re.sub(r"^\*", "", output, flags=re.M) for line in output.splitlines(): line = line.strip() if line == '': continue + if re.search(r"^---", line): + # Convert any '---' to VLAN 0 + line = re.sub(r"^---", "0", line, flags=re.M) + # Format1 - elif re.search(RE_MACTABLE_DEFAULT, line): + if re.search(RE_MACTABLE_DEFAULT, line): if len(line.split()) == 4: mac, mac_type, vlan, interface = line.split() mac_address_table.append(process_mac_fields(vlan, mac, mac_type, interface)) @@ -1413,20 +1544,27 @@ def process_mac_fields(vlan, mac, mac_type, interface): elif len(line.split()) == 6: vlan, mac, mac_type, _, _, interface = line.split() mac_address_table.append(process_mac_fields(vlan, mac, mac_type, interface)) - # Cat4948 format + # Cat4500 format elif re.search(RE_MACTABLE_4500, line) and len(line.split()) == 5: vlan, mac, mac_type, _, interface = line.split() mac_address_table.append(process_mac_fields(vlan, mac, mac_type, interface)) # Cat2960 format - ignore extra header line elif re.search(r"^Vlan\s+Mac Address\s+", line): continue - # Cat2960 format - elif (re.search(RE_MACTABLE_2960_1, line) or re.search(RE_MACTABLE_2960_2, line)) and \ + # Cat2960 format (Cat4500 format multicast entries) + elif (re.search(RE_MACTABLE_2960_1, line) or re.search(RE_MACTABLE_GEN_1, line)) and \ len(line.split()) == 4: vlan, mac, mac_type, interface = line.split() mac_address_table.append(process_mac_fields(vlan, mac, mac_type, interface)) + elif re.search(r"Total Mac Addresses", line): + continue + elif re.search(r"Multicast Entries", line): + continue + elif re.search(r"vlan.*mac.*address.*type.*", line): + continue else: raise ValueError("Unexpected output from: {}".format(repr(line))) + return mac_address_table def get_snmp_information(self): diff --git a/requirements.txt b/requirements.txt index 393ca39..a2c2728 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -napalm_base>=0.20.4 -netmiko>=1.0.0 +napalm_base>=0.21.0 +netmiko>=1.2.7 diff --git a/setup.cfg b/setup.cfg index 7e73a45..ac248c6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,6 +7,10 @@ skip = build/*,.tox/* max_line_length = 100 [tool:pytest] -addopts = --cov=./ -vs +addopts = --cov=napalm_ios --cov-report term-missing -vs --pylama json_report = report.json jsonapi = true + +[coverage:run] +source = napalm_ios + diff --git a/setup.py b/setup.py index 87d93f2..c00f850 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="napalm-ios", - version="0.5.1", + version="0.6.0", packages=find_packages(), author="Kirk Byers", author_email="ktbyers@twb-tech.com", diff --git a/test/unit/mocked_data/test_get_lldp_neighbors_detail/alternate/expected_result.json b/test/unit/mocked_data/test_get_lldp_neighbors_detail/alternate/expected_result.json new file mode 100644 index 0000000..11b7563 --- /dev/null +++ b/test/unit/mocked_data/test_get_lldp_neighbors_detail/alternate/expected_result.json @@ -0,0 +1,24 @@ +{ + "GigabitEthernet1": [ + { + "parent_interface": "N/A", + "remote_chassis_id": "2cc2.603e.363b", + "remote_port": "Management1", + "remote_port_description": "not advertised", + "remote_system_capab": "B,R", + "remote_system_description": "Arista Networks EOS version 4.15.2F running on an Arista Networks vEOS", + "remote_system_enable_capab": "B,R", + "remote_system_name": "eos-spine1.ntc.com" + }, + { + "parent_interface": "N/A", + "remote_chassis_id": "0005.8671.58c0", + "remote_port": "fxp0", + "remote_port_description": "fxp0", + "remote_system_capab": "B,R", + "remote_system_description": "Juniper Networks, Inc. vmx internet router, kernel JUNOS 15.1F4.15, Build date: 2015-12-23 19:22:55 UTC Copyright (c) 1996-2015 Juniper Networks, Inc.", + "remote_system_enable_capab": "B,R", + "remote_system_name": "vmx1" + } + ] +} diff --git a/test/unit/mocked_data/test_get_lldp_neighbors_detail/alternate/show_int_Gi1.txt b/test/unit/mocked_data/test_get_lldp_neighbors_detail/alternate/show_int_Gi1.txt new file mode 100644 index 0000000..4d4e9c2 --- /dev/null +++ b/test/unit/mocked_data/test_get_lldp_neighbors_detail/alternate/show_int_Gi1.txt @@ -0,0 +1,28 @@ +GigabitEthernet1 is up, line protocol is up + Hardware is CSR vNIC, address is 2cc2.603f.437a (bia 2cc2.603f.437a) + Internet address is 10.0.0.51/24 + MTU 1500 bytes, BW 1000000 Kbit/sec, DLY 10 usec, + reliability 198/255, txload 1/255, rxload 1/255 + Encapsulation ARPA, loopback not set + Keepalive set (10 sec) + Full Duplex, 1000Mbps, link type is auto, media type is RJ45 + output flow-control is unsupported, input flow-control is unsupported + ARP type: ARPA, ARP Timeout 04:00:00 + Last input 00:00:07, output 00:00:18, output hang never + Last clearing of "show interface" counters never + Input queue: 0/375/0/0 (size/max/drops/flushes); Total output drops: 0 + Queueing strategy: fifo + Output queue: 0/40 (size/max) + 5 minute input rate 1000 bits/sec, 1 packets/sec + 5 minute output rate 0 bits/sec, 0 packets/sec + 3815699 packets input, 275467675 bytes, 0 no buffer + Received 0 broadcasts (0 IP multicasts) + 0 runts, 0 giants, 0 throttles + 2610907 input errors, 0 CRC, 0 frame, 0 overrun, 0 ignored + 0 watchdog, 0 multicast, 0 pause input + 5249863 packets output, 384472234 bytes, 0 underruns + 0 output errors, 0 collisions, 0 interface resets + 11192 unknown protocol drops + 0 babbles, 0 late collision, 0 deferred + 0 lost carrier, 0 no carrier, 0 pause output + 0 output buffer failures, 0 output buffers swapped out diff --git a/test/unit/mocked_data/test_get_lldp_neighbors_detail/alternate/show_lldp_neighbors.txt b/test/unit/mocked_data/test_get_lldp_neighbors_detail/alternate/show_lldp_neighbors.txt new file mode 100644 index 0000000..1ad0449 --- /dev/null +++ b/test/unit/mocked_data/test_get_lldp_neighbors_detail/alternate/show_lldp_neighbors.txt @@ -0,0 +1,10 @@ +Capability codes: + (R) Router, (B) Bridge, (T) Telephone, (C) DOCSIS Cable Device + (W) WLAN Access Point, (P) Repeater, (S) Station, (O) Other + +Device ID Local Intf Hold-time Capability Port ID +eos-spine1.ntc.com Gi1 120 B,R Management1 +vmx1 Gi1 120 B,R fxp0 + +Total entries displayed: 2 + diff --git a/test/unit/mocked_data/test_get_lldp_neighbors_detail/alternate/show_lldp_neighbors_GigabitEthernet1_detail.txt b/test/unit/mocked_data/test_get_lldp_neighbors_detail/alternate/show_lldp_neighbors_GigabitEthernet1_detail.txt new file mode 100644 index 0000000..daa224d --- /dev/null +++ b/test/unit/mocked_data/test_get_lldp_neighbors_detail/alternate/show_lldp_neighbors_GigabitEthernet1_detail.txt @@ -0,0 +1,53 @@ +------------------------------------------------ +Local Intf: Gi1 +Chassis id: 2cc2.603e.363b +Port id: Management1 +Port Description - not advertised +System Name: eos-spine1.ntc.com + +System Description: +Arista Networks EOS version 4.15.2F running on an Arista Networks vEOS + +Time remaining: 95 seconds +System Capabilities: B,R +Enabled Capabilities: B,R +Management Addresses: + Other: 2C FF 60 3E 36 3B 00 +Auto Negotiation - not supported +Physical media capabilities - not advertised +Media Attachment Unit type - not advertised +Vlan ID: - not advertised + +------------------------------------------------ +Local Intf: Gi1 +Chassis id: 0005.8671.58c0 +Port id: fxp0 +Port Description: fxp0 +System Name: vmx1 + +System Description: +Juniper Networks, Inc. vmx internet router, kernel JUNOS 15.1F4.15, Build date: 2015-12-23 19:22:55 UTC Copyright (c) 1996-2015 Juniper Networks, Inc. + +Time remaining: 116 seconds +System Capabilities: B,R +Enabled Capabilities: B,R +Management Addresses: + IP: 10.0.0.31 + OID: + 0.1.3.6.1.2.1.31.1.1.1.1.1. +Auto Negotiation - supported, disabled +Physical media capabilities: + 1000baseT(FD) + 1000baseX(FD) + 1000baseX(HD) + Symm, Asym Pause(FD) + 100base-TX(FD) + 100base-TX(HD) + 10base-T(FD) + 10base-T(HD) +Media Attachment Unit type - not advertised +Vlan ID: - not advertised + + +Total entries displayed: 2 + diff --git a/test/unit/mocked_data/test_get_mac_address_table/3560_format/expected_result.json b/test/unit/mocked_data/test_get_mac_address_table/3560_format/expected_result.json new file mode 100644 index 0000000..87cd0a1 --- /dev/null +++ b/test/unit/mocked_data/test_get_mac_address_table/3560_format/expected_result.json @@ -0,0 +1 @@ +[{"vlan": 0, "static": true, "interface": "", "last_move": -1.0, "active": false, "mac": "0100.0ccc.cccc", "moves": -1}, {"vlan": 0, "static": true, "interface": "", "last_move": -1.0, "active": false, "mac": "0100.0ccc.cccd", "moves": -1}, {"vlan": 0, "static": true, "interface": "", "last_move": -1.0, "active": false, "mac": "0180.c200.0000", "moves": -1}, {"vlan": 0, "static": true, "interface": "", "last_move": -1.0, "active": false, "mac": "0180.c200.0001", "moves": -1}, {"vlan": 0, "static": true, "interface": "", "last_move": -1.0, "active": false, "mac": "0180.c200.0002", "moves": -1}, {"vlan": 0, "static": true, "interface": "", "last_move": -1.0, "active": false, "mac": "0180.c200.0003", "moves": -1}, {"vlan": 0, "static": true, "interface": "", "last_move": -1.0, "active": false, "mac": "0180.c200.0004", "moves": -1}, {"vlan": 0, "static": true, "interface": "", "last_move": -1.0, "active": false, "mac": "0180.c200.0005", "moves": -1}, {"vlan": 0, "static": true, "interface": "", "last_move": -1.0, "active": false, "mac": "0180.c200.0006", "moves": -1}, {"vlan": 0, "static": true, "interface": "", "last_move": -1.0, "active": false, "mac": "0180.c200.0007", "moves": -1}, {"vlan": 0, "static": true, "interface": "", "last_move": -1.0, "active": false, "mac": "0180.c200.0008", "moves": -1}, {"vlan": 0, "static": true, "interface": "", "last_move": -1.0, "active": false, "mac": "0180.c200.0009", "moves": -1}, {"vlan": 0, "static": true, "interface": "", "last_move": -1.0, "active": false, "mac": "0180.c200.000a", "moves": -1}, {"vlan": 0, "static": true, "interface": "", "last_move": -1.0, "active": false, "mac": "0180.c200.000b", "moves": -1}, {"vlan": 0, "static": true, "interface": "", "last_move": -1.0, "active": false, "mac": "0180.c200.000c", "moves": -1}, {"vlan": 0, "static": true, "interface": "", "last_move": -1.0, "active": false, "mac": "0180.c200.000d", "moves": -1}, {"vlan": 0, "static": true, "interface": "", "last_move": -1.0, "active": false, "mac": "0180.c200.000e", "moves": -1}, {"vlan": 0, "static": true, "interface": "", "last_move": -1.0, "active": false, "mac": "0180.c200.000f", "moves": -1}, {"vlan": 0, "static": true, "interface": "", "last_move": -1.0, "active": false, "mac": "0180.c200.0010", "moves": -1}, {"vlan": 0, "static": true, "interface": "", "last_move": -1.0, "active": false, "mac": "ffff.ffff.ffff", "moves": -1}, {"vlan": 1, "static": false, "interface": "Fa0/16", "last_move": -1.0, "active": true, "mac": "000a.b82d.10e0", "moves": -1}, {"vlan": 1, "static": false, "interface": "Fa0/3", "last_move": -1.0, "active": true, "mac": "0012.80b6.4cd8", "moves": -1}, {"vlan": 1, "static": false, "interface": "Fa0/16", "last_move": -1.0, "active": true, "mac": "0012.80b6.4cd9", "moves": -1}, {"vlan": 1, "static": false, "interface": "Fa0/16", "last_move": -1.0, "active": true, "mac": "0014.6915.4100", "moves": -1}, {"vlan": 1, "static": false, "interface": "Fa0/16", "last_move": -1.0, "active": true, "mac": "0018.b921.9200", "moves": -1}, {"vlan": 1, "static": false, "interface": "Fa0/1", "last_move": -1.0, "active": true, "mac": "0018.b921.9278", "moves": -1}, {"vlan": 1, "static": false, "interface": "Fa0/16", "last_move": -1.0, "active": true, "mac": "0018.b974.528f", "moves": -1}, {"vlan": 1, "static": false, "interface": "Fa0/13", "last_move": -1.0, "active": true, "mac": "0019.0617.660f", "moves": -1}, {"vlan": 1, "static": false, "interface": "Fa0/14", "last_move": -1.0, "active": true, "mac": "0019.0617.6610", "moves": -1}, {"vlan": 1, "static": false, "interface": "Fa0/15", "last_move": -1.0, "active": true, "mac": "0019.0617.6611", "moves": -1}, {"vlan": 1, "static": false, "interface": "Fa0/19", "last_move": -1.0, "active": true, "mac": "001b.d450.970f", "moves": -1}, {"vlan": 1, "static": false, "interface": "Fa0/20", "last_move": -1.0, "active": true, "mac": "001b.d450.9710", "moves": -1}, {"vlan": 1, "static": false, "interface": "Fa0/21", "last_move": -1.0, "active": true, "mac": "001b.d450.9711", "moves": -1}, {"vlan": 4, "static": false, "interface": "Fa0/16", "last_move": -1.0, "active": true, "mac": "0018.b974.528f", "moves": -1}, {"vlan": 45, "static": false, "interface": "Fa0/19", "last_move": -1.0, "active": true, "mac": "0018.b945.d5a9", "moves": -1}, {"vlan": 45, "static": false, "interface": "Fa0/5", "last_move": -1.0, "active": true, "mac": "0018.b945.f780", "moves": -1}, {"vlan": 45, "static": false, "interface": "Fa0/16", "last_move": -1.0, "active": true, "mac": "0018.b974.528f", "moves": -1}, {"vlan": 56, "static": false, "interface": "Fa0/16", "last_move": -1.0, "active": true, "mac": "0018.b945.f781", "moves": -1}, {"vlan": 56, "static": false, "interface": "Fa0/16", "last_move": -1.0, "active": true, "mac": "0018.b974.528f", "moves": -1}, {"vlan": 56, "static": false, "interface": "Fa0/19", "last_move": -1.0, "active": true, "mac": "0019.069c.80e1", "moves": -1}, {"vlan": 6, "static": false, "interface": "Fa0/16", "last_move": -1.0, "active": true, "mac": "0018.b974.528f", "moves": -1}, {"vlan": 6, "static": false, "interface": "Fa0/13", "last_move": -1.0, "active": true, "mac": "0019.069c.80e0", "moves": -1}] diff --git a/test/unit/mocked_data/test_get_mac_address_table/3560_format/show_mac_address_table.txt b/test/unit/mocked_data/test_get_mac_address_table/3560_format/show_mac_address_table.txt new file mode 100644 index 0000000..b6af798 --- /dev/null +++ b/test/unit/mocked_data/test_get_mac_address_table/3560_format/show_mac_address_table.txt @@ -0,0 +1,48 @@ + Mac Address Table +------------------------------------------- + +Vlan Mac Address Type Ports +---- ----------- -------- ----- +All 0100.0ccc.cccc STATIC CPU +All 0100.0ccc.cccd STATIC CPU +All 0180.c200.0000 STATIC CPU +All 0180.c200.0001 STATIC CPU +All 0180.c200.0002 STATIC CPU +All 0180.c200.0003 STATIC CPU +All 0180.c200.0004 STATIC CPU +All 0180.c200.0005 STATIC CPU +All 0180.c200.0006 STATIC CPU +All 0180.c200.0007 STATIC CPU +All 0180.c200.0008 STATIC CPU +All 0180.c200.0009 STATIC CPU +All 0180.c200.000a STATIC CPU +All 0180.c200.000b STATIC CPU +All 0180.c200.000c STATIC CPU +All 0180.c200.000d STATIC CPU +All 0180.c200.000e STATIC CPU +All 0180.c200.000f STATIC CPU +All 0180.c200.0010 STATIC CPU +All ffff.ffff.ffff STATIC CPU + 1 000a.b82d.10e0 DYNAMIC Fa0/16 + 1 0012.80b6.4cd8 DYNAMIC Fa0/3 + 1 0012.80b6.4cd9 DYNAMIC Fa0/16 + 1 0014.6915.4100 DYNAMIC Fa0/16 + 1 0018.b921.9200 DYNAMIC Fa0/16 + 1 0018.b921.9278 DYNAMIC Fa0/1 + 1 0018.b974.528f DYNAMIC Fa0/16 + 1 0019.0617.660f DYNAMIC Fa0/13 + 1 0019.0617.6610 DYNAMIC Fa0/14 + 1 0019.0617.6611 DYNAMIC Fa0/15 + 1 001b.d450.970f DYNAMIC Fa0/19 + 1 001b.d450.9710 DYNAMIC Fa0/20 + 1 001b.d450.9711 DYNAMIC Fa0/21 + 4 0018.b974.528f DYNAMIC Fa0/16 + 45 0018.b945.d5a9 DYNAMIC Fa0/19 + 45 0018.b945.f780 DYNAMIC Fa0/5 + 45 0018.b974.528f DYNAMIC Fa0/16 + 56 0018.b945.f781 DYNAMIC Fa0/16 + 56 0018.b974.528f DYNAMIC Fa0/16 + 56 0019.069c.80e1 DYNAMIC Fa0/19 + 6 0018.b974.528f DYNAMIC Fa0/16 + 6 0019.069c.80e0 DYNAMIC Fa0/13 +Total Mac Addresses for this criterion: 42 diff --git a/test/unit/mocked_data/test_get_mac_address_table/4500_format/expected_result.json b/test/unit/mocked_data/test_get_mac_address_table/4500_format/expected_result.json new file mode 100644 index 0000000..a08408d --- /dev/null +++ b/test/unit/mocked_data/test_get_mac_address_table/4500_format/expected_result.json @@ -0,0 +1 @@ +[{"moves": -1, "interface": "Port-channel1", "vlan": 1, "static": false, "mac": "30a3.30a3.a1c3", "active": true, "last_move": -1.0}, {"moves": -1, "interface": "Port-channel1", "vlan": 99, "static": false, "mac": "30a3.30a3.a1c4", "active": true, "last_move": -1.0}, {"moves": -1, "interface": "Port-channel1", "vlan": 99, "static": false, "mac": "30a3.30a3.a1c5", "active": true, "last_move": -1.0}, {"moves": -1, "interface": "Port-channel1", "vlan": 99, "static": false, "mac": "30a3.30a3.a1c6", "active": true, "last_move": -1.0}, {"moves": -1, "interface": "Port-channel1", "vlan": 99, "static": false, "mac": "30a3.30a3.a1c7", "active": true, "last_move": -1.0}, {"moves": -1, "interface": "Port-channel1", "vlan": 99, "static": false, "mac": "30a3.30a3.a1c8", "active": true, "last_move": -1.0}, {"moves": -1, "interface": "Port-channel1", "vlan": 99, "static": false, "mac": "30a3.30a3.a1c9", "active": true, "last_move": -1.0}, {"moves": -1, "interface": "Port-channel1", "vlan": 99, "static": false, "mac": "30a3.30a3.a1ca", "active": true, "last_move": -1.0}, {"moves": -1, "interface": "Po1", "vlan": 1, "static": true, "mac": "0100.0ccc.ccce", "active": false, "last_move": -1.0}, {"moves": -1, "interface": "Po1", "vlan": 1, "static": true, "mac": "ffff.ffff.ffff", "active": false, "last_move": -1.0}, {"moves": -1, "interface": "", "vlan": 39, "static": true, "mac": "ffff.ffff.ffff", "active": false, "last_move": -1.0}] diff --git a/test/unit/mocked_data/test_get_mac_address_table/4500_format/show_mac_address_table.txt b/test/unit/mocked_data/test_get_mac_address_table/4500_format/show_mac_address_table.txt new file mode 100644 index 0000000..de245ec --- /dev/null +++ b/test/unit/mocked_data/test_get_mac_address_table/4500_format/show_mac_address_table.txt @@ -0,0 +1,18 @@ +Unicast Entries + vlan mac address type protocols port +---------+---------------+--------+---------------------+------------------------- + 1 30a3.30a3.a1c3 dynamic ip,ipx,assigned,other Port-channel1 + 99 30a3.30a3.a1c4 dynamic ip,ipx,assigned,other Port-channel1 + 99 30a3.30a3.a1c5 dynamic ip,ipx,assigned,other Port-channel1 + 99 30a3.30a3.a1c6 dynamic ip,ipx,assigned,other Port-channel1 + 99 30a3.30a3.a1c7 dynamic ip,ipx,assigned,other Port-channel1 + 99 30a3.30a3.a1c8 dynamic ip,ipx,assigned,other Port-channel1 + 99 30a3.30a3.a1c9 dynamic ip,ipx,assigned,other Port-channel1 + 99 30a3.30a3.a1ca dynamic ip,ipx,assigned,other Port-channel1 + +Multicast Entries +vlan mac address type ports +---------+---------------+-------+-------------------------------------------- +1 0100.0ccc.ccce system Po1 +1 ffff.ffff.ffff system Po1 +39 ffff.ffff.ffff system Gi10/31,Gi10/32,Switch,Po1 diff --git a/test/unit/mocked_data/test_get_mac_address_table/6500_format2/expected_result.json b/test/unit/mocked_data/test_get_mac_address_table/6500_format2/expected_result.json new file mode 100644 index 0000000..9be13e7 --- /dev/null +++ b/test/unit/mocked_data/test_get_mac_address_table/6500_format2/expected_result.json @@ -0,0 +1 @@ +[{"last_move": -1.0, "vlan": 666, "moves": -1, "mac": "30a3.30a3.a1c3", "static": false, "interface": "Te1/30", "active": true}, {"last_move": -1.0, "vlan": 666, "moves": -1, "mac": "30a3.30a3.5ab8", "static": false, "interface": "Po3", "active": true}, {"last_move": -1.0, "vlan": 60, "moves": -1, "mac": "30a3.30a3.4d54", "static": false, "interface": "Te1/21", "active": true}, {"last_move": -1.0, "vlan": 777, "moves": -1, "mac": "0000.30a3.0167", "static": true, "interface": "", "active": false}, {"last_move": -1.0, "vlan": 664, "moves": -1, "mac": "30a3.30a3.58b5", "static": false, "interface": "Po6", "active": true}, {"last_move": -1.0, "vlan": 667, "moves": -1, "mac": "30a3.30a3.daf5", "static": false, "interface": "Te3/20", "active": true}, {"last_move": -1.0, "vlan": 668, "moves": -1, "mac": "30a3.30a3.e401", "static": false, "interface": "Po6", "active": true}, {"last_move": -1.0, "vlan": 669, "moves": -1, "mac": "30a3.30a3.5a22", "static": false, "interface": "Te3/20", "active": true}, {"last_move": -1.0, "vlan": 0, "moves": -1, "mac": "0000.0000.0000", "static": true, "interface": "", "active": false}] diff --git a/test/unit/mocked_data/test_get_mac_address_table/6500_format2/show_mac_address_table.txt b/test/unit/mocked_data/test_get_mac_address_table/6500_format2/show_mac_address_table.txt new file mode 100644 index 0000000..e86a9a7 --- /dev/null +++ b/test/unit/mocked_data/test_get_mac_address_table/6500_format2/show_mac_address_table.txt @@ -0,0 +1,15 @@ +Legend: * - primary entry + age - seconds since last seen + n/a - not available + + vlan mac address type learn age ports +------+----------------+--------+-----+----------+-------------------------- + 666 30a3.30a3.a1c3 dynamic Yes 1200 Te1/30 + 666 30a3.30a3.5ab8 dynamic Yes 0 Po3 + 60 30a3.30a3.4d54 dynamic Yes 3600 Te1/21 +* 777 0000.30a3.0167 static No - Router + 664 30a3.30a3.58b5 dynamic Yes 180 Po6 + 667 30a3.30a3.daf5 dynamic Yes 0 Te3/20 + 668 30a3.30a3.e401 dynamic Yes 600 Po6 + 669 30a3.30a3.5a22 dynamic Yes 300 Te3/20 +* --- 0000.0000.0000 static No - Router diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..5ae5d81 --- /dev/null +++ b/tox.ini @@ -0,0 +1,9 @@ +[tox] +envlist = py27,py34,py35 + +[testenv] +deps = + -rrequirements-dev.txt + +commands= + py.test