Exemple #1
0
class DNOS6Driver(NetworkDriver):
    def __init__(self,
                 hostname,
                 username,
                 password,
                 timeout=60,
                 optional_args=None):
        self.hostname = hostname
        self.username = username
        self.password = password
        self.timeout = timeout
        optional_args = optional_args or dict()

        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')
        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)
        self.auto_rollback_on_error = optional_args.get(
            'auto_rollback_on_error', True)

        # Control automatic toggling of 'file prompt quiet' for file operations
        self.auto_file_prompt = optional_args.get('auto_file_prompt', True)

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

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

        default_port = {'ssh': 22, 'telnet': 23}
        self.port = optional_args.get('port', default_port[self.transport])

        self.device = None
        self.config_replace = False

        self.profile = ["dnos6"]
        self.use_canonical_interface = optional_args.get(
            'canonical_int', False)

    def open(self):
        """Open a connection to the device."""
        device_type = 'dell_dnos6'
        if self.transport == 'telnet':
            device_type = 'dell_dnos6_telnet'
        self.device = ConnectHandler(device_type=device_type,
                                     host=self.hostname,
                                     username=self.username,
                                     password=self.password,
                                     **self.netmiko_optional_args)
        # ensure in enable mode
        self.device.enable()

    def _discover_file_system(self):
        try:
            return self.device._autodetect_fs()
        except Exception:
            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."""
        self.device.disconnect()

    def _send_command(self, command):
        """Wrapper for self.device.send.command().

        If command is a list will iterate through commands until valid command.
        """
        try:
            if isinstance(command, list):
                for cmd in command:
                    output = self.device.send_command(cmd)
                    if "% Invalid" not in output:
                        break
            else:
                output = self.device.send_command(command)
            # return self._send_command_postprocess(output)
            return output
        except (socket.error, EOFError) as e:
            raise ConnectionClosedException(str(e))

    def is_alive(self):
        """Returns a flag with the state of the connection."""
        null = chr(0)
        if self.device is None:
            return {'is_alive': False}
        if self.transport == 'telnet':
            try:
                # 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:
                # Netmiko logging bug (remove after Netmiko >= 1.4.3)
                return {'is_alive': True}
            except AttributeError:
                return {'is_alive': False}
        else:
            # SSH
            try:
                # Try sending ASCII null byte to maintain the connection alive
                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):
        """Write temp file and for use with inline config and SCP."""
        tmp_dir = tempfile.gettempdir()
        rand_fname = py23_compat.text_type(uuid.uuid4())
        filename = os.path.join(tmp_dir, rand_fname)
        with open(filename, 'wt') as fobj:
            fobj.write(config)
        return filename

    def get_config(self, retrieve='all'):
        """Implementation of get_config for DNOS6.

        Returns the startup or/and running configuration as dictionary.
        The keys of the dictionary represent the type of configuration
        (startup or running). The candidate is always empty string,
        since IOS does not support candidate configuration.
        """

        configs = {
            'startup': '',
            'running': '',
            'candidate': '',
        }

        if retrieve in ('startup', 'all'):
            command = 'show startup-config'
            output = self._send_command(command)
            configs['startup'] = output

        if retrieve in ('running', 'all'):
            command = 'show running-config'
            output = self._send_command(command)
            configs['running'] = output

        return configs

    def get_environment(self):
        """
        Get environment facts.

        power and fan are currently not implemented
        cpu is using 1-minute average
        cpu hard-coded to cpu0 (i.e. only a single CPU)
        """
        environment = {}
        cpu_cmd = 'show proc cpu'
        temp_cmd = 'show system temperature'

        output = self._send_command(cpu_cmd)
        environment.setdefault('cpu', {})
        environment['cpu'][0] = {}
        environment['cpu'][0]['%usage'] = 0.0

        for line in output.splitlines():
            if 'Total CPU Utilization' in line:
                # ['Total', 'CPU', 'Utilization', '9.26%', '9.75%', '9.72%']
                oneminute = float(line.split()[4][:-1])
                environment['cpu'][0]['%usage'] = float(oneminute)
                break
            if 'alloc' in line:
                used_mem = int(line.split()[1])
            if 'free' in line:
                avail_mem = int(line.split()[1])

        environment.setdefault('memory', {})
        environment['memory']['used_ram'] = used_mem
        environment['memory']['available_ram'] = used_mem + avail_mem

        environment.setdefault('temperature', {})
        re_temp_value = re.compile('(.*) Temperature Value')
        # TODO
        output = self._send_command(temp_cmd)
        env_value = {
            'is_alert': False,
            'is_critical': False,
            'temperature': -1.0
        }
        environment['temperature']['invalid'] = env_value

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

        return environment

    def get_mac_address_table(self):
        """
        Returns a lists of dictionaries. Each dictionary represents an entry in the MAC Address
        Table, having the following keys
            * mac (string)
            * interface (string)
            * vlan (int)
            * active (boolean)
            * static (boolean)
            * moves (int)
            * last_move (float)

        Format1:
        Aging time is 300 Sec

        Vlan     Mac Address           Type        Port
        -------- --------------------- ----------- ---------------------
        1        0025.90C2.88ED        Dynamic     Gi1/0/48
        1        F48E.3841.9628        Management  Vl1
        """
        def _process_mac_fields(vlan, mac, mac_type, interface):
            """Return proper data for mac address fields."""
            if mac_type.lower() in ['management', 'static']:
                static = True
            else:
                static = False

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

            return {
                'mac': napalm.base.helpers.mac(mac),
                'interface': self._canonical_int(interface),
                'vlan': int(vlan),
                'static': static,
                'active': active,
                'moves': -1,
                'last_move': -1.0
            }

        output = self._send_command("show mac address-table")
        output = re.split(r'^----.*', output, flags=re.M)[1:]
        output = re.split(r'\n\nTotal.*', output[0], flags=re.M)[0]
        lines = output.split('\n')
        entries = []
        [
            entries.append(_process_mac_fields(*i.split())) for i in lines
            if i != ''
        ]
        return entries

    def get_arp_table(self):
        """
        Returns a list of dictionaries having the following set of keys:
            * interface (string)
            * mac (string)
            * ip (string)
            * age (float)

        Example::

            [
                {
                    'interface' : 'MgmtEth0/RSP0/CPU0/0',
                    'mac'       : '5C:5E:AB:DA:3C:F0',
                    'ip'        : '172.17.17.1',
                    'age'       : 1454496274.84
                },
                {
                    'interface' : 'MgmtEth0/RSP0/CPU0/0',
                    'mac'       : '5C:5E:AB:DA:3C:FF',
                    'ip'        : '172.17.17.2',
                    'age'       : 1435641582.49
                }
            ]

        """
        def _process_arp_fields(ip, mac, interface, mac_type, h, m='', s=''):
            if h == 'n/a':
                age = -1
            else:
                h = int(h[:-1])
                m = int(m[:-1])
                s = int(s[:-1])
                age = h * 3600 + m * 60 + s
            return {
                'interface': self._canonical_int(interface),
                'mac': napalm.base.helpers.mac(mac),
                'ip': ip,
                'age': float(age)
            }

        output = self._send_command("show arp")
        output = re.split(r'^----.*', output, flags=re.M)[1]
        lines = output.split('\n')
        entries = []
        [
            entries.append(_process_arp_fields(*i.split())) for i in lines
            if i != ''
        ]
        return entries

    def get_interfaces(self):
        def config_for_iface(iface, configs):
            for config in configs:
                if config[0] == iface:
                    iface_config = config[1]
                    m = re.search('description: "(.+?)"', iface_config)
                    descr = ''
                    if m != None:
                        descr = m.group(1)
                    m = re.search('((no )?shutdown)', iface_config)
                    enabled = not bool(m != None and m.group(1) == 'shutdown')
                    return (descr, enabled)
            return ('', True)

        config_raw = self._send_command("show running-config")
        config_ifaces = re.findall("!\ninterface (.*)\n((.|\n)*?)\nexit",
                                   config_raw)
        ifaces_raw = self._send_command("show interfaces")
        ifaces = ifaces_raw.split('\n\n')
        iface_list = []
        for iface in ifaces:
            if iface == '':
                continue
            name = re.search('Interface Name : \\.+\s(.*)\n', iface).group(1)
            speed = re.search('Port Speed : \\.+\s(.*)\n', iface).group(1)
            mac = re.search('L3 MAC Address\\.+\s(.*)\n', iface).group(1)
            status = re.search('Link Status : \\.+\s(.*)\n', iface).group(1)
            description, enabled = config_for_iface(name, config_ifaces)
            if speed == 'Unknown':
                speed = 0
            iface_list.append({
                self._canonical_int(name): {
                    'is_up': bool(status is 'Up'),
                    'is_enable': enabled,
                    'description': description,
                    'last_flapped': -1,
                    'speed': int(speed),
                    'mac_address': napalm.base.helpers.mac(mac)
                }
            })
        return iface_list

    def get_lldp_neighbors(self):
        result = collections.defaultdict(list)
        output = self._send_command("show lldp remote-device all")
        output = re.split(r'^----.*', output, flags=re.M)[1]
        lines = output.split('\n')
        for line in lines:
            if line == '':
                continue
            iface = line[0:10].strip()
            portid = line[38:55]
            systemname = line[57:].strip()
            if systemname == '':
                systemname = None
            result[iface].append({'hostname': systemname, 'port': portid})
        return dict(result)

    def _get_lldp_neighbor_detail_iface(self, interface):
        caps_map = {
            'bridge': 'B',
            'router': 'R',
            'WLAN access point': 'W',
            'station only': 'S',
        }
        # support empty interface
        output = self._send_command("show lldp remote-device detail %s" %
                                    interface)
        chassis_id = re.search(r'Chassis ID: (.+)', output).group(1)
        try:
            system_name = re.search(r'System Name: (.+)', output).group(1)
        except:
            system_name = ''
        try:
            port_description = re.search(r'Port Description: (.+)',
                                         output).group(1)
        except:
            port_description = ''
        try:
            caps_supported_output = re.search(
                r'System Capabilities Supported: (.+)', output).group(1)
        except:
            caps_supported_output = ''
        try:
            caps_enabled_output = re.search(
                r'System Capabilities Enabled: (.+)', output).group(1)
        except:
            caps_enabled_output = ''

        caps_supported = caps_supported_output.split(', ')
        caps_supported = [[caps_maps[cap] for cap in caps_supported]]
        caps_enabled = caps_enabled_output.split(', ')
        caps_enabled = [[caps_enabled[cap] for cap in caps_enabled]]

        return {
            #                'parent_interface': u''
            'remote_chassis_id': chassis_id,
            'remote_system_name': system_name,
            'remote_port': port_description,
            'remote_port_description': port_description,
            'remote_system_description': system_decription,
            'remote_system_capab': caps_supported.join(', '),
            'remote_system_enable_capab': caps_enabled.join(', ')
        }

    def get_lldp_neighbor_detail(self, interface=''):
        if interface == '':
            ifaces = []
            neighs = self.get_lldp_neighbors()
            for iface in neighs:
                ifaces.append(self._get_lldp_neighbor_detail_iface(iface))
            return ifaces
        else:
            return _get_lldp_neighbor_detail_iface(iface)

    def get_ntp_peers(self):
        """
        Returns the NTP peers configuration as dictionary.
        The keys of the dictionary represent the IP Addresses of the peers.
        Inner dictionaries do not have yet any available keys.

        Example::

            {
                '192.168.0.1': {},
                '17.72.148.53': {},
                '37.187.56.220': {},
                '162.158.20.18': {}
            }

        """
        output = self._send_command("show sntp server")
        output = re.findall('Host Address:\s(\S+)', output)
        entries = dict()
        for i in output:
            entries[i] = {}
        return entries

    def getnfacts(self):
        """
        Returns a dictionary containing the following information:
         * uptime - Uptime of the device in seconds.
         * vendor - Manufacturer of the device.
         * model - Device model.
         * hostname - Hostname of the device
         * fqdn - Fqdn of the device
         * os_version - String with the OS version running on the device.
         * serial_number - Serial number of the device
         * interface_list - List of the interfaces of the device

        Example::

            {
            'uptime': 151005.57332897186,
            'vendor': u'Arista',
            'os_version': u'4.14.3-2329074.gaatlantarel',
            'serial_number': u'SN0123A34AS',
            'model': u'vEOS',
            'hostname': u'eos-router',
            'fqdn': u'eos-router',
            'interface_list': [u'Ethernet2', u'Management1', u'Ethernet1', u'Ethernet3']
            }

        """

        return {
            'uptime':
            151005.57332897186,
            'vendor':
            u'Dell',
            'os_version':
            u'4.14.3-2329074.gaatlantarel',
            'serial_number':
            u'SN0123A34AS',
            'model':
            u'vEOS',
            'hostname':
            u'eos-router',
            'fqdn':
            u'eos-router',
            'interface_list':
            [u'Ethernet2', u'Management1', u'Ethernet1', u'Ethernet3']
        }
class S350Driver(NetworkDriver):
    """Napalm driver for S350."""

    def __init__(self, hostname, username, password, timeout=60, optional_args=None):
        """Constructor."""
        self.device = None
        self.hostname = hostname
        self.username = username
        self.password = password
        self.timeout = timeout

        if optional_args is None:
            optional_args = {}

        self._dest_file_system = optional_args.get("dest_file_system", None)

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

        # Allow for passing additional Netmiko arguments
        self.netmiko_optional_args = {}
        for k, v in netmiko_argument_map.items():
            try:
                self.netmiko_optional_args[k] = optional_args[k]
            except KeyError:
                pass

        self.port = optional_args.get("port", 22)
        self.device = None
        self.force_no_enable = optional_args.get("force_no_enable", False)

    def open(self):
        """Open a connection to the device."""

        self.device = ConnectHandler(
            device_type="cisco_s300",
            host=self.hostname,
            username=self.username,
            password=self.password,
            **self.netmiko_optional_args,
        )
        if not self.force_no_enable:
            self.device.enable()

    def _discover_file_system(self):
        try:
            return self.device._autodetect_fs()
        except Exception:
            msg = (
                "Netmiko _autodetect_fs failed (to work around specify "
                "dest_file_system in optional_args)."
            )
            raise CommandErrorException(msg)

    def close(self):
        """Close the connection to the device."""
        self.device.disconnect()

    def cli(self, commands):
        output = {}
        try:
            for cmd in commands:
                output[cmd] = self.device.send_command(cmd)

            return output
        except (socket.error, EOFError) as e:
            raise ConnectionClosedException(str(e))

    def _send_command(self, command):
        """Wrapper for self.device.send.command().

        If command is a list will iterate through commands until valid command.
        """
        try:
            if isinstance(command, list):
                for cmd in command:
                    output = self.device.send_command(cmd)
                    if "% Invalid" not in output:
                        break
            else:
                output = self.device.send_command(command)
            return output.strip()
        except (socket.error, EOFError) as e:
            raise ConnectionClosedException(str(e))

    def _parse_uptime(self, uptime_str):
        """Parse an uptime string into number of seconds"""
        uptime_str = uptime_str.strip()
        days, timespec = uptime_str.split(",")

        hours, minutes, seconds = timespec.split(":")

        uptime_sec = (int(days) * 86400) + (int(hours) * 3600) + (int(minutes) * 60) + int(seconds)
        return uptime_sec

    def get_arp_table(self, vrf=""):
        """
        Get the ARP table, the age isn't readily available so we leave that out for now.

        vrf is needed for test - no support on s350
        """

        arp_table = []

        output = self._send_command("show arp")

        for line in output.splitlines():
            # A VLAN may not be set for the entry
            if "vlan" not in line:
                continue
            if len(line.split()) == 4:
                interface, ip, mac, _ = line.split()
            elif len(line.split()) == 5:
                if1, if2, ip, mac, _ = line.split()
                interface = "{} {}".format(if1, if2)
            elif len(line.split()) == 6:
                _, _, interface, ip, mac, _ = line.split()
            else:
                raise ValueError("Unexpected output: {}".format(line.split()))

            interface = canonical_interface_name(interface, s350_base_interfaces)

            entry = {
                "interface": interface,
                "mac": napalm.base.helpers.mac(mac),
                "ip": ip,
                "age": 0.0,
            }

            arp_table.append(entry)

        return arp_table

    def get_config(self, retrieve="all", full=False, sanitized=False):
        """
        get_config for S350. Since this firmware doesn't support a candidate
        configuration we leave it empty.
        """

        configs = {
            "startup": "",
            "running": "",
            "candidate": "",
        }

        if retrieve in ("all", "startup"):
            startup = self._send_command("show startup-config")
            configs["startup"] = self._get_config_filter(startup)

        if retrieve in ("all", "running"):
            # IOS supports "full" only on "show running-config"
            run_full = " detailed" if full else ""
            running = self._send_command("show running-config" + run_full)
            configs["running"] = self._get_config_filter(running)

        if sanitized:
            configs = self._get_config_sanitized(configs)

        return configs

    def _get_config_filter(self, config):
        # The output of get_config should be directly usable by load_replace_candidate()

        # remove header
        filter_strings = [
            r"(?sm)^config-file-header.*^@$",
        ]

        for ft in filter_strings:
            config = re.sub(ft, "", config)

        return config

    def _get_config_sanitized(self, configs):
        # Do not output sensitive information

        # use Cisco IOS filters
        configs = napalm.base.helpers.sanitize_configs(configs, C.CISCO_SANITIZE_FILTERS)

        # defina my own filters
        s350_filters = {
            r"^(.* password) (\S+) (\S+) (.*)$": r"\1 \2 <removed> \4",
            r"^(snmp-server location) (\S+).*$": r"\1 <removed>",
        }

        configs = napalm.base.helpers.sanitize_configs(configs, s350_filters)

        return configs

    def get_facts(self):
        """Return a set of facts from the device."""
        serial_number, fqdn, os_version, hostname, domainname = ("Unknown",) * 5

        # Submit commands to the device.
        show_ver = self._send_command("show version")
        show_sys = self._send_command("show system")
        show_inv = self._send_command("show inventory")
        show_hosts = self._send_command("show hosts")
        show_int_st = self._send_command("show interfaces status")

        os_version = self._get_facts_parse_os_version(show_ver)

        # hostname
        hostname = self._get_facts_hostname(show_sys)
        # special case for SG500 fw v1.4.x
        if hostname == "Unknown":
            hostname = self._get_facts_hostname_from_config(
                self._send_command("show running-config")
            )

        # uptime
        uptime_str = self._get_facts_uptime(show_sys)
        uptime = self._parse_uptime(uptime_str)

        # serial_number and model
        inventory = self._get_facts_parse_inventory(show_inv)["1"]
        serial_number = inventory["sn"]
        model = inventory["pid"]

        # fqdn
        domainname = napalm.base.helpers.textfsm_extractor(self, "hosts", show_hosts)[0]
        domainname = domainname["domain_name"]
        if domainname == "Domain":
            domainname = "Unknown"
        if domainname != "Unknown" and hostname != "Unknown":
            fqdn = "{0}.{1}".format(hostname, domainname)

        # interface_list
        interfaces = []
        show_int_st = show_int_st.strip()
        # remove the header information
        show_int_st = re.sub(
            r"(^-.*$|^Port .*$|^Ch .*$)|^\s.*$|^.*Flow.*$", "", show_int_st, flags=re.M
        )
        for line in show_int_st.splitlines():
            if not line:
                continue
            interface = line.split()[0]
            interface = canonical_interface_name(interface, s350_base_interfaces)

            interfaces.append(str(interface))

        return {
            "fqdn": str(fqdn),
            "hostname": str(hostname),
            "interface_list": interfaces,
            "model": str(model),
            "os_version": str(os_version),
            "serial_number": str(serial_number),
            "uptime": uptime,
            "vendor": "Cisco",
        }

    def _get_facts_hostname_from_config(self, show_running):
        # special case for SG500 fw v1.4.x
        hostname = "Unknown"
        for line in show_running.splitlines():
            if line.startswith("hostname "):
                _, hostname = line.split("hostname")
                hostname = hostname.strip()
                break

        return hostname

    def _get_facts_hostname(self, show_sys):
        hostname = "Unknown"
        for line in show_sys.splitlines():
            if line.startswith("System Name:"):
                _, hostname = line.split("System Name:")
                hostname = hostname.strip()
                break

        return hostname

    def _get_facts_uptime(self, show_sys):
        i = 0
        syslines = []
        fields = []
        uptime_header_lineNo = None
        uptime_str = None
        for line in show_sys.splitlines():
            # All models except SG500 fw 1.4.x
            if line.startswith("System Up Time (days,hour:min:sec):"):
                _, uptime_str = line.split("System Up Time (days,hour:min:sec):")
                break

            line = re.sub(r"  *", " ", line, re.M)
            line = line.strip()

            fields = line.split(" ")
            syslines.append(fields)

            if "Unit" in syslines[i] and "time" in syslines[i]:
                uptime_header_lineNo = i

            i += 1

        # SG500 fw 1.4.x
        if not uptime_str:
            uptime_str = syslines[uptime_header_lineNo + 2][1]

        return uptime_str

    def _get_facts_parse_inventory(self, show_inventory):
        """ inventory can list more modules/devices """
        # make 1 module 1 line
        show_inventory = re.sub(r"\nPID", "  PID", show_inventory, re.M)
        # delete empty lines
        show_inventory = re.sub(r"^\n", "", show_inventory, re.M)
        show_inventory = re.sub(r"\n\n", "", show_inventory, re.M)
        show_inventory = re.sub(r"\n\s*\n", r"\n", show_inventory, re.M)
        lines = show_inventory.splitlines()

        modules = {}
        for line in lines:
            match = re.search(
                r"""
                ^
                NAME:\s"(?P<name>\S+)"\s*
                DESCR:\s"(?P<descr>[^"]+)"\s*
                PID:\s(?P<pid>\S+)\s*
                VID:\s(?P<vid>.+\S)\s*
                SN:\s(?P<sn>\S+)\s*
                """,
                line,
                re.X,
            )
            module = match.groupdict()
            modules[module["name"]] = module

        if modules:
            return modules

    def _get_facts_parse_os_version(self, show_ver):
        # os_version
        # detect os ver > 2
        if re.search(r"^Active-image", show_ver):
            for line in show_ver.splitlines():
                # First version line is the active version
                if re.search(r"Version:", line):
                    _, os_version = line.split("Version: ")
                    break
        elif re.search(r"^SW version", show_ver):
            for line in show_ver.splitlines():
                if re.search(r"^SW version", line):
                    _, ver = line.split("    ")
                    os_version, _ = ver.split(" (")
                    break
        else:
            # show_ver = re.sub(r'^\n', '', show_ver, re.M)
            for line in show_ver.splitlines():
                line = re.sub(r"  *", " ", line, re.M)
                line = line.strip()
                line_comps = line.split(" ")
                if line_comps[0] == "1":
                    os_version = line_comps[1]
                    break

        return os_version

    def get_interfaces(self):
        """
        get_interfaces() implementation for S350
        """
        interfaces = {}

        show_status_output = self._send_command("show interfaces status")
        show_description_output = self._send_command("show interfaces description")

        # by documentation SG350
        show_jumbo_frame = self._send_command("show ports jumbo-frame")
        match = re.search(r"Jumbo frames are enabled", show_jumbo_frame, re.M)
        if match:
            mtu = 9000
        else:
            mtu = 1518

        mac = "0"

        for status_line in show_status_output.splitlines():
            if "Up" in status_line or "Down" in status_line:
                if "Po" in status_line:
                    interface, _, _, speed, _, _, link_state = status_line.split()
                else:
                    interface, _, _, speed, _, _, link_state, _, _ = status_line.split()

                # Since the MAC address for all the local ports are equal, get the address
                # from the first port and use it everywhere.
                if mac == "0":
                    show_system_output = self._send_command("show lldp local " + interface)
                    mac = show_system_output.splitlines()[0].split(":", maxsplit=1)[1].strip()

                if speed == "--":
                    is_enabled = False
                    speed = 0
                else:
                    is_enabled = True
                    speed = int(speed)

                is_up = link_state == "Up"

                for descr_line in show_description_output.splitlines():
                    description = 0
                    if descr_line.startswith(interface):
                        description = " ".join(descr_line.split()[1:])
                        break

                # last_flapped can not be get - setting to default
                entry = {
                    "is_up": is_up,
                    "is_enabled": is_enabled,
                    "speed": speed,
                    "mtu": mtu,
                    "last_flapped": -1.0,
                    "description": description,
                    "mac_address": napalm.base.helpers.mac(mac),
                }

                interface = canonical_interface_name(interface, s350_base_interfaces)

                interfaces[interface] = entry

        return interfaces

    def get_interfaces_ip(self):
        """Returns all configured interface IP addresses."""
        interfaces = {}
        show_ip_int = self._send_command("show ip interface")

        header = True  # cycle trought header
        for line in show_ip_int.splitlines():
            if header:
                # last line of first header
                match = re.match(r"^---+ -+ .*$", line)
                if match:
                    header = False
                    fields_end = self._get_ip_int_fields_end(line)
                continue

            # next header, stop processing text
            if re.match(r"^---+ -+ .*$", line):
                break

            line_elems = self._get_ip_int_line_to_fields(line, fields_end)

            # only valid interfaces
            # in diferent firmwares there is 'Status' field allwais on last place
            if line_elems[len(line_elems) - 1] != "Valid":
                continue

            cidr = line_elems[0]
            interface = line_elems[1]

            ip = netaddr.IPNetwork(cidr)
            family = "ipv{0}".format(ip.version)

            interface = canonical_interface_name(interface, s350_base_interfaces)

            interfaces[interface] = {family: {str(ip.ip): {"prefix_length": ip.prefixlen}}}

        return interfaces

    def _get_ip_int_line_to_fields(self, line, fields_end):
        """ dynamic fields lenghts """
        line_elems = {}
        index = 0
        f_start = 0
        for f_end in fields_end:
            line_elems[index] = line[f_start:f_end].strip()
            index += 1
            f_start = f_end
        return line_elems

    def _get_ip_int_fields_end(self, dashline):
        """ fields length are diferent device to device, detect them on horizontal lin """

        fields_end = [m.start() for m in re.finditer(" ", dashline.strip())]
        # fields_position.insert(0,0)
        fields_end.append(len(dashline))

        return fields_end

    def get_lldp_neighbors(self):
        """get_lldp_neighbors implementation for s350"""
        neighbors = {}
        output = self._send_command("show lldp neighbors")

        header = True  # cycle trought header
        local_port = ""  # keep previous context - multiline syname
        remote_port = ""
        remote_name = ""
        for line in output.splitlines():
            if header:
                # last line of header
                match = re.match(r"^--------- -+ .*$", line)
                if match:
                    header = False
                    fields_end = self._get_lldp_neighbors_fields_end(line)
                continue

            line_elems = self._get_lldp_neighbors_line_to_fields(line, fields_end)

            # info owerflow to the other line
            if line_elems[0] == "" or line_elems[4] == "" or line_elems[5] == "":
                # complete owerflown fields
                local_port = local_port + line_elems[0]
                remote_port = remote_port + line_elems[2]
                remote_name = remote_name + line_elems[3]
                # then reuse old values na rewrite previous entry
            else:
                local_port = line_elems[0]
                remote_port = line_elems[2]
                remote_name = line_elems[3]

            local_port = canonical_interface_name(local_port, s350_base_interfaces)

            neighbor = {
                "hostname": remote_name,
                "port": remote_port,
            }
            neighbor_list = [
                neighbor,
            ]
            neighbors[local_port] = neighbor_list

        return neighbors

    def _get_lldp_neighbors_line_to_fields(self, line, fields_end):
        """ dynamic fields lenghts """
        line_elems = {}
        index = 0
        f_start = 0
        for f_end in fields_end:
            line_elems[index] = line[f_start:f_end].strip()
            index += 1
            f_start = f_end
        return line_elems

    def _get_lldp_neighbors_fields_end(self, dashline):
        """ fields length are diferent device to device, detect them on horizontal lin """

        fields_end = [m.start() for m in re.finditer(" ", dashline)]
        fields_end.append(len(dashline))

        return fields_end

    def _get_lldp_line_value(self, line):
        """
        Safe-ish method to get the value from an 'lldp neighbors $IF' line.
        """
        try:
            value = line.split(":")[1:][0].strip()
        except KeyError:
            value = "N/A"

        return value

    def get_lldp_neighbors_detail(self, interface=""):
        """
        get_lldp_neighbors_detail() implementation for s350
        """
        details = {}

        # First determine all interfaces with valid LLDP neighbors
        for local_port in self.get_lldp_neighbors().keys():
            if interface:
                if interface == local_port:
                    entry = self._get_lldp_neighbors_detail_parse(local_port)
                    local_port = canonical_interface_name(local_port, s350_base_interfaces)
                    details[local_port] = [
                        entry,
                    ]

            else:
                entry = self._get_lldp_neighbors_detail_parse(local_port)
                local_port = canonical_interface_name(local_port, s350_base_interfaces)
                details[local_port] = [
                    entry,
                ]

        return details

    def _get_lldp_neighbors_detail_parse(self, local_port):
        # Set defaults, just in case the remote fails to provide a field.
        (
            remote_port_id,
            remote_port_description,
            remote_chassis_id,
            remote_system_name,
            remote_system_description,
            remote_system_capab,
            remote_system_enable_capab,
        ) = ("N/A",) * 7

        output = self._send_command("show lldp neighbors {}".format(local_port))

        for line in output.splitlines():
            if line.startswith("Port ID"):
                remote_port_id = line.split()[-1]
            elif line.startswith("Device ID"):
                remote_chassis_id = line.split()[-1]
            elif line.startswith("Port description"):
                remote_port_description = self._get_lldp_line_value(line)
            elif line.startswith("System Name"):
                remote_system_name = self._get_lldp_line_value(line)
            elif line.startswith("System description"):
                remote_system_description = self._get_lldp_line_value(line)
            elif line.startswith("Capabilities"):
                caps = self._get_lldp_neighbors_detail_capabilities_parse(line)

        remote_port_id = canonical_interface_name(remote_port_id, s350_base_interfaces)

        entry = {
            "parent_interface": "N/A",
            "remote_port": remote_port_id,
            "remote_port_description": remote_port_description,
            "remote_chassis_id": remote_chassis_id,
            "remote_system_name": remote_system_name,
            "remote_system_description": remote_system_description,
            "remote_system_capab": caps,
            "remote_system_enable_capab": caps,
        }

        return entry

    def _get_lldp_neighbors_detail_capabilities_parse(self, line):
        # Only the enabled capabilities are displayed.
        try:
            # Split a line like 'Capabilities: Bridge, Router, Wlan-Access-Point'
            capabilities = line.split(":")[1:][0].split(",")
        except KeyError:
            capabilities = []

        caps = []
        # For all capabilities, except 'Repeater', the shorthand
        # is the first character.
        for cap in capabilities:
            cap = cap.strip()
            if cap == "Repeater":
                caps.append("r")
            else:
                caps.append(cap[0])

        return caps

    def get_ntp_servers(self):
        """Returns NTP servers."""
        ntp_servers = {}
        output = self._send_command("show sntp status")

        servers = re.findall(r"^Server\s*:\s*(\S+)\s*.*$", output, re.M)

        for server in servers:
            ntp_servers[server] = {}

        return ntp_servers

    def is_alive(self):
        """Returns an indication of the state of the connection."""
        null = chr(0)

        if self.device is None:
            return {"is_alive": False}

        # Send a NUL byte to keep the connection alive.
        try:
            self.device.write_channel(null)
            return {"is_alive": self.device.remote_conn.transport.is_active()}
        except (socket.error, EOFError):
            # If we couldn't send it, the connection is not available.
            return {"is_alive": False}

        # If we made it here, assume the worst.
        return {"is_alive": False}

    @property
    def dest_file_system(self):
        # First ensure we have 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
Exemple #3
0
class S350Driver(NetworkDriver):
    """Napalm driver for S350."""
    def __init__(self,
                 hostname,
                 username,
                 password,
                 timeout=60,
                 optional_args=None):
        """Constructor."""
        self.device = None
        self.hostname = hostname
        self.username = username
        self.password = password
        self.timeout = timeout
        self.config_replace = False
        self.candidate_cfg = optional_args.get("candidate_cfg",
                                               "candidate_config.txt")
        self.merge_cfg = optional_args.get("merge_cfg", "merge_config.txt")
        self.rollback_cfg = optional_args.get("rollback_cfg",
                                              "rollback_config.txt")
        self.inline_transfer = optional_args.get("inline_transfer", False)
        self.auto_file_prompt = optional_args.get("auto_file_prompt", True)
        self.prompt_quiet_configured = None

        if optional_args is None:
            optional_args = {}

        self._dest_file_system = optional_args.get('dest_file_system', None)

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

        # Allow for passing additional Netmiko arguments
        self.netmiko_optional_args = {}
        for k, v in netmiko_argument_map.items():
            try:
                self.netmiko_optional_args[k] = optional_args[k]
            except KeyError:
                pass

        self.port = optional_args.get('port', 22)
        self.device = None

    def open(self):
        """Open a connection to the device."""
        self.device = ConnectHandler(device_type='cisco_s300',
                                     host=self.hostname,
                                     username=self.username,
                                     password=self.password,
                                     **self.netmiko_optional_args)
        self.device.enable()

    def _discover_file_system(self):
        try:
            return self.device._autodetect_fs()
        except Exception:
            msg = "Netmiko _autodetect_fs failed (to work around specify " \
                  "dest_file_system in optional_args)."
            raise CommandErrorException(msg)

    def close(self):
        """Close the connection to the device."""
        self.device.disconnect()

    def _send_command(self, command):
        """Wrapper for self.device.send.command().

        If command is a list will iterate through commands until valid command.
        """
        try:
            if isinstance(command, list):
                for cmd in command:
                    output = self.device.send_command(cmd)
                    if "% Invalid" not in output:
                        break
            else:
                output = self.device.send_command(command)
            return output.strip()
        except (socket.error, EOFError) as e:
            raise ConnectionClosedException(str(e))

    def _parse_uptime(self, uptime_str):
        """Parse an uptime string into number of seconds"""
        uptime_str = uptime_str.strip()
        days, timespec = uptime_str.split(',')

        hours, minutes, seconds = timespec.split(':')

        uptime_sec = (int(days) * 86400) + (int(hours) * 3600) + (
            int(minutes) * 60) + int(seconds)
        return uptime_sec

    def get_arp_table(self):
        """
        Get the ARP table, the age isn't readily available so we leave that out for now.
        """
        arp_table = []

        output = self._send_command('show arp | include (static|dynamic)')

        for line in output.splitlines():
            # A VLAN may not be set for the entry
            if len(line.split()) == 4:
                interface, ip, mac, _ = line.split()
            elif len(line.split()) == 5:
                _, interface, mac, _ = line.split()
            else:
                raise ValueError('Unexpected output: {}'.format(line.split()))

            entry = {
                'interface': interface,
                'mac': napalm.base.helpers.mac(mac),
                'ip': ip,
                'age': 0,
            }

            arp_table.append(entry)

        return arp_table

    def get_config(self, retrieve='all'):
        """
        get_config for S350. Since this firmware doesn't support a candidate
        configuration we leave it empty.
        """

        configs = {
            'startup': '',
            'running': '',
            'candidate': '',
        }

        if retrieve in ('all', 'startup'):
            output = self._send_command('show startup-config')
            configs['startup'] = output

        if retrieve in ('all', 'running'):
            output = self._send_command('show running-config')
            configs['running'] = output

        return configs

    def get_facts(self):
        """Return a set of facts from the device."""
        serial_number, fqdn, os_version, hostname, domain_name = (
            'Unknown', ) * 5

        # Submit commands to the device.
        show_ver = self._send_command('show version | include Version')
        show_sys = self._send_command('show system')
        show_inv = self._send_command('show inventory')
        show_hosts = self._send_command(
            'show hosts | begin Default Domain Table')
        show_int_st = self._send_command('show interface status | include gi')

        # os_version
        for line in show_ver.splitlines():
            # First version line is the active version
            if 'Version' in line:
                _, os_version = line.split('Version: ')
                break

        # hostname, uptime
        for line in show_sys.splitlines():
            if line.startswith('System Name:'):
                _, hostname = line.split('System Name:')
                hostname = hostname.strip()
            elif line.startswith('System Description:'):
                _, model = line.split('System Description:')
                model = model.strip()
            elif line.startswith('System Up Time (days,hour:min:sec):'):
                _, uptime_str = line.split(
                    'System Up Time (days,hour:min:sec):')
                uptime = self._parse_uptime(uptime_str)

        # serial_number
        for line in show_inv.splitlines():
            if 'SN:' in line:
                serial_number = line.split('SN: ')[-1]
                break

        # fqdn
        domain_line = show_hosts.splitlines()[4]
        domainname = domain_line.split()[0]
        fqdn = '{0}.{1}'.format(hostname, domainname)

        # interface_list
        interfaces = []
        for line in show_int_st.splitlines():
            interfaces.append(py23_compat.text_type(line.split()[0]))

        return {
            'fqdn': py23_compat.text_type(fqdn),
            'hostname': py23_compat.text_type(hostname),
            'interface_list': interfaces,
            'model': py23_compat.text_type(model),
            'os_version': py23_compat.text_type(os_version),
            'serial_number': py23_compat.text_type(serial_number),
            'uptime': uptime,
            'vendor': u'Cisco',
        }

    def get_interfaces(self):
        """
        get_interfaces() implementation for S350
        """
        interfaces = {}

        show_status_output = self._send_command(
            'show interfaces status | include (Up|Down)')
        show_description_output = self._send_command(
            'show interfaces description')
        # Since the MAC address for all the local ports are equal, get the address
        # from the first port and use it everywhere.
        show_system_output = self._send_command(
            'show lldp local GigabitEthernet1 | begin Device\ ID')

        try:
            mac = show_system_output.splitlines()[0].split(
                ':', maxsplit=1)[1].strip()
        except:
            mac = '0'

        for status_line in show_status_output.splitlines():
            interface, _, _, speed, _, _, link_state, _, _ = status_line.split(
            )

            if speed == '--':
                is_enabled = False
                speed = 0
            else:
                is_enabled = True
                speed = int(speed)

            is_up = (link_state == 'Up')

            for descr_line in show_description_output.splitlines():
                description = 0
                if descr_line.startswith(interface):
                    description = ' '.join(descr_line.split()[1:])
                    break

            entry = {
                'is_up': is_up,
                'is_enabled': is_enabled,
                'speed': speed,
                'last_flapped': -1,
                'description': description,
                'mac_address': napalm.base.helpers.mac(mac)
            }

            interfaces[interface] = entry

        return interfaces

    def get_interfaces_ip(self):
        """Returns all configured interface IP addresses."""
        interfaces = {}
        show_ip_int = self._send_command('show ip int | include (UP|DOWN)')

        # Limit to valid interfaces (i.e. ignore vlan 1)
        valid_interfaces = [
            i for i in show_ip_int.splitlines() if i.split()[-1] == 'Valid'
        ]

        for interface in valid_interfaces:
            network, name = interface.split()[:2]

            ip = netaddr.IPNetwork(network)

            family = 'ipv{0}'.format(ip.version)

            interfaces[name] = {
                family: {
                    str(ip.ip): {
                        'prefix_length': ip.prefixlen
                    }
                }
            }

        return interfaces

    def get_lldp_neighbors(self):
        """get_lldp_neighbors implementation for s350"""
        neighbors = {}
        output = self._send_command('show lldp neighbors | begin \ \ Port')

        for line in output.splitlines()[2:]:
            line_elems = line.split()
            local_port = line_elems[0]
            remote_port = line_elems[2]
            remote_name = line_elems[3]

            neighbors[local_port] = {
                'hostname': remote_name,
                'port': remote_port,
            }

        return neighbors

    def _get_lldp_line_value(self, line):
        """
        Safe-ish method to get the value from an 'lldp neighbors $IF' line.
        """
        try:
            value = line.split(':')[1:][0].strip()
        except:
            value = u'N/A'

        return value

    def get_lldp_neighbors_detail(self):
        """
        get_lldp_neighbors_detail() implementation for s350
        """
        details = {}

        # First determine all interfaces with valid LLDP neighbors
        for local_port in self.get_lldp_neighbors().keys():
            # Set defaults, just in case the remote fails to provide a field.
            remote_port_id, remote_port_description, remote_chassis_id, \
                remote_system_name, remote_system_description, \
                remote_system_capab, remote_system_enable_capab = (u'N/A',) * 7

            output = self._send_command(
                'show lldp neighbor {}'.format(local_port))

            for line in output.splitlines():
                if line.startswith('Port ID'):
                    remote_port_id = line.split()[-1]
                elif line.startswith('Device ID'):
                    remote_chassis_id = line.split()[-1]
                elif line.startswith('Port description'):
                    remote_port_description = self._get_lldp_line_value(line)
                elif line.startswith('System Name'):
                    remote_system_name = self._get_lldp_line_value(line)
                elif line.startswith('System description'):
                    remote_system_description = self._get_lldp_line_value(line)
                elif line.startswith('Capabilities'):
                    # Only the enabled capabilities are displayed.
                    try:
                        # Split a line like 'Capabilities: Bridge, Router, Wlan-Access-Point'
                        capabilities = line.split(':')[1:][0].split(',')
                    except:
                        capabilities = []

                    caps = []
                    # For all capabilities, except 'Repeater', the shorthand
                    # is the first character.
                    for cap in capabilities:
                        cap = cap.strip()
                        if cap == 'Repeater':
                            caps.append('r')
                        else:
                            caps.append(cap[0])

            entry = {
                'parent_interface': u'N/A',
                'remote_port': remote_port_id,
                'remote_port_description': remote_port_description,
                'remote_chassis_id': remote_chassis_id,
                'remote_system_name': remote_system_name,
                'remote_system_description': remote_system_description,
                'remote_system_capab': u', '.join(caps),
                'remote_system_enable_capab': u', '.join(caps),
            }

            details[local_port] = entry

        return details

    def get_ntp_servers(self):
        """get_ntp_servers implementation for S350"""
        ntp_servers = {}
        output = self._send_command('show sntp status | include Server')

        for line in output.splitlines():
            ntp_servers[line.split()[2]] = {}

        return ntp_servers

    def is_alive(self):
        """Returns an indication of the state of the connection."""
        null = chr(0)

        if self.device is None:
            return {'is_alive': False}

        # Send a NUL byte to keep the connection alive.
        try:
            self.device.write_channel(null)
            return {'is_alive': self.device.remote_conn.transport.is_active()}
        except (socket.error, EOFError):
            # If we couldn't send it, the connection is not available.
            return {'is_alive': False}

        # If we made it here, assume the worst.
        return {'is_alive': False}

    @property
    def dest_file_system(self):
        # First ensure we have 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

    def cli(self, commands):
        """
        Execute a list of commands and return the output in a dictionary format using the command
        as the key.

        Example input:
        ['show clock', 'show calendar']

        Output example:
        {   'show calendar': u'22:02:01 UTC Thu Feb 18 2016',
            'show clock': u'*22:01:51.165 UTC Thu Feb 18 2016'}

        """
        cli_output = dict()
        if type(commands) is not list:
            raise TypeError("Please enter a valid list of commands!")

        for command in commands:
            output = self._send_command(command)
            cli_output.setdefault(command, {})
            cli_output[command] = output

        return cli_output
Exemple #4
0
class SG350XDriver(NetworkDriver):
    """Napalm driver for SG350X."""
    def __init__(self,
                 hostname,
                 username,
                 password,
                 timeout=60,
                 optional_args=None):
        """Constructor."""
        self.device = None
        self.hostname = hostname
        self.username = username
        self.password = password
        self.timeout = timeout

        if optional_args is None:
            optional_args = {}

        self._dest_file_system = optional_args.get("dest_file_system", None)

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

        # Allow for passing additional Netmiko arguments
        self.netmiko_optional_args = {}
        for k, v in netmiko_argument_map.items():
            try:
                self.netmiko_optional_args[k] = optional_args[k]
            except KeyError:
                pass

        self.port = optional_args.get("port", 22)
        self.device = None

    def open(self):
        """Open a connection to the device."""
        self.device = ConnectHandler(
            device_type="cisco_SG350X",
            host=self.hostname,
            username=self.username,
            password=self.password,
            **self.netmiko_optional_args,
        )
        self.device.enable()

    def _discover_file_system(self):
        try:
            return self.device._autodetect_fs()
        except Exception:
            msg = ("Netmiko _autodetect_fs failed (to work around specify "
                   "dest_file_system in optional_args).")
            raise CommandErrorException(msg)

    def close(self):
        """Close the connection to the device."""
        self.device.disconnect()

    def _send_command(self, command):
        """Wrapper for self.device.send.command().

        If command is a list will iterate through commands until valid command.
        """
        try:
            if isinstance(command, list):
                for cmd in command:
                    output = self.device.send_command(cmd)
                    if "% Invalid" not in output:
                        break
            else:
                output = self.device.send_command(command)
            return output.strip()
        except (socket.error, EOFError) as e:
            raise ConnectionClosedException(str(e))

    def _parse_uptime(self, uptime_str):
        """Parse an uptime string into number of seconds"""
        uptime_str = uptime_str.strip()
        days, timespec = uptime_str.split(",")

        hours, minutes, seconds = timespec.split(":")

        uptime_sec = ((int(days) * 86400) + (int(hours) * 3600) +
                      (int(minutes) * 60) + int(seconds))
        return uptime_sec

    def get_arp_table(self):
        """
        Get the ARP table, the age isn't readily available so we leave that out for now.
        """
        arp_table = []

        output = self._send_command("show arp | include (static|dynamic)")

        for line in output.splitlines():

            if line.startswith("vlan"):
                if len(line.split()) == 6:
                    # vlan, vlan id, interface, ip, mac, staus
                    _, _, interface, ip, mac, _ = line.split()
                elif len(len.split() == 5):
                    _, _, ip, mac, _ = line.split()
                    interface = ""
                else:
                    raise ValueError(f"Unexpected output: {line.split()}")

            entry = {
                "interface": interface,
                "mac": napalm.base.helpers.mac(mac),
                "ip": ip,
                "age": 0,
            }

            arp_table.append(entry)

        return arp_table

    def get_config(self, retrieve="all"):
        """
        get_config for SG350X. Since this firmware doesn't support a candidate
        configuration we leave it empty.
        """

        configs = {
            "startup": "",
            "running": "",
            "candidate": "",
        }

        if retrieve in ("all", "startup"):
            output = self._send_command("show startup-config")
            configs["startup"] = output

        if retrieve in ("all", "running"):
            output = self._send_command("show running-config")
            configs["running"] = output

        return configs

    def get_enviroment(self):
        """
        Return a dictionary of enviroment factsself.

        cpu is using 1-minute average
        cpu hard coded to cpu0 (i.e. only a single CPU)
        fans status
        memory - Not easily obtained expensive to run show tech-support memory
                 values are set to
        """
        enviroment.setdefault("cpu", {})
        enviroment["cpu"][0] = {}
        enviroment["cpu"][0]["%usage"] = 0.0
        show_fans = self._send_command("show system fans")
        show_cpu_util = self._send_command("show cpu utilization")
        show_power_inline = self._send_command("show power inline | include %")
        show_system_sensors = self._send_command("show system sensors")

        for line in show_cpu_util.splitlines():
            if line.startswith("five seconds:"):
                cpu_regex = r"one minute: (\d+)%"
                match = re.search(cpu_regex, line)
                enviroment["cpu"][0]["%usage"] = float(match.group(1))
                break
        enviroment["fans"] = {}
        fan_regex = r"(\d+/\d+)\s+\d+\s+(\S+)"
        for match in re.finditer(fan):
            if match.group(2) == "OK":
                eviroment["fans"][match.group(1)]["status"] = True
            else:
                enviroment["fans"][match.group(1)]["status"] = False

        for line in show_power_inline.splitlines():
            if line.startwith("Usage threshold:"):
                threshold = float(line.split(":")[-1].strip("%"))

        show_power_inline_data = napalm.base.helpers.textfsm_extractor(
            self, "powerinline", show_power_inline)
        enviroment.setdefault("power", {})
        for item in show_power_inline_data:
            enviroment["power"][int(item["requiredid"])] = {
                "status": item["consumedpowerpercent"] < item["threshold"],
                "capacity": item["nominalpower"],
                "output": item["consumedpower"],
            }

        show_system_sensors_data = napalm.base.helpers.textfsm_extractor(
            self, "sensor", show_system_sensors)
        enviroment.setdefault("temperature", {})
        for item in show_system_sensors_data:
            enviroment["temperature"][item["sensor"]] = {
                "temperature": item["temperature"],
                "is_alert": item["temperature"] > item["warntemperature"],
                "is_critical": item["temperature"] > item["crittemperature"],
            }

        return enviroment

    def get_facts(self):
        """Return a set of facts from the device."""
        serial_number, fqdn, os_version, hostname, domain_name = (
            "Unknown", ) * 5

        # Submit commands to the device.
        show_ver = self._send_command("show version")
        show_sys = self._send_command("show system")
        show_inv = self._send_command("show inventory")
        show_hosts = self._send_command("show hosts")
        show_int_st = self._send_command("show interface status")

        # os_version
        for line in show_ver.splitlines():
            # First version line is the active version
            if "Version" in line:
                _, os_version = line.split("Version: ")
                break

        # hostname, uptime
        for line in show_sys.splitlines():
            if line.startswith("System Name:"):
                _, hostname = line.split("System Name:")
                hostname = hostname.strip()
                continue
            elif line.startswith("System Description:"):
                _, model = line.split("System Description:")
                model = model.strip()
                continue
            elif line.startswith("System Up Time (days,hour:min:sec):"):
                _, uptime_str = line.split(
                    "System Up Time (days,hour:min:sec):")
                uptime = self._parse_uptime(uptime_str)

        # serial_number
        for line in show_inv.splitlines():
            if "SN:" in line:
                serial_number = line.split("SN: ")[-1]
                break

        # fqdn
        domainname = napalm.base.helpers.textfsm_extractor(
            self, "hosts", show_hosts)[0]
        domainname = domainname["domain_name"]
        if domainname == "Domain" or domainname == "Name":
            domainname = ""
        fqdn = f"{hostname}.{domainname}"

        # interface_list
        interfaces = []
        show_int_st = show_int_st.strip()
        # remove the header information
        show_int_st = re.sub(r"(^-.*$|^Port .*$|^Ch .*$)|^\s.*$|^.*Flow.*$",
                             "",
                             show_int_st,
                             flags=re.M)
        for line in show_int_st.splitlines():
            if not line:
                continue
            interface = line.split()[0]
            interfaces.append(str(interface))

        return {
            "fqdn": str(fqdn),
            "hostname": str(hostname),
            "interface_list": interfaces,
            "model": str(model),
            "os_version": str(os_version),
            "serial_number": str(serial_number),
            "uptime": uptime,
            "vendor": "Cisco",
        }

    def get_interfaces(self):
        """
        get_interfaces() implementation for S350
        """
        interfaces = {}

        show_status_output = self._send_command(
            "show interfaces status | include (Up|Down)")
        show_description_output = self._send_command(
            "show interfaces description")
        # Since the MAC address for all the local ports are equal, get the address
        # from the first port and use it everywhere.
        show_system_output = self._send_command(
            "show lldp local g1/0/1 | begin Device\ ID")

        try:
            mac = show_system_output.splitlines()[0].split(
                ":", maxsplit=1)[1].strip()
        except:
            mac = "0"

        for status_line in show_status_output.splitlines():
            interface, _, _, speed, _, _, link_state, _, _ = status_line.split(
            )

            if speed == "--":
                is_enabled = False
                speed = 0
            else:
                is_enabled = True
                speed = int(speed)

            is_up = link_state == "Up"

            for descr_line in show_description_output.splitlines():
                description = 0
                if descr_line.startswith(interface):
                    description = " ".join(descr_line.split()[1:])
                    break

            entry = {
                "is_up": is_up,
                "is_enabled": is_enabled,
                "speed": speed,
                "last_flapped": -1,
                "description": description,
                "mac_address": napalm.base.helpers.mac(mac),
            }

            interfaces[interface] = entry

        return interfaces

    def get_interfaces_ip(self):
        """Returns all configured interface IP addresses."""
        interfaces = {}
        show_ip_int = self._send_command("show ip int | include (UP|DOWN)")

        # Limit to valid interfaces (i.e. ignore vlan 1)
        valid_interfaces = [
            i for i in show_ip_int.splitlines() if i.split()[-1] == "Valid"
        ]

        for interface in valid_interfaces:
            network, _, name = interface.split()[:3]

            ip = netaddr.IPNetwork(network)

            family = f"ipv{ip.version}"

            interfaces[name] = {
                family: {
                    str(ip.ip): {
                        "prefix_length": ip.prefixlen
                    }
                }
            }

        return interfaces

    def get_lldp_neighbors(self):
        """get_lldp_neighbors implementation for s350"""
        neighbors = {}
        output = self._send_command("show lldp neighbors | begin \ \ Port")

        for line in output.splitlines()[2:]:
            line_elems = line.split()
            local_port = line_elems[0]
            remote_port = line_elems[2]
            remote_name = line_elems[3]

            neighbors[local_port] = {
                "hostname": remote_name,
                "port": remote_port,
            }

        return neighbors

    def _get_lldp_line_value(self, line):
        """
        Safe-ish method to get the value from an 'lldp neighbors $IF' line.
        """
        try:
            value = line.split(":")[1:][0].strip()
        except:
            value = "N/A"

        return value

    def get_lldp_neighbors_detail(self):
        """
        get_lldp_neighbors_detail() implementation for s350
        """
        details = {}

        # First determine all interfaces with valid LLDP neighbors
        for local_port in self.get_lldp_neighbors().keys():
            # Set defaults, just in case the remote fails to provide a field.
            (
                remote_port_id,
                remote_port_description,
                remote_chassis_id,
                remote_system_name,
                remote_system_description,
                remote_system_capab,
                remote_system_enable_capab,
            ) = ("N/A", ) * 7

            output = self._send_command(
                "show lldp neighbor {}".format(local_port))

            for line in output.splitlines():
                if line.startswith("Port ID"):
                    remote_port_id = line.split()[-1]
                elif line.startswith("Device ID"):
                    remote_chassis_id = line.split()[-1]
                elif line.startswith("Port description"):
                    remote_port_description = self._get_lldp_line_value(line)
                elif line.startswith("System Name"):
                    remote_system_name = self._get_lldp_line_value(line)
                elif line.startswith("System description"):
                    remote_system_description = self._get_lldp_line_value(line)
                elif line.startswith("Capabilities"):
                    # Only the enabled capabilities are displayed.
                    try:
                        # Split a line like 'Capabilities: Bridge, Router, Wlan-Access-Point'
                        capabilities = line.split(":")[1:][0].split(",")
                    except:
                        capabilities = []

                    caps = []
                    # For all capabilities, except 'Repeater', the shorthand
                    # is the first character.
                    for cap in capabilities:
                        cap = cap.strip()
                        if cap == "Repeater":
                            caps.append("r")
                        else:
                            caps.append(cap[0])

            entry = {
                "parent_interface": "N/A",
                "remote_port": remote_port_id,
                "remote_port_description": remote_port_description,
                "remote_chassis_id": remote_chassis_id,
                "remote_system_name": remote_system_name,
                "remote_system_description": remote_system_description,
                "remote_system_capab": ", ".join(caps),
                "remote_system_enable_capab": ", ".join(caps),
            }

            details[local_port] = entry

        return details

    def get_ntp_servers(self):
        """get_ntp_servers implementation for S350"""
        ntp_servers = {}
        output = self._send_command("show sntp status | include Server")

        for line in output.splitlines():
            ntp_servers[line.split()[2]] = {}

        return ntp_servers

    def get_vlans(self):
        """get_vlans implementation for SG350X"""
        output = self._send_command("show vlan")
        vlans = {}
        vlan = None
        re_min_max = r"(\d+)-(\d+)"
        # Output has fixed width columns with cells wrapping
        # vlan num, vlan name, Tagged Ports, UnTagged Ports, created by
        # skip header
        for row in output.splitlines()[4:]:
            t_ports = row[22:41].strip()
            u_ports = row[41:60].strip()
            status = row[60:].strip()
            # Indicates a wrapped cell
            if row[:5].strip() == "":
                vlans[vlan]["name"] = " ".join(
                    (vlans[vlan]["name"], row[5:22].strip()))
                vlans[vlan]["name"] = vlans[vlan]["name"].strip()
            else:
                vlan = int(row[:5])
                name = row[5:22].strip()
                vlans[vlan] = {"name": name, "interfaces": []}
            for item in t_ports.split(","):
                if item != "":
                    if "-" in item:
                        result = re.search(re_min_max, item)
                        item = item.replace(result.group(0), "")
                        for x in range(int(result.group(1)),
                                       int(result.group(2)) + 1):
                            vlans[vlan]["interfaces"].append(f"{item}{x}")
                    else:
                        vlans[vlan]["interfaces"].append(item)
            for item in u_ports.split(","):
                if item != "":
                    if "-" in item:
                        result = re.search(re_min_max, item)
                        item = item.replace(result.group(0), "")
                        for x in range(int(result.group(1)),
                                       int(result.group(2)) + 1):
                            vlans[vlan]["interfaces"].append(f"{item}{x}")
                    else:
                        vlans[vlan]["interfaces"].append(item)

        return vlans

    def is_alive(self):
        """Returns an indication of the state of the connection."""
        null = chr(0)

        if self.device is None:
            return {"is_alive": False}

        # Send a NUL byte to keep the connection alive.
        try:
            self.device.write_channel(null)
            return {"is_alive": self.device.remote_conn.transport.is_active()}
        except (socket.error, EOFError):
            # If we couldn't send it, the connection is not available.
            return {"is_alive": False}

        # If we made it here, assume the worst.
        return {"is_alive": False}

    @property
    def dest_file_system(self):
        # First ensure we have 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