From 3224f96be7181d092f177610e6879c84e6b8a0c6 Mon Sep 17 00:00:00 2001 From: The Loeki Date: Tue, 6 Jun 2017 19:02:42 +0200 Subject: [PATCH 01/15] Add config syntax support for 'transport=telnet' --- napalm_ios/ios.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/napalm_ios/ios.py b/napalm_ios/ios.py index 8330c41..a323c7c 100644 --- a/napalm_ios/ios.py +++ b/napalm_ios/ios.py @@ -73,6 +73,8 @@ def __init__(self, hostname, username, password, timeout=60, optional_args=None) self.password = password self.timeout = timeout + self.transport = optional_args.get('transport', 'ssh') + # 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') @@ -114,7 +116,7 @@ def __init__(self, hostname, username, password, timeout=60, optional_args=None) except KeyError: pass self.global_delay_factor = optional_args.get('global_delay_factor', 1) - self.port = optional_args.get('port', 22) + self.port = optional_args.get('port', {'ssh': 22, 'telnet': 23}[self.transport]) self.device = None self.config_replace = False @@ -124,7 +126,10 @@ def __init__(self, hostname, username, password, timeout=60, optional_args=None) def open(self): """Open a connection to the device.""" - self.device = ConnectHandler(device_type='cisco_ios', + device_type = 'cisco_ios' + if self.transport != 'ssh': + device_type += '_' + self.transport + self.device = ConnectHandler(device_type=device_type, host=self.hostname, username=self.username, password=self.password, From c89df08f5a1c299cea8b7177a1cfb6d7383f5012 Mon Sep 17 00:00:00 2001 From: itdependsnetworks Date: Sun, 2 Jul 2017 22:14:57 -0400 Subject: [PATCH 02/15] Update mac for 6500 one of use case --- napalm_ios/ios.py | 26 +- .../6500_format3/expected_result.json | 245 ++++++++++++++++++ .../6500_format3/show_mac_address_table.txt | 22 ++ 3 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 test/unit/mocked_data/test_get_mac_address_table/6500_format3/expected_result.json create mode 100644 test/unit/mocked_data/test_get_mac_address_table/6500_format3/show_mac_address_table.txt diff --git a/napalm_ios/ios.py b/napalm_ios/ios.py index 616eee1..153b583 100644 --- a/napalm_ios/ios.py +++ b/napalm_ios/ios.py @@ -1774,6 +1774,7 @@ def get_mac_address_table(self): RE_MACTABLE_DEFAULT = r"^" + MAC_REGEX RE_MACTABLE_6500_1 = r"^\*\s+{}\s+{}\s+".format(VLAN_REGEX, MAC_REGEX) # 7 fields RE_MACTABLE_6500_2 = r"^{}\s+{}\s+".format(VLAN_REGEX, MAC_REGEX) # 6 fields + RE_MACTABLE_6500_3 = r"^\s{51}\S+" # Fill down from prior 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_GEN_1 = r"^{}\s+{}\s+".format(VLAN_REGEX, MAC_REGEX) # 4 fields (2960/4500) @@ -1812,7 +1813,21 @@ def process_mac_fields(vlan, mac, mac_type, interface): output = "\n".join(output).strip() # Strip any leading astericks output = re.sub(r"^\*", "", output, flags=re.M) + fill_down_vlan = fill_down_mac = fill_down_mac_type = '' for line in output.splitlines(): + # Cat6500 one off format + if (re.search(RE_MACTABLE_6500_3, line)): + interface = line.strip() + if ',' in interface: + interfaces = interface.split(',') + else: + interfaces = [] + interfaces.append(interface) + for single_interface in interfaces: + mac_address_table.append(process_mac_fields(fill_down_vlan, fill_down_mac, + fill_down_mac_type, + single_interface)) + continue line = line.strip() if line == '': continue @@ -1834,7 +1849,16 @@ def process_mac_fields(vlan, mac, mac_type, interface): _, vlan, mac, mac_type, _, _, interface = line.split() elif len(line.split()) == 6: vlan, mac, mac_type, _, _, interface = line.split() - mac_address_table.append(process_mac_fields(vlan, mac, mac_type, interface)) + if ',' in interface: + interfaces = interface.split(',') + fill_down_vlan = vlan + fill_down_mac = mac + fill_down_mac_type = mac_type + for single_interface in interfaces: + mac_address_table.append(process_mac_fields(vlan, mac, mac_type, + single_interface)) + else: + mac_address_table.append(process_mac_fields(vlan, mac, mac_type, interface)) # Cat4500 format elif re.search(RE_MACTABLE_4500, line) and len(line.split()) == 5: vlan, mac, mac_type, _, interface = line.split() diff --git a/test/unit/mocked_data/test_get_mac_address_table/6500_format3/expected_result.json b/test/unit/mocked_data/test_get_mac_address_table/6500_format3/expected_result.json new file mode 100644 index 0000000..18eeafb --- /dev/null +++ b/test/unit/mocked_data/test_get_mac_address_table/6500_format3/expected_result.json @@ -0,0 +1,245 @@ +[ + { + "vlan": 19, + "active": true, + "last_move": -1, + "static": false, + "mac": "00:FF:9A:B4:03:AD", + "moves": -1, + "interface": "Te7/1" + }, + { + "vlan": 197, + "active": false, + "last_move": -1, + "static": true, + "mac": "C0:FF:BC:1F:72:F2", + "moves": -1, + "interface": "Gi4/28" + }, + { + "vlan": 197, + "active": false, + "last_move": -1, + "static": true, + "mac": "10:FF:AE:63:7A:9E", + "moves": -1, + "interface": "Gi1/14" + }, + { + "vlan": 197, + "active": false, + "last_move": -1, + "static": true, + "mac": "D4:FF:56:B2:95:27", + "moves": -1, + "interface": "Gi2/1" + }, + { + "vlan": 100, + "active": false, + "last_move": -1, + "static": true, + "mac": "1C:FF:0F:C9:11:80", + "moves": -1, + "interface": "" + }, + { + "vlan": 0, + "active": false, + "last_move": -1, + "static": true, + "mac": "00:FF:00:00:00:00", + "moves": -1, + "interface": "" + }, + { + "vlan": 118, + "active": false, + "last_move": -1, + "static": true, + "mac": "50:FF:9D:CC:18:2B", + "moves": -1, + "interface": "Gi2/16" + }, + { + "vlan": 118, + "active": false, + "last_move": -1, + "static": true, + "mac": "50:FF:9D:CC:16:67", + "moves": -1, + "interface": "Gi3/29" + }, + { + "vlan": 197, + "active": false, + "last_move": -1, + "static": true, + "mac": "10:FF:AE:63:9F:FE", + "moves": -1, + "interface": "Gi4/18" + }, + { + "vlan": 197, + "active": false, + "last_move": -1, + "static": true, + "mac": "33:33:00:00:00:FF", + "moves": -1, + "interface": "Gi1/1" + }, + { + "vlan": 197, + "active": false, + "last_move": -1, + "static": true, + "mac": "33:33:00:00:00:FF", + "moves": -1, + "interface": "Gi1/2" + }, + { + "vlan": 197, + "active": false, + "last_move": -1, + "static": true, + "mac": "33:33:00:00:00:FF", + "moves": -1, + "interface": "Gi1/3" + }, + { + "vlan": 197, + "active": false, + "last_move": -1, + "static": true, + "mac": "33:33:00:00:00:FF", + "moves": -1, + "interface": "Gi1/4" + }, + { + "vlan": 197, + "active": false, + "last_move": -1, + "static": true, + "mac": "33:33:00:00:00:FF", + "moves": -1, + "interface": "Gi1/6" + }, + { + "vlan": 197, + "active": false, + "last_move": -1, + "static": true, + "mac": "33:33:00:00:00:FF", + "moves": -1, + "interface": "Gi1/7" + }, + { + "vlan": 197, + "active": false, + "last_move": -1, + "static": true, + "mac": "33:33:00:00:00:FF", + "moves": -1, + "interface": "Gi1/8" + }, + { + "vlan": 197, + "active": false, + "last_move": -1, + "static": true, + "mac": "33:33:00:00:00:FF", + "moves": -1, + "interface": "Gi1/9" + }, + { + "vlan": 197, + "active": false, + "last_move": -1, + "static": true, + "mac": "33:33:00:00:00:FF", + "moves": -1, + "interface": "Gi10/44" + }, + { + "vlan": 197, + "active": false, + "last_move": -1, + "static": true, + "mac": "33:33:00:00:00:FF", + "moves": -1, + "interface": "Gi10/45" + }, + { + "vlan": 197, + "active": false, + "last_move": -1, + "static": true, + "mac": "33:33:00:00:00:FF", + "moves": -1, + "interface": "Gi10/46" + }, + { + "vlan": 197, + "active": false, + "last_move": -1, + "static": true, + "mac": "33:33:00:00:00:FF", + "moves": -1, + "interface": "Gi10/48" + }, + { + "vlan": 197, + "active": false, + "last_move": -1, + "static": true, + "mac": "33:33:00:00:00:FF", + "moves": -1, + "interface": "" + }, + { + "vlan": 197, + "active": false, + "last_move": -1, + "static": true, + "mac": "33:33:00:00:00:FF", + "moves": -1, + "interface": "" + }, + { + "vlan": 197, + "active": false, + "last_move": -1, + "static": true, + "mac": "33:33:00:00:00:FF", + "moves": -1, + "interface": "" + }, + { + "vlan": 197, + "active": false, + "last_move": -1, + "static": true, + "mac": "33:33:00:00:00:FF", + "moves": -1, + "interface": "Gi10/43" + }, + { + "vlan": 197, + "active": false, + "last_move": -1, + "static": true, + "mac": "C0:FF:BC:1F:71:B7", + "moves": -1, + "interface": "Gi5/8" + }, + { + "vlan": 118, + "active": false, + "last_move": -1, + "static": true, + "mac": "3C:FF:0E:D0:0F:7B", + "moves": -1, + "interface": "Gi4/16" + } +] diff --git a/test/unit/mocked_data/test_get_mac_address_table/6500_format3/show_mac_address_table.txt b/test/unit/mocked_data/test_get_mac_address_table/6500_format3/show_mac_address_table.txt new file mode 100644 index 0000000..d25fbc7 --- /dev/null +++ b/test/unit/mocked_data/test_get_mac_address_table/6500_format3/show_mac_address_table.txt @@ -0,0 +1,22 @@ +Legend: * - primary entry + age - seconds since last seen + n/a - not available + + vlan mac address type learn age ports +------+----------------+--------+-----+----------+-------------------------- +* 19 00ff.9ab4.03ad dynamic Yes 25 Te7/1 + 197 c0ff.bc1f.72f2 static Yes - Gi4/28 + 197 10ff.ae63.7a9e static Yes - Gi1/14 + 197 d4ff.56b2.9527 static Yes - Gi2/1 +* 100 1cff.0fc9.1180 static No - Router +* --- 00ff.0000.0000 static No - Router + 118 50ff.9dcc.182b static Yes - Gi2/16 + 118 50ff.9dcc.1667 static Yes - Gi3/29 + 197 10ff.ae63.9ffe static Yes - Gi4/18 +* 197 3333.0000.00ff static Yes - Gi1/1,Gi1/2,Gi1/3,Gi1/4 + Gi1/6,Gi1/7,Gi1/8,Gi1/9 + Gi10/44,Gi10/45,Gi10/46 + Gi10/48,Router,Switch,Stby-Switch + Gi10/43 + 197 c0ff.bc1f.71b7 static Yes - Gi5/8 + 118 3cff.0ed0.0f7b static Yes - Gi4/16 From b1559148371969dd87843cdab23e14c86486ea3b Mon Sep 17 00:00:00 2001 From: itdependsnetworks Date: Mon, 3 Jul 2017 01:24:37 -0400 Subject: [PATCH 03/15] move auto detetec to merge and replace only --- napalm_ios/ios.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/napalm_ios/ios.py b/napalm_ios/ios.py index 616eee1..b0b13fd 100644 --- a/napalm_ios/ios.py +++ b/napalm_ios/ios.py @@ -135,12 +135,13 @@ def open(self): **self.netmiko_optional_args) # ensure in enable mode self.device.enable() - if not self.dest_file_system: - try: - self.dest_file_system = self.device._autodetect_fs() - except AttributeError: - raise AttributeError("Netmiko _autodetect_fs not found please upgrade Netmiko or " - "specify dest_file_system in optional_args.") + + def _autodetect(self): + try: + return self.device._autodetect_fs() + except AttributeError: + raise AttributeError("Netmiko _autodetect_fs not found please upgrade Netmiko or " + "specify dest_file_system in optional_args.") def close(self): """Close the connection to the device.""" @@ -235,6 +236,8 @@ def load_replace_candidate(self, filename=None, config=None): Return None or raise exception """ self.config_replace = True + if not self.dest_file_system or self.dest_file_system == None: + self.dest_file_system = self._autodetect() return_status, msg = self._load_candidate_wrapper(source_file=filename, source_config=config, dest_file=self.candidate_cfg, @@ -249,6 +252,9 @@ def load_merge_candidate(self, filename=None, config=None): Merge configuration in: copy running-config """ self.config_replace = False + + if not self.dest_file_system or self.dest_file_system == None: + self.dest_file_system = self._autodetect() return_status, msg = self._load_candidate_wrapper(source_file=filename, source_config=config, dest_file=self.merge_cfg, From 06cfc65501dbb81864bbd5c4b9bc36aaf8665369 Mon Sep 17 00:00:00 2001 From: itdependsnetworks Date: Mon, 3 Jul 2017 01:56:04 -0400 Subject: [PATCH 04/15] fix 4500 multicast --- napalm_ios/ios.py | 20 +- .../4500_format/expected_result.json | 129 +++++++++- .../4500_format2/expected_result.json | 227 ++++++++++++++++++ .../4500_format2/show_mac_address_table.txt | 19 ++ 4 files changed, 389 insertions(+), 6 deletions(-) create mode 100644 test/unit/mocked_data/test_get_mac_address_table/4500_format2/expected_result.json create mode 100644 test/unit/mocked_data/test_get_mac_address_table/4500_format2/show_mac_address_table.txt diff --git a/napalm_ios/ios.py b/napalm_ios/ios.py index 153b583..400b1f5 100644 --- a/napalm_ios/ios.py +++ b/napalm_ios/ios.py @@ -1775,7 +1775,8 @@ def get_mac_address_table(self): RE_MACTABLE_6500_1 = r"^\*\s+{}\s+{}\s+".format(VLAN_REGEX, MAC_REGEX) # 7 fields RE_MACTABLE_6500_2 = r"^{}\s+{}\s+".format(VLAN_REGEX, MAC_REGEX) # 6 fields RE_MACTABLE_6500_3 = r"^\s{51}\S+" # Fill down from prior - RE_MACTABLE_4500 = r"^{}\s+{}\s+".format(VLAN_REGEX, MAC_REGEX) # 5 fields + RE_MACTABLE_4500_1 = r"^{}\s+{}\s+".format(VLAN_REGEX, MAC_REGEX) # 5 fields + RE_MACTABLE_4500_2 = r"^\s{32}\S+" # Fill down from prior RE_MACTABLE_2960_1 = r"^All\s+{}".format(MAC_REGEX) RE_MACTABLE_GEN_1 = r"^{}\s+{}\s+".format(VLAN_REGEX, MAC_REGEX) # 4 fields (2960/4500) @@ -1815,8 +1816,8 @@ def process_mac_fields(vlan, mac, mac_type, interface): output = re.sub(r"^\*", "", output, flags=re.M) fill_down_vlan = fill_down_mac = fill_down_mac_type = '' for line in output.splitlines(): - # Cat6500 one off format - if (re.search(RE_MACTABLE_6500_3, line)): + # Cat6500 one off anf 4500 multicast format + if (re.search(RE_MACTABLE_6500_3, line) or re.search(RE_MACTABLE_4500_2, line)): interface = line.strip() if ',' in interface: interfaces = interface.split(',') @@ -1860,7 +1861,7 @@ def process_mac_fields(vlan, mac, mac_type, interface): else: mac_address_table.append(process_mac_fields(vlan, mac, mac_type, interface)) # Cat4500 format - elif re.search(RE_MACTABLE_4500, line) and len(line.split()) == 5: + elif re.search(RE_MACTABLE_4500_1, 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 @@ -1870,7 +1871,16 @@ def process_mac_fields(vlan, mac, mac_type, interface): 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)) + if ',' in interface: + interfaces = interface.split(',') + fill_down_vlan = vlan + fill_down_mac = mac + fill_down_mac_type = mac_type + for single_interface in interfaces: + mac_address_table.append(process_mac_fields(vlan, mac, mac_type, + single_interface)) + else: + 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): 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 index 02d9b3a..40b91b5 100644 --- 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 @@ -1 +1,128 @@ -[{"moves": -1, "interface": "Port-channel1", "vlan": 1, "static": false, "mac": "30:A3:30:A3:A1:C3", "active": true, "last_move": -1.0}, {"moves": -1, "interface": "Port-channel1", "vlan": 99, "static": false, "mac": "30:A3:30:A3:A1:C4", "active": true, "last_move": -1.0}, {"moves": -1, "interface": "Port-channel1", "vlan": 99, "static": false, "mac": "30:A3:30:A3:A1:C5", "active": true, "last_move": -1.0}, {"moves": -1, "interface": "Port-channel1", "vlan": 99, "static": false, "mac": "30:A3:30:A3:A1:C6", "active": true, "last_move": -1.0}, {"moves": -1, "interface": "Port-channel1", "vlan": 99, "static": false, "mac": "30:A3:30:A3:A1:C7", "active": true, "last_move": -1.0}, {"moves": -1, "interface": "Port-channel1", "vlan": 99, "static": false, "mac": "30:A3:30:A3:A1:C8", "active": true, "last_move": -1.0}, {"moves": -1, "interface": "Port-channel1", "vlan": 99, "static": false, "mac": "30:A3:30:A3:A1:C9", "active": true, "last_move": -1.0}, {"moves": -1, "interface": "Port-channel1", "vlan": 99, "static": false, "mac": "30:A3:30:A3:A1:CA", "active": true, "last_move": -1.0}, {"moves": -1, "interface": "Po1", "vlan": 1, "static": true, "mac": "01:00:0C:CC:CC:CE", "active": false, "last_move": -1.0}, {"moves": -1, "interface": "Po1", "vlan": 1, "static": true, "mac": "FF:FF:FF:FF:FF:FF", "active": false, "last_move": -1.0}, {"moves": -1, "interface": "", "vlan": 39, "static": true, "mac": "FF:FF:FF:FF:FF:FF", "active": false, "last_move": -1.0}] +[ + { + "moves": -1, + "interface": "Port-channel1", + "vlan": 1, + "static": false, + "mac": "30:A3:30:A3:A1:C3", + "active": true, + "last_move": -1 + }, + { + "moves": -1, + "interface": "Port-channel1", + "vlan": 99, + "static": false, + "mac": "30:A3:30:A3:A1:C4", + "active": true, + "last_move": -1 + }, + { + "moves": -1, + "interface": "Port-channel1", + "vlan": 99, + "static": false, + "mac": "30:A3:30:A3:A1:C5", + "active": true, + "last_move": -1 + }, + { + "moves": -1, + "interface": "Port-channel1", + "vlan": 99, + "static": false, + "mac": "30:A3:30:A3:A1:C6", + "active": true, + "last_move": -1 + }, + { + "moves": -1, + "interface": "Port-channel1", + "vlan": 99, + "static": false, + "mac": "30:A3:30:A3:A1:C7", + "active": true, + "last_move": -1 + }, + { + "moves": -1, + "interface": "Port-channel1", + "vlan": 99, + "static": false, + "mac": "30:A3:30:A3:A1:C8", + "active": true, + "last_move": -1 + }, + { + "moves": -1, + "interface": "Port-channel1", + "vlan": 99, + "static": false, + "mac": "30:A3:30:A3:A1:C9", + "active": true, + "last_move": -1 + }, + { + "moves": -1, + "interface": "Port-channel1", + "vlan": 99, + "static": false, + "mac": "30:A3:30:A3:A1:CA", + "active": true, + "last_move": -1 + }, + { + "moves": -1, + "interface": "Po1", + "vlan": 1, + "static": true, + "mac": "01:00:0C:CC:CC:CE", + "active": false, + "last_move": -1 + }, + { + "moves": -1, + "interface": "Po1", + "vlan": 1, + "static": true, + "mac": "FF:FF:FF:FF:FF:FF", + "active": false, + "last_move": -1 + }, + { + "moves": -1, + "interface": "Gi10/31", + "vlan": 39, + "static": true, + "mac": "FF:FF:FF:FF:FF:FF", + "active": false, + "last_move": -1 + }, + { + "moves": -1, + "interface": "Gi10/32", + "vlan": 39, + "static": true, + "mac": "FF:FF:FF:FF:FF:FF", + "active": false, + "last_move": -1 + }, + { + "moves": -1, + "interface": "", + "vlan": 39, + "static": true, + "mac": "FF:FF:FF:FF:FF:FF", + "active": false, + "last_move": -1 + }, + { + "moves": -1, + "interface": "Po1", + "vlan": 39, + "static": true, + "mac": "FF:FF:FF:FF:FF:FF", + "active": false, + "last_move": -1 + } +] diff --git a/test/unit/mocked_data/test_get_mac_address_table/4500_format2/expected_result.json b/test/unit/mocked_data/test_get_mac_address_table/4500_format2/expected_result.json new file mode 100644 index 0000000..9eaa4ce --- /dev/null +++ b/test/unit/mocked_data/test_get_mac_address_table/4500_format2/expected_result.json @@ -0,0 +1,227 @@ +[ + { + "vlan": 1, + "interface": "Port-channel21", + "static": false, + "mac": "00:11:21:E4:87:10", + "moves": -1, + "active": true, + "last_move": -1 + }, + { + "vlan": 1, + "interface": "", + "static": true, + "mac": "00:12:DA:F7:0C:FF", + "moves": -1, + "active": false, + "last_move": -1 + }, + { + "vlan": 1, + "interface": "Port-channel21", + "static": false, + "mac": "00:17:94:15:EE:C0", + "moves": -1, + "active": true, + "last_move": -1 + }, + { + "vlan": 11, + "interface": "GigabitEthernet3/15", + "static": true, + "mac": "00:04:F2:FE:7F:95", + "moves": -1, + "active": false, + "last_move": -1 + }, + { + "vlan": 11, + "interface": "GigabitEthernet2/47", + "static": true, + "mac": "00:04:F2:FE:82:DA", + "moves": -1, + "active": false, + "last_move": -1 + }, + { + "vlan": 1, + "interface": "Po21", + "static": true, + "mac": "01:00:0C:CC:CC:CE", + "moves": -1, + "active": false, + "last_move": -1 + }, + { + "vlan": 1, + "interface": "", + "static": true, + "mac": "FF:FF:FF:FF:FF:FF", + "moves": -1, + "active": false, + "last_move": -1 + }, + { + "vlan": 1, + "interface": "Po21", + "static": true, + "mac": "FF:FF:FF:FF:FF:FF", + "moves": -1, + "active": false, + "last_move": -1 + }, + { + "vlan": 11, + "interface": "Gi2/4", + "static": true, + "mac": "FF:FF:FF:FF:FF:FF", + "moves": -1, + "active": false, + "last_move": -1 + }, + { + "vlan": 11, + "interface": "Gi2/5", + "static": true, + "mac": "FF:FF:FF:FF:FF:FF", + "moves": -1, + "active": false, + "last_move": -1 + }, + { + "vlan": 11, + "interface": "Gi2/8", + "static": true, + "mac": "FF:FF:FF:FF:FF:FF", + "moves": -1, + "active": false, + "last_move": -1 + }, + { + "vlan": 11, + "interface": "Gi2/11", + "static": true, + "mac": "FF:FF:FF:FF:FF:FF", + "moves": -1, + "active": false, + "last_move": -1 + }, + { + "vlan": 11, + "interface": "Gi2/12", + "static": true, + "mac": "FF:FF:FF:FF:FF:FF", + "moves": -1, + "active": false, + "last_move": -1 + }, + { + "vlan": 11, + "interface": "Gi2/13", + "static": true, + "mac": "FF:FF:FF:FF:FF:FF", + "moves": -1, + "active": false, + "last_move": -1 + }, + { + "vlan": 11, + "interface": "Gi2/19", + "static": true, + "mac": "FF:FF:FF:FF:FF:FF", + "moves": -1, + "active": false, + "last_move": -1 + }, + { + "vlan": 11, + "interface": "Gi2/41", + "static": true, + "mac": "FF:FF:FF:FF:FF:FF", + "moves": -1, + "active": false, + "last_move": -1 + }, + { + "vlan": 11, + "interface": "Gi2/42", + "static": true, + "mac": "FF:FF:FF:FF:FF:FF", + "moves": -1, + "active": false, + "last_move": -1 + }, + { + "vlan": 11, + "interface": "Gi2/43", + "static": true, + "mac": "FF:FF:FF:FF:FF:FF", + "moves": -1, + "active": false, + "last_move": -1 + }, + { + "vlan": 11, + "interface": "Gi2/47", + "static": true, + "mac": "FF:FF:FF:FF:FF:FF", + "moves": -1, + "active": false, + "last_move": -1 + }, + { + "vlan": 11, + "interface": "Gi3/10", + "static": true, + "mac": "FF:FF:FF:FF:FF:FF", + "moves": -1, + "active": false, + "last_move": -1 + }, + { + "vlan": 11, + "interface": "Gi3/15", + "static": true, + "mac": "FF:FF:FF:FF:FF:FF", + "moves": -1, + "active": false, + "last_move": -1 + }, + { + "vlan": 11, + "interface": "Gi3/16", + "static": true, + "mac": "FF:FF:FF:FF:FF:FF", + "moves": -1, + "active": false, + "last_move": -1 + }, + { + "vlan": 24, + "interface": "Po21", + "static": true, + "mac": "FF:FF:FF:FF:FF:FF", + "moves": -1, + "active": false, + "last_move": -1 + }, + { + "vlan": 27, + "interface": "Gi3/30", + "static": true, + "mac": "FF:FF:FF:FF:FF:FF", + "moves": -1, + "active": false, + "last_move": -1 + }, + { + "vlan": 27, + "interface": "Po21", + "static": true, + "mac": "FF:FF:FF:FF:FF:FF", + "moves": -1, + "active": false, + "last_move": -1 + } +] diff --git a/test/unit/mocked_data/test_get_mac_address_table/4500_format2/show_mac_address_table.txt b/test/unit/mocked_data/test_get_mac_address_table/4500_format2/show_mac_address_table.txt new file mode 100644 index 0000000..c799074 --- /dev/null +++ b/test/unit/mocked_data/test_get_mac_address_table/4500_format2/show_mac_address_table.txt @@ -0,0 +1,19 @@ +Unicast Entries + vlan mac address type protocols port +-------+---------------+--------+---------------------+-------------------- + 1 0011.21e4.8710 dynamic ip,assigned,other Port-channel21 + 1 0012.daf7.0cff static ip,ipx,assigned,other Switch + 1 0017.9415.eec0 dynamic ip,assigned Port-channel21 + 11 0004.f2fe.7f95 static ip,ipx,assigned,other GigabitEthernet3/15 + 11 0004.f2fe.82da static ip,ipx,assigned,other GigabitEthernet2/47 + +Multicast Entries + vlan mac address type ports +-------+---------------+-------+-------------------------------------------- + 1 0100.0ccc.ccce system Po21 + 1 ffff.ffff.ffff system Switch,Po21 + 11 ffff.ffff.ffff system Gi2/4,Gi2/5,Gi2/8,Gi2/11,Gi2/12,Gi2/13,Gi2/19 + Gi2/41,Gi2/42,Gi2/43,Gi2/47,Gi3/10,Gi3/15 + Gi3/16 + 24 ffff.ffff.ffff system Po21 + 27 ffff.ffff.ffff system Gi3/30,Po21 From 6ad5fed4eead4c92ccee02e3e69c63ef330a1658 Mon Sep 17 00:00:00 2001 From: itdependsnetworks Date: Mon, 3 Jul 2017 02:01:00 -0400 Subject: [PATCH 05/15] pep8 compliance --- napalm_ios/ios.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/napalm_ios/ios.py b/napalm_ios/ios.py index b0b13fd..347f7eb 100644 --- a/napalm_ios/ios.py +++ b/napalm_ios/ios.py @@ -236,7 +236,7 @@ def load_replace_candidate(self, filename=None, config=None): Return None or raise exception """ self.config_replace = True - if not self.dest_file_system or self.dest_file_system == None: + if not self.dest_file_system or self.dest_file_system is None: self.dest_file_system = self._autodetect() return_status, msg = self._load_candidate_wrapper(source_file=filename, source_config=config, @@ -253,7 +253,7 @@ def load_merge_candidate(self, filename=None, config=None): """ self.config_replace = False - if not self.dest_file_system or self.dest_file_system == None: + if not self.dest_file_system or self.dest_file_system is None: self.dest_file_system = self._autodetect() return_status, msg = self._load_candidate_wrapper(source_file=filename, source_config=config, From d523fca8922ca4196d53ea1a94acbe06dc9ddc7d Mon Sep 17 00:00:00 2001 From: itdependsnetworks Date: Thu, 6 Jul 2017 20:46:38 -0400 Subject: [PATCH 06/15] change to property --- napalm_ios/ios.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/napalm_ios/ios.py b/napalm_ios/ios.py index 347f7eb..7ed7bac 100644 --- a/napalm_ios/ios.py +++ b/napalm_ios/ios.py @@ -136,7 +136,7 @@ def open(self): # ensure in enable mode self.device.enable() - def _autodetect(self): + def _discover_file_system(self): try: return self.device._autodetect_fs() except AttributeError: @@ -236,8 +236,7 @@ def load_replace_candidate(self, filename=None, config=None): Return None or raise exception """ self.config_replace = True - if not self.dest_file_system or self.dest_file_system is None: - self.dest_file_system = self._autodetect() + self.dest_file_system = self.file_system return_status, msg = self._load_candidate_wrapper(source_file=filename, source_config=config, dest_file=self.candidate_cfg, @@ -253,8 +252,7 @@ def load_merge_candidate(self, filename=None, config=None): """ self.config_replace = False - if not self.dest_file_system or self.dest_file_system is None: - self.dest_file_system = self._autodetect() + self.dest_file_system = self.file_system return_status, msg = self._load_candidate_wrapper(source_file=filename, source_config=config, dest_file=self.merge_cfg, @@ -2134,3 +2132,7 @@ def get_config(self, retrieve='all'): configs['running'] = output return configs + + @property + def file_system(self): + return self.dest_file_system or self._discover_file_system() From 0aa38ada6ac2b88c4b22f18b480212d47ff1cf80 Mon Sep 17 00:00:00 2001 From: itdependsnetworks Date: Thu, 6 Jul 2017 23:13:28 -0400 Subject: [PATCH 07/15] update property per recommendations --- napalm_ios/ios.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/napalm_ios/ios.py b/napalm_ios/ios.py index 7ed7bac..b0e65a8 100644 --- a/napalm_ios/ios.py +++ b/napalm_ios/ios.py @@ -83,7 +83,7 @@ def __init__(self, hostname, username, password, timeout=60, optional_args=None) 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) + self._dest_file_system = optional_args.get('dest_file_system', None) self.auto_rollback_on_error = optional_args.get('auto_rollback_on_error', True) # Netmiko possible arguments @@ -139,9 +139,9 @@ def open(self): def _discover_file_system(self): try: return self.device._autodetect_fs() - except AttributeError: - raise AttributeError("Netmiko _autodetect_fs not found please upgrade Netmiko or " - "specify dest_file_system in optional_args.") + except Exception: + raise Exception("Netmiko _autodetect_fs failed to workaround specify " + "dest_file_system in optional_args.") def close(self): """Close the connection to the device.""" @@ -236,7 +236,6 @@ def load_replace_candidate(self, filename=None, config=None): Return None or raise exception """ self.config_replace = True - self.dest_file_system = self.file_system return_status, msg = self._load_candidate_wrapper(source_file=filename, source_config=config, dest_file=self.candidate_cfg, @@ -251,8 +250,6 @@ def load_merge_candidate(self, filename=None, config=None): Merge configuration in: copy running-config """ self.config_replace = False - - self.dest_file_system = self.file_system return_status, msg = self._load_candidate_wrapper(source_file=filename, source_config=config, dest_file=self.merge_cfg, @@ -2134,5 +2131,7 @@ def get_config(self, retrieve='all'): return configs @property - def file_system(self): - return self.dest_file_system or self._discover_file_system() + def dest_file_system(self): + if self._dest_file_system is None: + self._dest_file_system = self._discover_file_system() + return self._dest_file_system From 453ca4ba0613908ad4cceb51729d05ae597359a2 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Fri, 7 Jul 2017 09:19:04 -0700 Subject: [PATCH 08/15] Fix exception handling on autodetect --- napalm_ios/ios.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/napalm_ios/ios.py b/napalm_ios/ios.py index c72ef66..6607440 100644 --- a/napalm_ios/ios.py +++ b/napalm_ios/ios.py @@ -27,7 +27,7 @@ from netmiko import __version__ as netmiko_version from napalm_base.base import NetworkDriver from napalm_base.exceptions import ReplaceConfigException, MergeConfigException, \ - ConnectionClosedException + ConnectionClosedException, CommandErrorException from napalm_base.utils import py23_compat import napalm_base.constants as C @@ -139,9 +139,10 @@ def open(self): def _discover_file_system(self): try: return self.device._autodetect_fs() - except Exception: - raise Exception("Netmiko _autodetect_fs failed to workaround specify " - "dest_file_system in optional_args.") + except Exception as e: + msg = "Netmiko _autodetect_fs failed (to workaround specify " \ + "dest_file_system in optional_args.)" + raise CommandErrorException(msg) def close(self): """Close the connection to the device.""" @@ -168,16 +169,15 @@ def is_alive(self): """Returns a flag with the state of the SSH connection.""" null = chr(0) try: - # Try sending ASCII null byte to maintain - # the connection alive - self.device.send_command(null) + if self.device is None: + return {'is_alive': False} + else: + # Try sending ASCII null byte to maintain the connection alive + self.device.send_command(null) except (socket.error, EOFError): - # If unable to send, we can tell for sure - # that the connection is unusable, - # hence return False. - return { - 'is_alive': False - } + # If unable to send, we can tell for sure that the connection is unusable, + # hence return False. + return {'is_alive': False} return { 'is_alive': self.device.remote_conn.transport.is_active() } @@ -2166,6 +2166,7 @@ def get_config(self, retrieve='all'): @property def dest_file_system(self): - if self._dest_file_system is None: + # The self.device check ensures napalm has an open connection + if self.device and self._dest_file_system is None: self._dest_file_system = self._discover_file_system() return self._dest_file_system From 38d527b7cbe4b438abfe2d8194db659fd72d255c Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Fri, 7 Jul 2017 09:26:11 -0700 Subject: [PATCH 09/15] Fixing linter issue --- napalm_ios/ios.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/napalm_ios/ios.py b/napalm_ios/ios.py index 6607440..33516d8 100644 --- a/napalm_ios/ios.py +++ b/napalm_ios/ios.py @@ -139,7 +139,7 @@ def open(self): def _discover_file_system(self): try: return self.device._autodetect_fs() - except Exception as e: + except Exception: msg = "Netmiko _autodetect_fs failed (to workaround specify " \ "dest_file_system in optional_args.)" raise CommandErrorException(msg) From 0ecf6ff3f0d5e8acc8f12ea1a6260e52a2f61ee1 Mon Sep 17 00:00:00 2001 From: Tarik K Date: Fri, 14 Jul 2017 17:03:32 -0700 Subject: [PATCH 10/15] adding output info when merge config failed with invalid input --- napalm_ios/ios.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/napalm_ios/ios.py b/napalm_ios/ios.py index 33516d8..0224fb2 100644 --- a/napalm_ios/ios.py +++ b/napalm_ios/ios.py @@ -412,7 +412,8 @@ def commit_config(self): self._enable_confirm() if 'Invalid input detected' in output: self.rollback() - merge_error = "Configuration merge failed; automatic rollback attempted" + err_header = "Configuration merge failed; automatic rollback attempted" + merge_error = "{0}:\n{1}".format(err_header, output) raise MergeConfigException(merge_error) # Save config to startup (both replace and merge) From 01d680a1fcd08d33e72c56b5ca31ad2a462b3bfa Mon Sep 17 00:00:00 2001 From: Guido-Jimenez Date: Mon, 7 Aug 2017 15:27:47 +0200 Subject: [PATCH 11/15] Fixing parsing of shutdown DHCP interfaces --- napalm_ios/ios.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/napalm_ios/ios.py b/napalm_ios/ios.py index 0224fb2..6cde548 100644 --- a/napalm_ios/ios.py +++ b/napalm_ios/ios.py @@ -1044,6 +1044,8 @@ def get_interfaces_ip(self): 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: {}} From d89e6286cb6075aa227af2cfbe5b0a94ac7092e6 Mon Sep 17 00:00:00 2001 From: Antoine Fourmy Date: Fri, 18 Aug 2017 18:47:27 +0200 Subject: [PATCH 12/15] save running conf to startup conf after rollback save running conf to startup conf after rollback --- napalm_ios/ios.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/napalm_ios/ios.py b/napalm_ios/ios.py index 0224fb2..a197224 100644 --- a/napalm_ios/ios.py +++ b/napalm_ios/ios.py @@ -437,6 +437,9 @@ def rollback(self): cmd = 'configure replace {} force'.format(cfg_file) self.device.send_command_expect(cmd) + # Save config to startup + self.device.send_command_expect("write mem") + def _inline_tcl_xfer(self, source_file=None, source_config=None, dest_file=None, file_system=None): """ From f1079cb8b449fc22cd367b0b8b67d634e3f3547e Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Fri, 1 Sep 2017 14:11:35 -0700 Subject: [PATCH 13/15] Telnet support --- napalm_ios/ios.py | 59 +++++++++++++++++++++++++++-------------------- requirements.txt | 4 ++-- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/napalm_ios/ios.py b/napalm_ios/ios.py index 69adff6..fa01246 100644 --- a/napalm_ios/ios.py +++ b/napalm_ios/ios.py @@ -21,10 +21,10 @@ import uuid import socket import tempfile +import telnetlib import copy 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, \ ConnectionClosedException, CommandErrorException @@ -83,6 +83,9 @@ def __init__(self, hostname, username, password, timeout=60, optional_args=None) 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) + if self.transport == 'telnet': + # Telnet only supports inline_transfer + self.inline_transfer = True # None will cause autodetection of dest_file_system self._dest_file_system = optional_args.get('dest_file_system', None) @@ -102,16 +105,9 @@ def __init__(self, hostname, username, password, timeout=60, optional_args=None) 'alt_host_keys': False, 'alt_key_file': '', 'ssh_config_file': None, + 'allow_agent': False, } - fields = netmiko_version.split('.') - fields = [int(x) for x in fields] - maj_ver, min_ver, bug_fix = fields - if maj_ver >= 2: - netmiko_argument_map['allow_agent'] = False - elif maj_ver == 1 and min_ver >= 1: - netmiko_argument_map['allow_agent'] = False - # Build dict of any optional Netmiko args self.netmiko_optional_args = {} for k, v in netmiko_argument_map.items(): @@ -119,8 +115,12 @@ def __init__(self, hostname, username, password, timeout=60, optional_args=None) 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', {'ssh': 22, 'telnet': 23}[self.transport]) + + default_port = { + 'ssh': 22, + 'telnet': 23 + } + self.port = optional_args.get('port', default_port[self.transport]) self.device = None self.config_replace = False @@ -131,8 +131,8 @@ def __init__(self, hostname, username, password, timeout=60, optional_args=None) def open(self): """Open a connection to the device.""" device_type = 'cisco_ios' - if self.transport != 'ssh': - device_type += '_' + self.transport + if self.transport == 'telnet': + device_type = 'cisco_ios_telnet' self.device = ConnectHandler(device_type=device_type, host=self.hostname, username=self.username, @@ -171,21 +171,30 @@ def _send_command(self, command): raise ConnectionClosedException(str(e)) def is_alive(self): - """Returns a flag with the state of the SSH connection.""" + """Returns a flag with the state of the connection.""" null = chr(0) - try: - if self.device is None: + if self.device is None: + return {'is_alive': False} + if self.transport == 'telnet': + try: + # Try sending IAC + NOP + self.device.write_channel(telnetlib.IAC + telnetlib.NOP) + return {'is_alive': True} + except UnicodeDecodeError: + # Netmiko logging bug (remove after Netmiko >= 1.4.3) + return {'is_alive': True} + except AttributeError: return {'is_alive': False} - else: + else: + # SSH + try: # Try sending ASCII null byte to maintain the connection alive - self.device.send_command(null) - except (socket.error, EOFError): - # If unable to send, we can tell for sure that the connection is unusable, - # hence return False. - return {'is_alive': False} - return { - 'is_alive': self.device.remote_conn.transport.is_active() - } + self.device.write_channel(null) + return {'is_alive': self.device.remote_conn.transport.is_active()} + except (socket.error, EOFError): + # If unable to send, we can tell for sure that the connection is unusable + return {'is_alive': False} + return {'is_alive': False} @staticmethod def _create_tmp_file(config): diff --git a/requirements.txt b/requirements.txt index a560cde..bcdb5aa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -napalm_base>=0.24.0 -netmiko>=1.4.1 +napalm_base>=0.25.0 +netmiko>=1.4.2 From 634948e3786cd30f336858b7f7fa8a11b089d189 Mon Sep 17 00:00:00 2001 From: Kirk Byers Date: Fri, 1 Sep 2017 15:12:30 -0700 Subject: [PATCH 14/15] Add a bit more comment on telnet IAC and NOP --- napalm_ios/ios.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/napalm_ios/ios.py b/napalm_ios/ios.py index fa01246..061acac 100644 --- a/napalm_ios/ios.py +++ b/napalm_ios/ios.py @@ -177,7 +177,8 @@ def is_alive(self): return {'is_alive': False} if self.transport == 'telnet': try: - # Try sending IAC + NOP + # Try sending IAC + NOP (IAC is telnet way of sending command + # IAC = Interpret as Command (it comes before the NOP) self.device.write_channel(telnetlib.IAC + telnetlib.NOP) return {'is_alive': True} except UnicodeDecodeError: From e3009efdc4d916114c36a6b0b345599295fee84f Mon Sep 17 00:00:00 2001 From: Mircea Ulinic Date: Mon, 4 Sep 2017 11:06:50 +0100 Subject: [PATCH 15/15] Version 0.8.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 031e32b..888b57f 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="napalm-ios", - version="0.7.0", + version="0.8.0", packages=find_packages(), author="Kirk Byers", author_email="ktbyers@twb-tech.com",