1# -*- coding: utf-8 -*-
2# Copyright 2015 Spotify AB. All rights reserved.
3#
4# The contents of this file are licensed under the Apache License, Version 2.0
5# (the "License"); you may not use this file except in compliance with the
6# License. You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations under
14# the License.
15
16# import stdlib
17from builtins import super
18import re
19import socket
20
21# import third party lib
22from netaddr import IPAddress, IPNetwork
23from netaddr.core import AddrFormatError
24
25# import NAPALM Base
26from napalm.base import helpers
27from napalm.base.exceptions import CommandErrorException, ReplaceConfigException
28from napalm.nxos import NXOSDriverBase
29
30# Easier to store these as constants
31HOUR_SECONDS = 3600
32DAY_SECONDS = 24 * HOUR_SECONDS
33WEEK_SECONDS = 7 * DAY_SECONDS
34YEAR_SECONDS = 365 * DAY_SECONDS
35
36# STD REGEX PATTERNS
37IP_ADDR_REGEX = r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"
38IPV4_ADDR_REGEX = IP_ADDR_REGEX
39IPV6_ADDR_REGEX_1 = r"::"
40IPV6_ADDR_REGEX_2 = r"[0-9a-fA-F:]{1,39}::[0-9a-fA-F:]{1,39}"
41IPV6_ADDR_REGEX_3 = (
42    r"[0-9a-fA-F]{1,3}:[0-9a-fA-F]{1,3}:[0-9a-fA-F]{1,3}:[0-9a-fA-F]{1,3}:"
43    r"[0-9a-fA-F]{1,3}:[0-9a-fA-F]{1,3}:[0-9a-fA-F]{1,3}:[0-9a-fA-F]{1,3}"
44)
45# Should validate IPv6 address using an IP address library after matching with this regex
46IPV6_ADDR_REGEX = r"(?:{}|{}|{})".format(
47    IPV6_ADDR_REGEX_1, IPV6_ADDR_REGEX_2, IPV6_ADDR_REGEX_3
48)
49IPV4_OR_IPV6_REGEX = r"(?:{}|{})".format(IPV4_ADDR_REGEX, IPV6_ADDR_REGEX)
50
51MAC_REGEX = r"[a-fA-F0-9]{4}\.[a-fA-F0-9]{4}\.[a-fA-F0-9]{4}"
52VLAN_REGEX = r"\d{1,4}"
53
54RE_IPADDR = re.compile(r"{}".format(IP_ADDR_REGEX))
55RE_MAC = re.compile(r"{}".format(MAC_REGEX))
56
57# Period needed for 32-bit AS Numbers
58ASN_REGEX = r"[\d\.]+"
59
60RE_IP_ROUTE_VIA_REGEX = re.compile(
61    r"    (?P<used>[\*| ])via ((?P<ip>" + IPV4_ADDR_REGEX + r")"
62    r"(%(?P<vrf>\S+))?, )?"
63    r"((?P<int>[\w./:]+), )?\[(\d+)/(?P<metric>\d+)\]"
64    r", (?P<age>[\d\w:]+), (?P<source>[\w]+)(-(?P<procnr>\d+))?"
65    r"(?P<rest>.*)"
66)
67RE_RT_VRF_NAME = re.compile(r"VRF \"(\S+)\"")
68RE_RT_IPV4_ROUTE_PREF = re.compile(r"(" + IPV4_ADDR_REGEX + r"/\d{1,2}), ubest.*")
69
70RE_BGP_PROTO_TAG = re.compile(r"BGP Protocol Tag\s+: (\d+)")
71RE_BGP_REMOTE_AS = re.compile(r"remote AS (" + ASN_REGEX + r")")
72RE_BGP_COMMUN = re.compile(r"[ ]{10}([\S ]+)")
73
74
75def parse_intf_section(interface):
76    """Parse a single entry from show interfaces output.
77
78    Different cases:
79    mgmt0 is up
80    admin state is up
81
82    Ethernet2/1 is up
83    admin state is up, Dedicated Interface
84
85    Vlan1 is down (Administratively down), line protocol is down, autostate enabled
86
87    Ethernet154/1/48 is up (with no 'admin state')
88    """
89    interface = interface.strip()
90    re_protocol = (
91        r"^(?P<intf_name>\S+?)\s+is\s+(?P<status>.+?)"
92        r",\s+line\s+protocol\s+is\s+(?P<protocol>\S+).*$"
93    )
94    re_intf_name_state = r"^(?P<intf_name>\S+) is (?P<intf_state>\S+).*"
95    re_is_enabled_1 = r"^admin state is (?P<is_enabled>\S+)$"
96    re_is_enabled_2 = r"^admin state is (?P<is_enabled>\S+), "
97    re_is_enabled_3 = r"^.* is down.*Administratively down.*$"
98    re_mac = r"^\s+Hardware:\s+(?P<hardware>.*),\s+address:\s+(?P<mac_address>\S+) "
99    re_speed = (
100        r"\s+MTU (?P<mtu>\S+)\s+bytes,\s+BW\s+(?P<speed>\S+)\s+(?P<speed_unit>\S+).*$"
101    )
102    re_mtu_nve = r"\s+MTU (?P<mtu_nve>\S+)\s+bytes.*$"
103    re_description_1 = r"^\s+Description:\s+(?P<description>.*)  (?:MTU|Internet)"
104    re_description_2 = r"^\s+Description:\s+(?P<description>.*)$"
105    re_hardware = r"^.* Hardware: (?P<hardware>\S+)$"
106
107    # Check for 'protocol is ' lines
108    match = re.search(re_protocol, interface, flags=re.M)
109    if match:
110        intf_name = match.group("intf_name")
111        status = match.group("status")
112        protocol = match.group("protocol")
113
114        if "admin" in status.lower():
115            is_enabled = False
116        else:
117            is_enabled = True
118        is_up = bool("up" in protocol)
119
120    else:
121        # More standard is up, next line admin state is lines
122        match = re.search(re_intf_name_state, interface)
123        intf_name = helpers.canonical_interface_name(match.group("intf_name"))
124        intf_state = match.group("intf_state").strip()
125        is_up = True if intf_state == "up" else False
126
127        admin_state_present = re.search("admin state is", interface)
128        if admin_state_present:
129            # Parse cases where 'admin state' string exists
130            for x_pattern in [re_is_enabled_1, re_is_enabled_2]:
131                match = re.search(x_pattern, interface, flags=re.M)
132                if match:
133                    is_enabled = match.group("is_enabled").strip()
134                    is_enabled = True if re.search("up", is_enabled) else False
135                    break
136            else:
137                msg = "Error parsing intf, 'admin state' never detected:\n\n{}".format(
138                    interface
139                )
140                raise ValueError(msg)
141        else:
142            # No 'admin state' should be 'is up' or 'is down' strings
143            # If interface is up; it is enabled
144            is_enabled = True
145            if not is_up:
146                match = re.search(re_is_enabled_3, interface, flags=re.M)
147                if match:
148                    is_enabled = False
149
150    match = re.search(re_mac, interface, flags=re.M)
151    if match:
152        mac_address = match.group("mac_address")
153        mac_address = helpers.mac(mac_address)
154    else:
155        mac_address = ""
156
157    match = re.search(re_hardware, interface, flags=re.M)
158    speed_exist = True
159    if match:
160        if match.group("hardware") == "NVE":
161            match = re.search(re_mtu_nve, interface, flags=re.M)
162            mtu = int(match.group("mtu_nve"))
163            speed_exist = False
164
165    if speed_exist:
166        match = re.search(re_speed, interface, flags=re.M)
167        speed = int(match.group("speed"))
168        mtu = int(match.group("mtu"))
169        speed_unit = match.group("speed_unit")
170        speed_unit = speed_unit.rstrip(",")
171        # This was alway in Kbit (in the data I saw)
172        if speed_unit != "Kbit":
173            msg = "Unexpected speed unit in show interfaces parsing:\n\n{}".format(
174                interface
175            )
176            raise ValueError(msg)
177        speed = int(round(speed / 1000.0))
178    else:
179        speed = -1
180
181    description = ""
182    for x_pattern in [re_description_1, re_description_2]:
183        match = re.search(x_pattern, interface, flags=re.M)
184        if match:
185            description = match.group("description")
186            break
187
188    return {
189        intf_name: {
190            "description": description,
191            "is_enabled": is_enabled,
192            "is_up": is_up,
193            "last_flapped": -1.0,
194            "mac_address": mac_address,
195            "mtu": mtu,
196            "speed": speed,
197        }
198    }
199
200
201def convert_hhmmss(hhmmss):
202    """Convert hh:mm:ss to seconds."""
203    fields = hhmmss.split(":")
204    if len(fields) != 3:
205        raise ValueError("Received invalid HH:MM:SS data: {}".format(hhmmss))
206    fields = [int(x) for x in fields]
207    hours, minutes, seconds = fields
208    return (hours * 3600) + (minutes * 60) + seconds
209
210
211def bgp_time_conversion(bgp_uptime):
212    """Convert string time to seconds.
213
214    Examples
215    00:14:23
216    00:13:40
217    00:00:21
218    00:00:13
219    00:00:49
220    1d11h
221    1d17h
222    1w0d
223    8w5d
224    1y28w
225    never
226    """
227    bgp_uptime = bgp_uptime.strip()
228    uptime_letters = set(["w", "h", "d"])
229
230    if "never" in bgp_uptime:
231        return -1
232    elif ":" in bgp_uptime:
233        times = bgp_uptime.split(":")
234        times = [int(x) for x in times]
235        hours, minutes, seconds = times
236        return (hours * 3600) + (minutes * 60) + seconds
237    # Check if any letters 'w', 'h', 'd' are in the time string
238    elif uptime_letters & set(bgp_uptime):
239        form1 = r"(\d+)d(\d+)h"  # 1d17h
240        form2 = r"(\d+)w(\d+)d"  # 8w5d
241        form3 = r"(\d+)y(\d+)w"  # 1y28w
242        match = re.search(form1, bgp_uptime)
243        if match:
244            days = int(match.group(1))
245            hours = int(match.group(2))
246            return (days * DAY_SECONDS) + (hours * 3600)
247        match = re.search(form2, bgp_uptime)
248        if match:
249            weeks = int(match.group(1))
250            days = int(match.group(2))
251            return (weeks * WEEK_SECONDS) + (days * DAY_SECONDS)
252        match = re.search(form3, bgp_uptime)
253        if match:
254            years = int(match.group(1))
255            weeks = int(match.group(2))
256            return (years * YEAR_SECONDS) + (weeks * WEEK_SECONDS)
257    raise ValueError("Unexpected value for BGP uptime string: {}".format(bgp_uptime))
258
259
260def bgp_normalize_table_data(bgp_table):
261    """The 'show bgp all summary vrf all' table can have entries that wrap multiple lines.
262
263    2001:db8:4:701::2
264                4 65535  163664  163693      145    0    0     3w2d 3
265    2001:db8:e0:dd::1
266                4    10  327491  327278      145    0    0     3w1d 4
267
268    Normalize this so the line wrap doesn't exit.
269    """
270    bgp_table = bgp_table.strip()
271    bgp_multiline_pattern = r"({})\s*\n".format(IPV4_OR_IPV6_REGEX)
272    # Strip out the newline
273    return re.sub(bgp_multiline_pattern, r"\1", bgp_table)
274
275
276def bgp_table_parser(bgp_table):
277    """Generator that parses a line of bgp summary table and returns a dict compatible with NAPALM
278
279    Example line:
280    10.2.1.14       4    10  472516  472238      361    0    0     3w1d 9
281    """
282    bgp_table = bgp_table.strip()
283    for bgp_entry in bgp_table.splitlines():
284        bgp_table_fields = bgp_entry.split()
285
286        try:
287            if re.search(r"Shut.*Admin", bgp_entry):
288                (
289                    peer_ip,
290                    bgp_version,
291                    remote_as,
292                    msg_rcvd,
293                    msg_sent,
294                    _,
295                    _,
296                    _,
297                    uptime,
298                    state_1,
299                    state_2,
300                ) = bgp_table_fields
301                state_pfxrcd = "{} {}".format(state_1, state_2)
302            else:
303                (
304                    peer_ip,
305                    bgp_version,
306                    remote_as,
307                    msg_rcvd,
308                    msg_sent,
309                    _,
310                    _,
311                    _,
312                    uptime,
313                    state_pfxrcd,
314                ) = bgp_table_fields
315        except ValueError:
316            raise ValueError(
317                "Unexpected entry ({}) in BGP summary table".format(bgp_table_fields)
318            )
319
320        is_enabled = True
321        try:
322            received_prefixes = int(state_pfxrcd)
323            is_up = True
324        except ValueError:
325            received_prefixes = -1
326            is_up = False
327            if re.search(r"Shut.*Admin", state_pfxrcd):
328                is_enabled = False
329
330        if not is_up:
331            uptime = -1
332        if uptime != -1:
333            uptime = bgp_time_conversion(uptime)
334
335        yield {
336            peer_ip: {
337                "is_enabled": is_enabled,
338                "uptime": uptime,
339                "remote_as": helpers.as_number(remote_as),
340                "is_up": is_up,
341                "description": "",
342                "received_prefixes": received_prefixes,
343            }
344        }
345
346
347def bgp_summary_parser(bgp_summary):
348    """Parse 'show bgp all summary vrf' output information from NX-OS devices."""
349
350    bgp_summary_dict = {}
351    # Check for BGP summary information lines that have no data
352    if len(bgp_summary.strip().splitlines()) <= 1:
353        return {}
354
355    allowed_afi = ["ipv4", "ipv6", "l2vpn"]
356    vrf_regex = r"^BGP summary information for VRF\s+(?P<vrf>\S+),"
357    afi_regex = (
358        r"^BGP summary information.*address family (?P<afi>\S+ (?:Unicast|EVPN))"
359    )
360    local_router_regex = (
361        r"^BGP router identifier\s+(?P<router_id>\S+)"
362        r",\s+local AS number\s+(?P<local_as>\S+)"
363    )
364
365    for pattern in [vrf_regex, afi_regex, local_router_regex]:
366        match = re.search(pattern, bgp_summary, flags=re.M)
367        if match:
368            bgp_summary_dict.update(match.groupdict(1))
369
370    # Some post regex cleanup and validation
371    vrf = bgp_summary_dict["vrf"]
372    if vrf.lower() == "default":
373        bgp_summary_dict["vrf"] = "global"
374
375    afi = bgp_summary_dict["afi"]
376    afi = afi.split()[0].lower()
377    if afi not in allowed_afi:
378        raise ValueError("AFI ({}) is invalid and not supported.".format(afi))
379    bgp_summary_dict["afi"] = afi
380
381    local_as = bgp_summary_dict["local_as"]
382    local_as = helpers.as_number(local_as)
383
384    match = re.search(IPV4_ADDR_REGEX, bgp_summary_dict["router_id"])
385    if not match:
386        raise ValueError(
387            "BGP router_id ({}) is not valid".format(bgp_summary_dict["router_id"])
388        )
389
390    vrf = bgp_summary_dict["vrf"]
391    bgp_return_dict = {vrf: {"router_id": bgp_summary_dict["router_id"], "peers": {}}}
392
393    # Extract and process the tabular data
394    tabular_divider = r"^Neighbor\s+.*PfxRcd$"
395    tabular_data = re.split(tabular_divider, bgp_summary, flags=re.M)
396    if len(tabular_data) != 2:
397        msg = "Unexpected data processing BGP summary information:\n\n{}".format(
398            bgp_summary
399        )
400        raise ValueError(msg)
401    tabular_data = tabular_data[1]
402    bgp_table = bgp_normalize_table_data(tabular_data)
403    for bgp_entry in bgp_table_parser(bgp_table):
404        bgp_return_dict[vrf]["peers"].update(bgp_entry)
405
406    bgp_new_dict = {}
407    for neighbor, bgp_data in bgp_return_dict[vrf]["peers"].items():
408        received_prefixes = bgp_data.pop("received_prefixes")
409        bgp_data["address_family"] = {}
410        prefixes_dict = {
411            "sent_prefixes": -1,
412            "accepted_prefixes": -1,
413            "received_prefixes": received_prefixes,
414        }
415        bgp_data["address_family"][afi] = prefixes_dict
416        bgp_data["local_as"] = local_as
417        # FIX, hard-coding
418        bgp_data["remote_id"] = "0.0.0.0"
419        bgp_new_dict[neighbor] = bgp_data
420
421    bgp_return_dict[vrf]["peers"] = bgp_new_dict
422
423    return bgp_return_dict
424
425
426class NXOSSSHDriver(NXOSDriverBase):
427    def __init__(self, hostname, username, password, timeout=60, optional_args=None):
428        super().__init__(
429            hostname, username, password, timeout=timeout, optional_args=optional_args
430        )
431        self.platform = "nxos_ssh"
432        self.connector_type_map = {
433            "1000base-LH": "LC_CONNECTOR",
434            "1000base-SX": "LC_CONNECTOR",
435            "1000base-T": "Unknown",
436            "10Gbase-LR": "LC_CONNECTOR",
437            "10Gbase-SR": "LC_CONNECTOR",
438            "SFP-H10GB-CU1M": "DAC_CONNECTOR",
439            "SFP-H10GB-CU1.45M": "DAC_CONNECTOR",
440            "SFP-H10GB-CU3M": "DAC_CONNECTOR",
441            "SFP-H10GB-CU3.45M": "DAC_CONNECTOR",
442        }
443
444    def open(self):
445        self.device = self._netmiko_open(
446            device_type="cisco_nxos", netmiko_optional_args=self.netmiko_optional_args
447        )
448
449    def close(self):
450        self._netmiko_close()
451
452    def _send_command(self, command, raw_text=False, cmd_verify=True):
453        """
454        Wrapper for Netmiko's send_command method.
455
456        raw_text argument is not used and is for code sharing with NX-API.
457        """
458        return self.device.send_command(command, cmd_verify=cmd_verify)
459
460    def _send_command_list(self, commands, expect_string=None):
461        """Wrapper for Netmiko's send_command method (for list of commands."""
462        output = ""
463        for command in commands:
464            output += self.device.send_command(
465                command,
466                strip_prompt=False,
467                strip_command=False,
468                expect_string=expect_string,
469            )
470        return output
471
472    def _send_config(self, commands):
473        if isinstance(commands, str):
474            commands = (command for command in commands.splitlines() if command)
475        return self.device.send_config_set(commands)
476
477    @staticmethod
478    def parse_uptime(uptime_str):
479        """
480        Extract the uptime string from the given Cisco IOS Device.
481        Return the uptime in seconds as an integer
482        """
483        # Initialize to zero
484        (years, weeks, days, hours, minutes) = (0, 0, 0, 0, 0)
485
486        uptime_str = uptime_str.strip()
487        time_list = uptime_str.split(",")
488        for element in time_list:
489            if re.search("year", element):
490                years = int(element.split()[0])
491            elif re.search("week", element):
492                weeks = int(element.split()[0])
493            elif re.search("day", element):
494                days = int(element.split()[0])
495            elif re.search("hour", element):
496                hours = int(element.split()[0])
497            elif re.search("minute", element):
498                minutes = int(element.split()[0])
499            elif re.search("second", element):
500                seconds = int(element.split()[0])
501
502        uptime_sec = (
503            (years * YEAR_SECONDS)
504            + (weeks * WEEK_SECONDS)
505            + (days * DAY_SECONDS)
506            + (hours * 3600)
507            + (minutes * 60)
508            + seconds
509        )
510        return uptime_sec
511
512    def is_alive(self):
513        """Returns a flag with the state of the SSH connection."""
514        null = chr(0)
515        try:
516            if self.device is None:
517                return {"is_alive": False}
518            else:
519                # Try sending ASCII null byte to maintain the connection alive
520                self._send_command(null, cmd_verify=False)
521        except (socket.error, EOFError):
522            # If unable to send, we can tell for sure that the connection is unusable,
523            # hence return False.
524            return {"is_alive": False}
525        return {"is_alive": self.device.remote_conn.transport.is_active()}
526
527    def _copy_run_start(self):
528
529        output = self.device.save_config()
530        if "complete" in output.lower():
531            return True
532        else:
533            msg = "Unable to save running-config to startup-config!"
534            raise CommandErrorException(msg)
535
536    def _load_cfg_from_checkpoint(self):
537
538        commands = [
539            "terminal dont-ask",
540            "rollback running-config file {}".format(self.candidate_cfg),
541            "no terminal dont-ask",
542        ]
543
544        try:
545            rollback_result = self._send_command_list(commands, expect_string=r"[#>]")
546        finally:
547            self.changed = True
548        msg = rollback_result
549        if "Rollback failed." in msg:
550            raise ReplaceConfigException(msg)
551
552    def rollback(self):
553        if self.changed:
554            commands = [
555                "terminal dont-ask",
556                "rollback running-config file {}".format(self.rollback_cfg),
557                "no terminal dont-ask",
558            ]
559            result = self._send_command_list(commands, expect_string=r"[#>]")
560            if "completed" not in result.lower():
561                raise ReplaceConfigException(result)
562            # If hostname changes ensure Netmiko state is updated properly
563            self._netmiko_device.set_base_prompt()
564            self._copy_run_start()
565            self.changed = False
566
567    def _apply_key_map(self, key_map, table):
568        new_dict = {}
569        for key, value in table.items():
570            new_key = key_map.get(key)
571            if new_key:
572                new_dict[new_key] = str(value)
573        return new_dict
574
575    def _convert_uptime_to_seconds(self, uptime_facts):
576        seconds = int(uptime_facts["up_days"]) * 24 * 60 * 60
577        seconds += int(uptime_facts["up_hours"]) * 60 * 60
578        seconds += int(uptime_facts["up_mins"]) * 60
579        seconds += int(uptime_facts["up_secs"])
580        return seconds
581
582    def get_facts(self):
583        """Return a set of facts from the devices."""
584        # default values.
585        vendor = "Cisco"
586        uptime = -1
587        serial_number, fqdn, os_version, hostname, domain_name, model = ("",) * 6
588
589        # obtain output from device
590        show_ver = self._send_command("show version")
591        show_hosts = self._send_command("show hosts")
592        show_int_status = self._send_command("show interface status")
593        show_hostname = self._send_command("show hostname")
594
595        try:
596            show_inventory_table = self._get_command_table(
597                "show inventory | json", "TABLE_inv", "ROW_inv"
598            )
599            if isinstance(show_inventory_table, dict):
600                show_inventory_table = [show_inventory_table]
601
602            for row in show_inventory_table:
603                if row["name"] == '"Chassis"' or row["name"] == "Chassis":
604                    serial_number = row.get("serialnum", "")
605                    break
606        except ValueError:
607            show_inventory = self._send_command("show inventory")
608            find_regexp = r"^NAME:\s+\"(.*)\",.*\n^PID:.*SN:\s+(\w*)"
609            find = re.findall(find_regexp, show_inventory, re.MULTILINE)
610            for row in find:
611                if row[0] == "Chassis":
612                    serial_number = row[1]
613                    break
614
615        # uptime/serial_number/IOS version
616        for line in show_ver.splitlines():
617            if " uptime is " in line:
618                _, uptime_str = line.split(" uptime is ")
619                uptime = self.parse_uptime(uptime_str)
620
621            if "system: " in line or "NXOS: " in line:
622                line = line.strip()
623                os_version = line.split()[2]
624                os_version = os_version.strip()
625
626            if "cisco" in line and "hassis" in line:
627                match = re.search(r".cisco (.*) \(", line)
628                if match:
629                    model = match.group(1).strip()
630                match = re.search(r".cisco (.* [cC]hassis)", line)
631                if match:
632                    model = match.group(1).strip()
633
634        hostname = show_hostname.strip()
635
636        # Determine domain_name and fqdn
637        for line in show_hosts.splitlines():
638            if "Default domain" in line:
639                _, domain_name = re.split(r".*Default domain.*is ", line)
640                domain_name = domain_name.strip()
641                break
642        if hostname.count(".") >= 2:
643            fqdn = hostname
644            # Remove domain name from hostname
645            if domain_name:
646                hostname = re.sub(re.escape(domain_name) + "$", "", hostname)
647                hostname = hostname.strip(".")
648        elif domain_name:
649            fqdn = "{}.{}".format(hostname, domain_name)
650
651        # interface_list filter
652        interface_list = []
653        show_int_status = show_int_status.strip()
654        # Remove the header information
655        show_int_status = re.sub(
656            r"(?:^---------+$|^Port .*$|^ .*$)", "", show_int_status, flags=re.M
657        )
658        for line in show_int_status.splitlines():
659            if not line:
660                continue
661            interface = line.split()[0]
662            # Return canonical interface name
663            interface_list.append(helpers.canonical_interface_name(interface))
664
665        return {
666            "uptime": int(uptime),
667            "vendor": vendor,
668            "os_version": str(os_version),
669            "serial_number": str(serial_number),
670            "model": str(model),
671            "hostname": str(hostname),
672            "fqdn": fqdn,
673            "interface_list": interface_list,
674        }
675
676    def get_interfaces(self):
677        """
678        Get interface details.
679
680        last_flapped is not implemented
681
682        Example Output:
683
684        {   u'Vlan1': {   'description': u'',
685                      'is_enabled': True,
686                      'is_up': True,
687                      'last_flapped': -1.0,
688                      'mac_address': u'a493.4cc1.67a7',
689                      'speed': 100},
690        u'Vlan100': {   'description': u'Data Network',
691                        'is_enabled': True,
692                        'is_up': True,
693                        'last_flapped': -1.0,
694                        'mac_address': u'a493.4cc1.67a7',
695                        'speed': 100},
696        u'Vlan200': {   'description': u'Voice Network',
697                        'is_enabled': True,
698                        'is_up': True,
699                        'last_flapped': -1.0,
700                        'mac_address': u'a493.4cc1.67a7',
701                        'speed': 100}}
702        """
703        interfaces = {}
704        command = "show interface"
705        output = self._send_command(command)
706        if not output:
707            return {}
708
709        # Break output into per-interface sections (note, separator text is retained)
710        separator1 = r"^\S+\s+is \S+.*\nadmin state is.*$"
711        separator2 = r"^.* is .*, line protocol is .*$"
712        separator3 = r"^.* is (?:down|up).*$"
713        separators = r"({}|{}|{})".format(separator1, separator2, separator3)
714        interface_lines = re.split(separators, output, flags=re.M)
715
716        if len(interface_lines) == 1:
717            msg = "Unexpected output data in '{}':\n\n{}".format(
718                command, interface_lines
719            )
720            raise ValueError(msg)
721
722        # Get rid of the blank data at the beginning
723        interface_lines.pop(0)
724
725        # Must be pairs of data (the separator and section corresponding to it)
726        if len(interface_lines) % 2 != 0:
727            msg = "Unexpected output data in '{}':\n\n{}".format(
728                command, interface_lines
729            )
730            raise ValueError(msg)
731
732        # Combine the separator and section into one string
733        intf_iter = iter(interface_lines)
734        try:
735            new_interfaces = [line + next(intf_iter, "") for line in intf_iter]
736        except TypeError:
737            raise ValueError()
738
739        for entry in new_interfaces:
740            interfaces.update(parse_intf_section(entry))
741
742        return interfaces
743
744    def get_bgp_neighbors(self):
745        """BGP neighbor information.
746
747        Supports VRFs and IPv4 and IPv6 AFIs
748
749        {
750        "global": {
751            "router_id": "1.1.1.103",
752            "peers": {
753                "10.99.99.2": {
754                    "is_enabled": true,
755                    "uptime": -1,
756                    "remote_as": 22,
757                    "address_family": {
758                        "ipv4": {
759                            "sent_prefixes": -1,
760                            "accepted_prefixes": -1,
761                            "received_prefixes": -1
762                        }
763                    },
764                    "remote_id": "0.0.0.0",
765                    "local_as": 22,
766                    "is_up": false,
767                    "description": ""
768                 }
769            }
770        }
771        """
772        bgp_dict = {}
773
774        # get summary output from device
775        cmd_bgp_all_sum = "show bgp all summary vrf all"
776        bgp_summary_output = self._send_command(cmd_bgp_all_sum).strip()
777
778        section_separator = r"BGP summary information for "
779        bgp_summary_sections = re.split(section_separator, bgp_summary_output)
780        if len(bgp_summary_sections):
781            bgp_summary_sections.pop(0)
782
783        for bgp_section in bgp_summary_sections:
784            bgp_section = section_separator + bgp_section
785            bgp_dict.update(bgp_summary_parser(bgp_section))
786
787        # FIX -- look up logical or behavior we did in Cisco IOS bgp parser (make consistent here)
788        # FIX -- need to merge IPv6 and IPv4 AFI for same neighbor
789        return bgp_dict
790
791    def cli(self, commands):
792        cli_output = {}
793        if type(commands) is not list:
794            raise TypeError("Please enter a valid list of commands!")
795
796        for command in commands:
797            output = self._send_command(command)
798            cli_output[str(command)] = output
799        return cli_output
800
801    def get_environment(self):
802        """
803        Get environment facts.
804
805        power and fan are currently not implemented
806        cpu is using 1-minute average
807        """
808
809        environment = {}
810        # sys_resources contains cpu and mem output
811        sys_resources = self._send_command("show system resources")
812        temp_cmd = "show environment temperature"
813
814        # cpu
815        environment.setdefault("cpu", {})
816        environment["cpu"]["0"] = {}
817        environment["cpu"]["0"]["%usage"] = -1.0
818        system_resources_cpu = helpers.textfsm_extractor(
819            self, "system_resources", sys_resources
820        )
821        for cpu in system_resources_cpu:
822            cpu_dict = {
823                cpu.get("cpu_id"): {
824                    "%usage": round(100 - float(cpu.get("cpu_idle")), 2)
825                }
826            }
827            environment["cpu"].update(cpu_dict)
828
829        # memory
830        environment.setdefault("memory", {})
831        for line in sys_resources.splitlines():
832            # Memory usage:   16401224K total,   4798280K used,   11602944K free
833            if "Memory usage:" in line:
834                proc_total_mem, proc_used_mem, _ = line.split(",")
835                proc_used_mem = re.search(r"\d+", proc_used_mem).group(0)
836                proc_total_mem = re.search(r"\d+", proc_total_mem).group(0)
837                break
838        else:
839            raise ValueError("Unexpected output from: {}".format(line))
840        environment["memory"]["used_ram"] = int(proc_used_mem)
841        environment["memory"]["available_ram"] = int(proc_total_mem)
842
843        # temperature
844        output = self._send_command(temp_cmd)
845        environment.setdefault("temperature", {})
846        for line in output.splitlines():
847            # Module   Sensor        MajorThresh   MinorThres   CurTemp     Status
848            # 1        Intake          70              42          28         Ok
849            if re.match(r"^[0-9]", line):
850                module, sensor, is_critical, is_alert, temp, _ = line.split()
851                is_critical = float(is_critical)
852                is_alert = float(is_alert)
853                temp = float(temp)
854                env_value = {
855                    "is_alert": temp >= is_alert,
856                    "is_critical": temp >= is_critical,
857                    "temperature": temp,
858                }
859                location = "{0}-{1}".format(sensor, module)
860                environment["temperature"][location] = env_value
861
862        # Initialize 'power' and 'fan' to default values (not implemented)
863        environment.setdefault("power", {})
864        environment["power"]["invalid"] = {
865            "status": True,
866            "output": -1.0,
867            "capacity": -1.0,
868        }
869        environment.setdefault("fans", {})
870        environment["fans"]["invalid"] = {"status": True}
871
872        return environment
873
874    def get_arp_table(self, vrf=""):
875        """
876        Get arp table information.
877
878        Return a list of dictionaries having the following set of keys:
879            * interface (string)
880            * mac (string)
881            * ip (string)
882            * age (float)
883
884        For example::
885            [
886                {
887                    'interface' : 'MgmtEth0/RSP0/CPU0/0',
888                    'mac'       : '5c:5e:ab:da:3c:f0',
889                    'ip'        : '172.17.17.1',
890                    'age'       : 12.0
891                },
892                {
893                    'interface': 'MgmtEth0/RSP0/CPU0/0',
894                    'mac'       : '66:0e:94:96:e0:ff',
895                    'ip'        : '172.17.17.2',
896                    'age'       : 14.0
897                }
898            ]
899        """
900        arp_table = []
901
902        command = "show ip arp vrf {} | exc INCOMPLETE".format(vrf or "all")
903        output = self._send_command(command)
904
905        separator = r"^Address\s+Age.*Interface.*$"
906        arp_list = re.split(separator, output, flags=re.M)
907        if len(arp_list) != 2:
908            raise ValueError("Error processing arp table output:\n\n{}".format(output))
909
910        arp_entries = arp_list[1].strip()
911        for line in arp_entries.splitlines():
912            if len(line.split()) >= 4:
913                # Search for extra characters to strip, currently strip '*', '+', '#', 'D'
914                line = re.sub(r"\s+[\*\+\#D]{1,4}\s*$", "", line, flags=re.M)
915                address, age, mac, interface = line.split()
916            else:
917                raise ValueError("Unexpected output from: {}".format(line.split()))
918
919            if age == "-":
920                age = -1.0
921            elif ":" not in age:
922                # Cisco sometimes returns a sub second arp time 0.411797
923                try:
924                    age = float(age)
925                except ValueError:
926                    age = -1.0
927            else:
928                age = convert_hhmmss(age)
929                age = float(age)
930            age = round(age, 1)
931
932            # Validate we matched correctly
933            if not re.search(RE_IPADDR, address):
934                raise ValueError("Invalid IP Address detected: {}".format(address))
935            if not re.search(RE_MAC, mac):
936                raise ValueError("Invalid MAC Address detected: {}".format(mac))
937            entry = {
938                "interface": interface,
939                "mac": helpers.mac(mac),
940                "ip": address,
941                "age": age,
942            }
943            arp_table.append(entry)
944        return arp_table
945
946    def _get_ntp_entity(self, peer_type):
947        ntp_entities = {}
948        command = "show ntp peers"
949        output = self._send_command(command)
950
951        for line in output.splitlines():
952            # Skip first two lines and last line of command output
953            if line == "" or "-----" in line or "Peer IP Address" in line:
954                continue
955            elif IPAddress(len(line.split()[0])).is_unicast:
956                peer_addr = line.split()[0]
957                ntp_entities[peer_addr] = {}
958            else:
959                raise ValueError("Did not correctly find a Peer IP Address")
960
961        return ntp_entities
962
963    def get_ntp_peers(self):
964        return self._get_ntp_entity("Peer")
965
966    def get_ntp_servers(self):
967        return self._get_ntp_entity("Server")
968
969    def get_interfaces_ip(self):
970        """
971        Get interface IP details. Returns a dictionary of dictionaries.
972
973        Sample output:
974        {
975            "Ethernet2/3": {
976                "ipv4": {
977                    "4.4.4.4": {
978                        "prefix_length": 16
979                    }
980                },
981                "ipv6": {
982                    "2001:db8::1": {
983                        "prefix_length": 10
984                    },
985                    "fe80::2ec2:60ff:fe4f:feb2": {
986                        "prefix_length": "128"
987                    }
988                }
989            },
990            "Ethernet2/2": {
991                "ipv4": {
992                    "2.2.2.2": {
993                        "prefix_length": 27
994                    }
995                }
996            }
997        }
998        """
999        interfaces_ip = {}
1000        ipv4_command = "show ip interface vrf all"
1001        ipv6_command = "show ipv6 interface vrf all"
1002        output_v4 = self._send_command(ipv4_command)
1003        output_v6 = self._send_command(ipv6_command)
1004
1005        v4_interfaces = {}
1006        for line in output_v4.splitlines():
1007            # Ethernet2/2, Interface status: protocol-up/link-up/admin-up, iod: 38,
1008            # IP address: 2.2.2.2, IP subnet: 2.2.2.0/27 route-preference: 0, tag: 0
1009            # IP address: 3.3.3.3, IP subnet: 3.3.3.0/25 secondary route-preference: 0, tag: 0
1010            if "Interface status" in line:
1011                interface = line.split(",")[0]
1012                continue
1013            if "IP address" in line:
1014                ip_address = line.split(",")[0].split()[2]
1015                try:
1016                    prefix_len = int(line.split()[5].split("/")[1])
1017                except (ValueError, IndexError):
1018                    prefix_len = "N/A"
1019
1020                if ip_address == "none":
1021                    v4_interfaces.setdefault(interface, {})
1022                else:
1023                    val = {"prefix_length": prefix_len}
1024                    v4_interfaces.setdefault(interface, {})[ip_address] = val
1025
1026        v6_interfaces = {}
1027        for line in output_v6.splitlines():
1028            # Ethernet2/4, Interface status: protocol-up/link-up/admin-up, iod: 40
1029            # IPv6 address:
1030            #   2001:11:2233::a1/24 [VALID]
1031            #   2001:cc11:22bb:0:2ec2:60ff:fe4f:feb2/64 [VALID]
1032            # IPv6 subnet:  2001::/24
1033            # IPv6 link-local address: fe80::2ec2:60ff:fe4f:feb2 (default) [VALID]
1034            # IPv6 address: fe80::a293:51ff:fe5f:5ce9 [VALID]
1035            if "Interface status" in line:
1036                interface = line.split(",")[0]
1037                continue
1038            if "VALID" in line:
1039                line = line.strip()
1040                if "link-local address" in line:
1041                    # match the following format:
1042                    # IPv6 link-local address: fe80::2ec2:60ff:fe4f:feb2 (default) [VALID]
1043                    ip_address = line.split()[3]
1044                    prefix_len = "64"
1045                elif "IPv6 address" in line:
1046                    # match the following format:
1047                    # IPv6 address: fe80::a293:51ff:fe5f:5ce9 [VALID]
1048                    ip_address = line.split()[2]
1049                    prefix_len = "64"
1050                else:
1051                    ip_address, prefix_len = line.split()[0].split("/")
1052                prefix_len = int(prefix_len)
1053                val = {"prefix_length": prefix_len}
1054                v6_interfaces.setdefault(interface, {})[ip_address] = val
1055            else:
1056                # match the following format:
1057                # IPv6 address: none
1058                v6_interfaces.setdefault(interface, {})
1059
1060        # Join data from intermediate dictionaries.
1061        for interface, data in v4_interfaces.items():
1062            interfaces_ip.setdefault(interface, {"ipv4": {}})["ipv4"] = data
1063
1064        for interface, data in v6_interfaces.items():
1065            interfaces_ip.setdefault(interface, {"ipv6": {}})["ipv6"] = data
1066
1067        return interfaces_ip
1068
1069    def get_mac_address_table(self):
1070        """
1071        Returns a lists of dictionaries. Each dictionary represents an entry in the MAC Address
1072        Table, having the following keys
1073            * mac (string)
1074            * interface (string)
1075            * vlan (int)
1076            * active (boolean)
1077            * static (boolean)
1078            * moves (int)
1079            * last_move (float)
1080        Format1:
1081
1082        Legend:
1083        * - primary entry, G - Gateway MAC, (R) - Routed MAC, O - Overlay MAC
1084        age - seconds since last seen,+ - primary entry using vPC Peer-Link,
1085        (T) - True, (F) - False
1086           VLAN     MAC Address      Type      age     Secure NTFY Ports/SWID.SSID.LID
1087        ---------+-----------------+--------+---------+------+----+------------------
1088        * 27       0026.f064.0000    dynamic      -       F    F    po1
1089        * 27       001b.54c2.2644    dynamic      -       F    F    po1
1090        * 27       0000.0c9f.f2bc    dynamic      -       F    F    po1
1091        * 27       0026.980a.df44    dynamic      -       F    F    po1
1092        * 16       0050.56bb.0164    dynamic      -       F    F    po2
1093        * 13       90e2.ba5a.9f30    dynamic      -       F    F    eth1/2
1094        * 13       90e2.ba4b.fc78    dynamic      -       F    F    eth1/1
1095          39       0100.5e00.4b4b    igmp         0       F    F    Po1 Po2 Po22
1096          110      0100.5e00.0118    igmp         0       F    F    Po1 Po2
1097                                                                    Eth142/1/3 Eth112/1/5
1098                                                                    Eth112/1/6 Eth122/1/5
1099
1100        """
1101
1102        #  The '*' is stripped out later
1103        RE_MACTABLE_FORMAT1 = r"^\s+{}\s+{}\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+".format(
1104            VLAN_REGEX, MAC_REGEX
1105        )
1106        RE_MACTABLE_FORMAT2 = r"^\s+{}\s+{}\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+".format(
1107            "-", MAC_REGEX
1108        )
1109        # REGEX dedicated for lines with only interfaces (suite of the previous MAC address)
1110        RE_MACTABLE_FORMAT3 = r"^\s+\S+"
1111
1112        mac_address_table = []
1113        command = "show mac address-table"
1114        output = self._send_command(command)
1115
1116        def remove_prefix(s, prefix):
1117            return s[len(prefix) :] if s.startswith(prefix) else s
1118
1119        def process_mac_fields(vlan, mac, mac_type, interface):
1120            """Return proper data for mac address fields."""
1121            if mac_type.lower() in ["self", "static", "system"]:
1122                static = True
1123                if vlan.lower() == "all":
1124                    vlan = 0
1125                elif vlan == "-":
1126                    vlan = 0
1127                if (
1128                    interface.lower() == "cpu"
1129                    or re.search(r"router", interface.lower())
1130                    or re.search(r"switch", interface.lower())
1131                ):
1132                    interface = ""
1133            else:
1134                static = False
1135            if mac_type.lower() in ["dynamic"]:
1136                active = True
1137            else:
1138                active = False
1139            return {
1140                "mac": helpers.mac(mac),
1141                "interface": helpers.canonical_interface_name(interface),
1142                "vlan": int(vlan),
1143                "static": static,
1144                "active": active,
1145                "moves": -1,
1146                "last_move": -1.0,
1147            }
1148
1149        # Skip the header lines
1150        output = re.split(r"^----.*", output, flags=re.M)[1:]
1151        output = "\n".join(output).strip()
1152        # Strip any leading characters
1153        output = re.sub(r"^[\*\+GOCE]", "", output, flags=re.M)
1154        output = re.sub(r"^\(R\)", "", output, flags=re.M)
1155        output = re.sub(r"^\(T\)", "", output, flags=re.M)
1156        output = re.sub(r"^\(F\)", "", output, flags=re.M)
1157        output = re.sub(r"vPC Peer-Link", "vPC-Peer-Link", output, flags=re.M)
1158
1159        for line in output.splitlines():
1160
1161            # Every 500 Mac's Legend is reprinted, regardless of terminal length
1162            if re.search(r"^Legend", line):
1163                continue
1164            elif re.search(r"^\s+\* \- primary entry", line):
1165                continue
1166            elif re.search(r"^\s+age \-", line):
1167                continue
1168            elif re.search(r"^\s+VLAN", line):
1169                continue
1170            elif re.search(r"^------", line):
1171                continue
1172            elif re.search(r"^\s*$", line):
1173                continue
1174
1175            for pattern in [
1176                RE_MACTABLE_FORMAT1,
1177                RE_MACTABLE_FORMAT2,
1178                RE_MACTABLE_FORMAT3,
1179            ]:
1180                if re.search(pattern, line):
1181                    fields = line.split()
1182                    if len(fields) >= 7:
1183                        vlan, mac, mac_type, _, _, _, interface = fields[:7]
1184                        mac_address_table.append(
1185                            process_mac_fields(vlan, mac, mac_type, interface)
1186                        )
1187
1188                        # there can be multiples interfaces for the same MAC on the same line
1189                        for interface in fields[7:]:
1190                            mac_address_table.append(
1191                                process_mac_fields(vlan, mac, mac_type, interface)
1192                            )
1193                        break
1194
1195                    # interfaces can overhang to the next line (line only contains interfaces)
1196                    elif len(fields) < 7:
1197                        for interface in fields:
1198                            mac_address_table.append(
1199                                process_mac_fields(vlan, mac, mac_type, interface)
1200                            )
1201                        break
1202            else:
1203                raise ValueError("Unexpected output from: {}".format(repr(line)))
1204
1205        return mac_address_table
1206
1207    def _get_bgp_route_attr(self, destination, vrf, next_hop, ip_version=4):
1208        """
1209        BGP protocol attributes for get_route_tp
1210        Only IPv4 supported
1211        """
1212
1213        CMD_SHIBNV = 'show ip bgp neighbors vrf {vrf} | include "is {neigh}"'
1214
1215        search_re_dict = {
1216            "aspath": {
1217                "re": r"AS-Path: ([\d\(\)]([\d\(\) ])*)",
1218                "group": 1,
1219                "default": "",
1220            },
1221            "bgpnh": {
1222                "re": r"[^|\\n][ ]{4}(" + IP_ADDR_REGEX + r")",
1223                "group": 1,
1224                "default": "",
1225            },
1226            "bgpfrom": {
1227                "re": r"from (" + IP_ADDR_REGEX + r")",
1228                "group": 1,
1229                "default": "",
1230            },
1231            "bgpcomm": {
1232                "re": r"  Community: ([\w\d\-\: ]+)",
1233                "group": 1,
1234                "default": "",
1235            },
1236            "bgplp": {"re": r"localpref (\d+)", "group": 1, "default": ""},
1237            # external, internal, redist
1238            "bgpie": {"re": r"^: (\w+),", "group": 1, "default": ""},
1239            "vrfimp": {
1240                "re": r"Imported from [\S]+ \(VRF (\S+)\)",
1241                "group": 1,
1242                "default": "",
1243            },
1244        }
1245
1246        bgp_attr = {}
1247        # get BGP AS number
1248        outbgp = self._send_command('show bgp process | include "BGP Protocol Tag"')
1249        matchbgpattr = RE_BGP_PROTO_TAG.match(outbgp)
1250        if not matchbgpattr:
1251            return bgp_attr
1252        bgpas = matchbgpattr.group(1)
1253        if ip_version == 4:
1254            bgpcmd = "show ip bgp vrf {vrf} {destination}".format(
1255                vrf=vrf, destination=destination
1256            )
1257            outbgp = self._send_command(bgpcmd)
1258            outbgpsec = outbgp.split("Path type")
1259
1260            # this should not happen (zero BGP paths)...
1261            if len(outbgpsec) == 1:
1262                return bgp_attr
1263
1264            # process all bgp paths
1265            for bgppath in outbgpsec[1:]:
1266                if "is best path" not in bgppath:
1267                    # only best path is added to protocol attributes
1268                    continue
1269                # find BGP attributes
1270                for key in search_re_dict:
1271                    matchre = re.search(search_re_dict[key]["re"], bgppath)
1272                    if matchre:
1273                        groupnr = int(search_re_dict[key]["group"])
1274                        search_re_dict[key]["result"] = matchre.group(groupnr)
1275                    else:
1276                        search_re_dict[key]["result"] = search_re_dict[key]["default"]
1277                bgpnh = search_re_dict["bgpnh"]["result"]
1278
1279                # if route is not leaked next hops have to match
1280                if (
1281                    not (search_re_dict["bgpie"]["result"] in ["redist", "local"])
1282                ) and (bgpnh != next_hop):
1283                    # this is not the right route
1284                    continue
1285                # find remote AS nr. of this neighbor
1286                bgpcmd = CMD_SHIBNV.format(vrf=vrf, neigh=bgpnh)
1287                outbgpnei = self._send_command(bgpcmd)
1288                matchbgpras = RE_BGP_REMOTE_AS.search(outbgpnei)
1289                if matchbgpras:
1290                    bgpras = matchbgpras.group(1)
1291                else:
1292                    # next-hop is not known in this vrf, route leaked from
1293                    #  other vrf or from vpnv4 table?
1294                    # get remote AS nr. from as-path if it is ebgp neighbor
1295                    # if locally sourced remote AS if undefined
1296                    bgpie = search_re_dict["bgpie"]["result"]
1297                    if bgpie == "external":
1298                        bgpras = bgpie.split(" ")[0].replace("(", "")
1299                    elif bgpie == "internal":
1300                        bgpras = bgpas
1301                    else:  # redist, local
1302                        bgpras = ""
1303                # community
1304                bothcomm = []
1305                extcomm = []
1306                stdcomm = search_re_dict["bgpcomm"]["result"].split()
1307                commsplit = bgppath.split("Extcommunity:")
1308                if len(commsplit) == 2:
1309                    for line in commsplit[1].split("\n")[1:]:
1310                        #          RT:65004:22
1311                        matchcommun = RE_BGP_COMMUN.match(line)
1312                        if matchcommun:
1313                            extcomm.append(matchcommun.group(1))
1314                        else:
1315                            # we've reached end of the extended community section
1316                            break
1317                bothcomm = stdcomm + extcomm
1318                bgp_attr = {
1319                    "as_path": search_re_dict["aspath"]["result"].strip(),
1320                    "remote_address": search_re_dict["bgpfrom"]["result"],
1321                    "local_preference": int(search_re_dict["bgplp"]["result"]),
1322                    "communities": bothcomm,
1323                    "local_as": helpers.as_number(bgpas),
1324                }
1325                if bgpras:
1326                    bgp_attr["remote_as"] = helpers.as_number(bgpras)
1327                else:
1328                    bgp_attr["remote_as"] = 0  # 0? , locally sourced
1329        return bgp_attr
1330
1331    def get_route_to(self, destination="", protocol="", longer=False):
1332        """
1333        Only IPv4 supported, vrf aware, longer_prefixes parameter ready
1334        """
1335        if longer:
1336            raise NotImplementedError("Longer prefixes not yet supported for NXOS")
1337        longer_pref = ""  # longer_prefixes support, for future use
1338        vrf = ""
1339
1340        ip_version = None
1341        try:
1342            ip_version = IPNetwork(destination).version
1343        except AddrFormatError:
1344            return "Please specify a valid destination!"
1345        if ip_version == 4:  # process IPv4 routing table
1346            routes = {}
1347            if vrf:
1348                send_cmd = "show ip route vrf {vrf} {destination} {longer}".format(
1349                    vrf=vrf, destination=destination, longer=longer_pref
1350                ).rstrip()
1351            else:
1352                send_cmd = "show ip route vrf all {destination} {longer}".format(
1353                    destination=destination, longer=longer_pref
1354                ).rstrip()
1355            out_sh_ip_rou = self._send_command(send_cmd)
1356            # IP Route Table for VRF "TEST"
1357            for vrfsec in out_sh_ip_rou.split("IP Route Table for ")[1:]:
1358                if "Route not found" in vrfsec:
1359                    continue
1360                vrffound = False
1361                preffound = False
1362                nh_list = []
1363                cur_prefix = ""
1364                for line in vrfsec.split("\n"):
1365                    if not vrffound:
1366                        vrfstr = RE_RT_VRF_NAME.match(line)
1367                        if vrfstr:
1368                            curvrf = vrfstr.group(1)
1369                            vrffound = True
1370                    else:
1371                        # 10.10.56.0/24, ubest/mbest: 2/0
1372                        prefstr = RE_RT_IPV4_ROUTE_PREF.match(line)
1373                        if prefstr:
1374                            if preffound:  # precess previous prefix
1375                                if cur_prefix not in routes:
1376                                    routes[cur_prefix] = []
1377                                for nh in nh_list:
1378                                    routes[cur_prefix].append(nh)
1379                                nh_list = []
1380                            else:
1381                                preffound = True
1382                            cur_prefix = prefstr.group(1)
1383                            continue
1384                        #     *via 10.2.49.60, Vlan3013, [0/0], 1y18w, direct
1385                        #      via 10.17.205.132, Po77.3602, [110/20], 1y18w, ospf-1000,
1386                        #            type-2, tag 2112
1387                        #     *via 10.17.207.42, Eth3/7.212, [110/20], 02:19:36, ospf-1000, type-2,
1388                        #            tag 2121
1389                        #     *via 10.17.207.73, [1/0], 1y18w, static
1390                        #     *via 10.17.209.132%vrf2, Po87.3606, [20/20], 1y25w, bgp-65000,
1391                        #            external, tag 65000
1392                        #     *via Vlan596, [1/0], 1y18w, static
1393                        viastr = RE_IP_ROUTE_VIA_REGEX.match(line)
1394                        if viastr:
1395                            nh_used = viastr.group("used") == "*"
1396                            nh_ip = viastr.group("ip") or ""
1397                            # when next hop is leaked from other vrf, for future use
1398                            # nh_vrf = viastr.group('vrf')
1399                            nh_int = viastr.group("int")
1400                            nh_metric = viastr.group("metric")
1401                            nh_age = bgp_time_conversion(viastr.group("age"))
1402                            nh_source = viastr.group("source")
1403                            # for future use
1404                            # rest_of_line = viastr.group('rest')
1405                            # use only routes from specified protocol
1406                            if protocol and protocol != nh_source:
1407                                continue
1408                            # routing protocol process number, for future use
1409                            # nh_source_proc_nr = viastr.group('procnr)
1410                            if nh_int:
1411                                nh_int_canon = helpers.canonical_interface_name(nh_int)
1412                            else:
1413                                nh_int_canon = ""
1414                            route_entry = {
1415                                "protocol": nh_source,
1416                                "outgoing_interface": nh_int_canon,
1417                                "age": nh_age,
1418                                "current_active": nh_used,
1419                                "routing_table": curvrf,
1420                                "last_active": nh_used,
1421                                "next_hop": nh_ip,
1422                                "selected_next_hop": nh_used,
1423                                "inactive_reason": "",
1424                                "preference": int(nh_metric),
1425                            }
1426                            if nh_source == "bgp":
1427                                route_entry[
1428                                    "protocol_attributes"
1429                                ] = self._get_bgp_route_attr(cur_prefix, curvrf, nh_ip)
1430                            else:
1431                                route_entry["protocol_attributes"] = {}
1432                            nh_list.append(route_entry)
1433                # process last next hop entries
1434                if preffound:
1435                    if cur_prefix not in routes:
1436                        routes[cur_prefix] = []
1437                    for nh in nh_list:
1438                        routes[cur_prefix].append(nh)
1439        return routes
1440
1441    def get_snmp_information(self):
1442        snmp_information = {}
1443        command = "show running-config"
1444        output = self._send_command(command)
1445        snmp_config = helpers.textfsm_extractor(self, "snmp_config", output)
1446
1447        if not snmp_config:
1448            return snmp_information
1449
1450        snmp_information = {
1451            "contact": str(""),
1452            "location": str(""),
1453            "community": {},
1454            "chassis_id": str(""),
1455        }
1456
1457        for snmp_entry in snmp_config:
1458            contact = str(snmp_entry.get("contact", ""))
1459            if contact:
1460                snmp_information["contact"] = contact
1461            location = str(snmp_entry.get("location", ""))
1462            if location:
1463                snmp_information["location"] = location
1464
1465            community_name = str(snmp_entry.get("community", ""))
1466            if not community_name:
1467                continue
1468
1469            if community_name not in snmp_information["community"].keys():
1470                snmp_information["community"][community_name] = {
1471                    "acl": str(snmp_entry.get("acl", "")),
1472                    "mode": str(snmp_entry.get("mode", "").lower()),
1473                }
1474            else:
1475                acl = str(snmp_entry.get("acl", ""))
1476                if acl:
1477                    snmp_information["community"][community_name]["acl"] = acl
1478                mode = str(snmp_entry.get("mode", "").lower())
1479                if mode:
1480                    snmp_information["community"][community_name]["mode"] = mode
1481        return snmp_information
1482
1483    def get_users(self):
1484        _CISCO_TO_CISCO_MAP = {"network-admin": 15, "network-operator": 5}
1485
1486        _DEFAULT_USER_DICT = {"password": "", "level": 0, "sshkeys": []}
1487
1488        users = {}
1489        command = "show running-config"
1490        output = self._send_command(command)
1491        section_username_tabled_output = helpers.textfsm_extractor(
1492            self, "users", output
1493        )
1494
1495        for user in section_username_tabled_output:
1496            username = user.get("username", "")
1497            if not username:
1498                continue
1499            if username not in users:
1500                users[username] = _DEFAULT_USER_DICT.copy()
1501
1502            password = user.get("password", "")
1503            if password:
1504                users[username]["password"] = str(password.strip())
1505
1506            level = 0
1507            role = user.get("role", "")
1508            if role.startswith("priv"):
1509                level = int(role.split("-")[-1])
1510            else:
1511                level = _CISCO_TO_CISCO_MAP.get(role, 0)
1512            if level > users.get(username).get("level"):
1513                # unfortunately on Cisco you can set different priv levels for the same user
1514                # Good news though: the device will consider the highest level
1515                users[username]["level"] = level
1516
1517            sshkeytype = user.get("sshkeytype", "")
1518            sshkeyvalue = user.get("sshkeyvalue", "")
1519            if sshkeytype and sshkeyvalue:
1520                if sshkeytype not in ["ssh-rsa", "ssh-dsa"]:
1521                    continue
1522                users[username]["sshkeys"].append(str(sshkeyvalue))
1523        return users
1524
1525    def get_vlans(self):
1526        vlans = {}
1527        command = "show vlan brief | json"
1528        vlan_table_raw = self._get_command_table(
1529            command, "TABLE_vlanbriefxbrief", "ROW_vlanbriefxbrief"
1530        )
1531        if isinstance(vlan_table_raw, dict):
1532            vlan_table_raw = [vlan_table_raw]
1533
1534        for vlan in vlan_table_raw:
1535            if "vlanshowplist-ifidx" not in vlan.keys():
1536                vlan["vlanshowplist-ifidx"] = []
1537            vlans[vlan["vlanshowbr-vlanid"]] = {
1538                "name": vlan["vlanshowbr-vlanname"],
1539                "interfaces": self._parse_vlan_ports(vlan["vlanshowplist-ifidx"]),
1540            }
1541        return vlans
1542
1543    def get_optics(self):
1544        command = "show interface transceiver details"
1545        output = self._send_command(command)
1546
1547        # Formatting data into return data structure
1548        optics_detail = {}
1549
1550        # Extraction Regexps
1551        port_ts_re = re.compile(r"^Ether.*?(?=\nEther|\Z)", re.M | re.DOTALL)
1552        port_re = re.compile(r"^(Ether.*)[ ]*?$", re.M)
1553        vendor_re = re.compile("name is (.*)$", re.M)
1554        vendor_part_re = re.compile("part number is (.*)$", re.M)
1555        vendor_rev_re = re.compile("revision is (.*)$", re.M)
1556        serial_no_re = re.compile("serial number is (.*)$", re.M)
1557        type_no_re = re.compile("type is (.*)$", re.M)
1558        rx_instant_re = re.compile(r"Rx Power[ ]+(?:(\S+?)[ ]+dBm|(N.A))", re.M)
1559        tx_instant_re = re.compile(r"Tx Power[ ]+(?:(\S+?)[ ]+dBm|(N.A))", re.M)
1560        current_instant_re = re.compile(r"Current[ ]+(?:(\S+?)[ ]+mA|(N.A))", re.M)
1561
1562        port_ts_l = port_ts_re.findall(output)
1563
1564        for port_ts in port_ts_l:
1565            port = port_re.search(port_ts).group(1)
1566            # No transceiver is present in those case
1567            if "transceiver is not present" in port_ts:
1568                continue
1569            if "transceiver is not applicable" in port_ts:
1570                continue
1571            port_detail = {"physical_channels": {"channel": []}}
1572            # No metric present
1573            vendor = vendor_re.search(port_ts).group(1)
1574            vendor_part = vendor_part_re.search(port_ts).group(1)
1575            vendor_rev = vendor_rev_re.search(port_ts).group(1)
1576            serial_no = serial_no_re.search(port_ts).group(1)
1577            type_s = type_no_re.search(port_ts).group(1)
1578            state = {
1579                "vendor": vendor.strip(),
1580                "vendor_part": vendor_part.strip(),
1581                "vendor_rev": vendor_rev.strip(),
1582                "serial_no": serial_no.strip(),
1583                "connector_type": self.connector_type_map.get(type_s, "Unknown"),
1584            }
1585            if "DOM is not supported" not in port_ts:
1586                res = rx_instant_re.search(port_ts)
1587                input_power = res.group(1) or res.group(2)
1588                res = tx_instant_re.search(port_ts)
1589                output_power = res.group(1) or res.group(2)
1590                res = current_instant_re.search(port_ts)
1591                current = res.group(1) or res.group(2)
1592
1593                # If interface is shutdown it returns "N/A" as output power
1594                # or "N/A" as input power
1595                # Converting that to -100.0 float
1596                try:
1597                    float(output_power)
1598                except ValueError:
1599                    output_power = -100.0
1600                try:
1601                    float(input_power)
1602                except ValueError:
1603                    input_power = -100.0
1604                try:
1605                    float(current)
1606                except ValueError:
1607                    current = -100.0
1608
1609                # Defaulting avg, min, max values to -100.0 since device does not
1610                # return these values
1611                optic_states = {
1612                    "index": 0,
1613                    "state": {
1614                        "input_power": {
1615                            "instant": (
1616                                float(input_power) if "input_power" else -100.0
1617                            ),
1618                            "avg": -100.0,
1619                            "min": -100.0,
1620                            "max": -100.0,
1621                        },
1622                        "output_power": {
1623                            "instant": (
1624                                float(output_power) if "output_power" else -100.0
1625                            ),
1626                            "avg": -100.0,
1627                            "min": -100.0,
1628                            "max": -100.0,
1629                        },
1630                        "laser_bias_current": {
1631                            "instant": (float(current) if "current" else -100.0),
1632                            "avg": 0.0,
1633                            "min": 0.0,
1634                            "max": 0.0,
1635                        },
1636                    },
1637                }
1638                port_detail["physical_channels"]["channel"].append(optic_states)
1639
1640            port_detail["state"] = state
1641            optics_detail[port] = port_detail
1642
1643        return optics_detail
1644
1645    def get_interfaces_counters(self):
1646        """
1647        Return interface counters and errors.
1648
1649        'tx_errors': int,
1650        'rx_errors': int,
1651        'tx_discards': int,
1652        'rx_discards': int,
1653        'tx_octets': int,
1654        'rx_octets': int,
1655        'tx_unicast_packets': int,
1656        'rx_unicast_packets': int,
1657        'tx_multicast_packets': int,
1658        'rx_multicast_packets': int,
1659        'tx_broadcast_packets': int,
1660        'rx_broadcast_packets': int,
1661        """
1662        if_mapping = {
1663            "eth": {
1664                "regexp": re.compile("^(Ether|port-channel).*"),
1665                "mapping": {
1666                    "tx_errors": "eth_outerr",
1667                    "rx_errors": "eth_inerr",
1668                    "tx_discards": "eth_outdiscard",
1669                    "rx_discards": "eth_indiscard",
1670                    "tx_octets": "eth_outbytes",
1671                    "rx_octets": "eth_inbytes",
1672                    "tx_unicast_packets": "eth_outucast",
1673                    "rx_unicast_packets": "eth_inucast",
1674                    "tx_multicast_packets": "eth_outmcast",
1675                    "rx_multicast_packets": "eth_inmcast",
1676                    "tx_broadcast_packets": "eth_outbcast",
1677                    "rx_broadcast_packets": "eth_inbcast",
1678                },
1679            },
1680            "mgmt": {
1681                "regexp": re.compile("mgm.*"),
1682                "mapping": {
1683                    "tx_errors": None,
1684                    "rx_errors": None,
1685                    "tx_discards": None,
1686                    "rx_discards": None,
1687                    "tx_octets": "mgmt_out_bytes",
1688                    "rx_octets": "mgmt_in_bytes",
1689                    "tx_unicast_packets": None,
1690                    "rx_unicast_packets": None,
1691                    "tx_multicast_packets": "mgmt_out_mcast",
1692                    "rx_multicast_packets": "mgmt_in_mcast",
1693                    "tx_broadcast_packets": None,
1694                    "rx_broadcast_packets": None,
1695                },
1696            },
1697        }
1698        command = "show interface counters detailed | json"
1699        # To retrieve discards
1700        command_interface = "show interface | json"
1701        counters_table_raw = self._get_command_table(
1702            command, "TABLE_interface", "ROW_interface"
1703        )
1704        counters_interface_table_raw = self._get_command_table(
1705            command_interface, "TABLE_interface", "ROW_interface"
1706        )
1707        all_stats_d = {}
1708        # Start with show interface as all interfaces
1709        # Are surely listed
1710        for row in counters_interface_table_raw:
1711            if_counter = {}
1712            # loop through regexp to find mapping
1713            for if_v in if_mapping:
1714                my_re = if_mapping[if_v]["regexp"]
1715                re_match = my_re.match(row["interface"])
1716                if re_match:
1717                    interface = re_match.group()
1718                    map_d = if_mapping[if_v]["mapping"]
1719                    for k, v in map_d.items():
1720                        if_counter[k] = int(row[v]) if v in row else 0
1721                    all_stats_d[interface] = if_counter
1722                    break
1723
1724        for row in counters_table_raw:
1725            if_counter = {}
1726            # loop through regexp to find mapping
1727            for if_v in if_mapping:
1728                my_re = if_mapping[if_v]["regexp"]
1729                re_match = my_re.match(row["interface"])
1730                if re_match:
1731                    interface = re_match.group()
1732                    map_d = if_mapping[if_v]["mapping"]
1733                    for k, v in map_d.items():
1734                        if v in row:
1735                            if_counter[k] = int(row[v])
1736                    all_stats_d[interface].update(if_counter)
1737                    break
1738
1739        return all_stats_d
1740