<commit_before># Copyright 2015 Spotify AB. All rights reserved.
#
# The contents of this file are licensed under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.

"""
Napalm driver for Arista EOS.

Read napalm.readthedocs.org for more information.
"""
from __future__ import print_function
from __future__ import unicode_literals

# std libs
import re
import time

from datetime import datetime
from collections import defaultdict
from netaddr import IPAddress
from netaddr import IPNetwork

from netaddr.core import AddrFormatError

# third party libs
import pyeapi
from pyeapi.eapilib import ConnectionError

# NAPALM base
import napalm_base.helpers
from napalm_base.base import NetworkDriver
from napalm_base.utils import string_parsers
from napalm_base.utils import py23_compat
from napalm_base.exceptions import ConnectionException, MergeConfigException, \
                        ReplaceConfigException, SessionLockedException, CommandErrorException

import napalm_base.constants as c
# local modules
# here add local imports
# e.g. import napalm_eos.helpers etc.


class EOSDriver(NetworkDriver):
    """Napalm driver for Arista EOS."""

    SUPPORTED_OC_MODELS = []

    _RE_BGP_INFO = re.compile('BGP neighbor is (?P<neighbor>.*?), remote AS (?P<as>.*?), .*') # noqa
    _RE_BGP_RID_INFO = re.compile('.*BGP version 4, remote router ID (?P<rid>.*?), VRF (?P<vrf>.*?)$') # noqa
    _RE_BGP_DESC = re.compile('\s+Description: (?P<description>.*?)')
    _RE_BGP_LOCAL = re.compile('Local AS is (?P<as>.*?),.*')
    _RE_BGP_PREFIX = re.compile('(\s*?)(?P<af>IPv[46]) Unicast:\s*(?P<sent>\d+)\s*(?P<received>\d+)') # noqa
    _RE_SNMP_COMM = re.compile(r"""^snmp-server\s+community\s+(?P<community>\S+)
                                (\s+view\s+(?P<view>\S+))?(\s+(?P<access>ro|rw)?)
                                (\s+ipv6\s+(?P<v6_acl>\S+))?(\s+(?P<v4_acl>\S+))?$""", re.VERBOSE)

    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_session = None

        if optional_args is None:
            optional_args = {}

        self.transport = optional_args.get('eos_transport', 'https')

        if self.transport == 'https':
            self.port = optional_args.get('port', 443)
        elif self.transport == 'http':
            self.port = optional_args.get('port', 80)

        self.enablepwd = optional_args.get('enable_password', '')

    def open(self):
        """Implementation of NAPALM method open."""
        try:
            if self.transport in ('http', 'https'):
                connection = pyeapi.client.connect(
                    transport=self.transport,
                    host=self.hostname,
                    username=self.username,
                    password=self.password,
                    port=self.port,
                    timeout=self.timeout
                )
            elif self.transport == 'socket':
                connection = pyeapi.client.connect(transport=self.transport)
            else:
                raise ConnectionException("Unknown transport: {}".format(self.transport))

            if self.device is None:
                self.device = pyeapi.client.Node(connection, enablepwd=self.enablepwd)
            # does not raise an Exception if unusable

            # let's try to run a very simple command
            self.device.run_commands(['show clock'], encoding='text')
        except ConnectionError as ce:
            # and this is raised either if device not avaiable
            # either if HTTP(S) agent is not enabled
            # show management api http-commands
            raise ConnectionException(ce.message)

    def close(self):
        """Implementation of NAPALM method close."""
        self.discard_config()

    def is_alive(self):
        return {
            'is_alive': True  # always true as eAPI is HTTP-based
        }

    def _load_config(self, filename=None, config=None, replace=True):
        if self.config_session is not None:
            raise SessionLockedException('Session is already in use by napalm')
        else:
            self.config_session = 'napalm_{}'.format(datetime.now().microsecond)

        commands = []
        commands.append('configure session {}'.format(self.config_session))

        if replace:
            commands.append('rollback clean-config')

        if filename is not None:
            with open(filename, 'r') as f:
                lines = f.readlines()
        else:
            if isinstance(config, list):
                lines = config
            else:
                lines = config.splitlines()

        for line in lines:
            line = line.strip()
            if line == '':
                continue
            if line.startswith('!'):
                continue
            commands.append(line)

        try:
            self.device.run_commands(commands)
        except pyeapi.eapilib.CommandError as e:
            self.discard_config()

            if replace:
                raise ReplaceConfigException(e.message)
            else:
                raise MergeConfigException(e.message)

    def load_replace_candidate(self, filename=None, config=None):
        """Implementation of NAPALM method load_replace_candidate."""
        self._load_config(filename, config, True)

    def load_merge_candidate(self, filename=None, config=None):
        """Implementation of NAPALM method load_merge_candidate."""
        self._load_config(filename, config, False)

    def compare_config(self):
        """Implementation of NAPALM method compare_config."""
        if self.config_session is None:
            return ''
        else:
            commands = ['show session-config named %s diffs' % self.config_session]
            result = self.device.run_commands(commands, encoding='text')[0]['output']

            result = '\n'.join(result.splitlines()[2:])

            return result.strip()

    def commit_config(self):
        """Implementation of NAPALM method commit_config."""
        commands = []
        commands.append('copy startup-config flash:rollback-0')
        commands.append('configure session {}'.format(self.config_session))
        commands.append('commit')
        commands.append('write memory')

        self.device.run_commands(commands)
        self.config_session = None

    def discard_config(self):
        """Implementation of NAPALM method discard_config."""
        if self.config_session is not None:
            commands = []
            commands.append('configure session {}'.format(self.config_session))
            commands.append('abort')
            self.device.run_commands(commands)
            self.config_session = None

    def rollback(self):
        """Implementation of NAPALM method rollback."""
        commands = []
        commands.append('configure replace flash:rollback-0')
        commands.append('write memory')
        self.device.run_commands(commands)

    def get_facts(self):
        """Implementation of NAPALM method get_facts."""
        commands = []
        commands.append('show version')
        commands.append('show hostname')
        commands.append('show interfaces')

        result = self.device.run_commands(commands)

        version = result[0]
        hostname = result[1]
        interfaces_dict = result[2]['interfaces']

        uptime = time.time() - version['bootupTimestamp']

        interfaces = [i for i in interfaces_dict.keys() if '.' not in i]
        interfaces = string_parsers.sorted_nicely(interfaces)

        return {
            'hostname': hostname['hostname'],
            'fqdn': hostname['fqdn'],
            'vendor': u'Arista',
            'model': version['modelName'],
            'serial_number': version['serialNumber'],
            'os_version': version['internalVersion'],
            'uptime': int(uptime),
            'interface_list': interfaces,
        }

    def get_interfaces(self):
        commands = []
        commands.append('show interfaces')
        output = self.device.run_commands(commands)[0]

        interfaces = {}

        for interface, values in output['interfaces'].items():
            interfaces[interface] = {}

            if values['lineProtocolStatus'] == 'up':
                interfaces[interface]['is_up'] = True
                interfaces[interface]['is_enabled'] = True
            else:
                interfaces[interface]['is_up'] = False
                if values['interfaceStatus'] == 'disabled':
                    interfaces[interface]['is_enabled'] = False
                else:
                    interfaces[interface]['is_enabled'] = True

            interfaces[interface]['description'] = values['description']

            interfaces[interface]['last_flapped'] = values.pop('lastStatusChangeTimestamp', None)

            interfaces[interface]['speed'] = int(values['bandwidth'] * 1e-6)
            interfaces[interface]['mac_address'] = napalm_base.helpers.convert(
                napalm_base.helpers.mac, values.pop('physicalAddress', u''))

        return interfaces

    def get_lldp_neighbors(self):
        commands = []
        commands.append('show lldp neighbors')
        output = self.device.run_commands(commands)[0]['lldpNeighbors']

        lldp = {}

        for n in output:
            if n['port'] not in lldp.keys():
                lldp[n['port']] = []

            lldp[n['port']].append(
                {
                    'hostname': n['neighborDevice'],
                    'port': n['neighborPort'],
                }
            )

        return lldp

    def get_interfaces_counters(self):
        commands = ['show interfaces']
        output = self.device.run_commands(commands)
        interface_counters = defaultdict(dict)
        for interface, data in output[0]['interfaces'].items():
            if data['hardware'] == 'subinterface':
                # Subinterfaces will never have counters so no point in parsing them at all
                continue
            counters = data.get('interfaceCounters', {})
            interface_counters[interface].update(
                tx_octets=counters.get('outOctets', -1),
                rx_octets=counters.get('inOctets', -1),
                tx_unicast_packets=counters.get('outUcastPkts', -1),
                rx_unicast_packets=counters.get('inUcastPkts', -1),
                tx_multicast_packets=counters.get('outMulticastPkts', -1),
                rx_multicast_packets=counters.get('inMulticastPkts', -1),
                tx_broadcast_packets=counters.get('outBroadcastPkts', -1),
                rx_broadcast_packets=counters.get('inBroadcastPkts', -1),
                tx_discards=counters.get('outDiscards', -1),
                rx_discards=counters.get('inDiscards', -1),
                tx_errors=counters.get('totalOutErrors', -1),
                rx_errors=counters.get('totalInErrors', -1)
            )
        return interface_counters

    def get_bgp_neighbors(self):

        def get_re_group(res, key, default=None):
            """ Small helper to retrive data from re match groups"""
            try:
                return res.group(key)
            except KeyError:
                return default

        NEIGHBOR_FILTER = 'bgp neighbors vrf all | include remote AS | remote router ID |IPv[46] Unicast:.*[0-9]+|^Local AS|Desc|BGP state'  # noqa
        output_summary_cmds = self.device.run_commands(
            ['show ipv6 bgp summary vrf all', 'show ip bgp summary vrf all'],
            encoding='json')
        output_neighbor_cmds = self.device.run_commands(
            ['show ip ' + NEIGHBOR_FILTER, 'show ipv6 ' + NEIGHBOR_FILTER],
            encoding='text')

        bgp_counters = defaultdict(lambda: dict(peers={}))
        for summary in output_summary_cmds:
            """
            Json output looks as follows
            "vrfs": {
                "default": {
                    "routerId": 1,
                    "asn": 1,
                    "peers": {
                        "1.1.1.1": {
                            "msgSent": 1,
                            "inMsgQueue": 0,
                            "prefixReceived": 3926,
                            "upDownTime": 1449501378.418644,
                            "version": 4,
                            "msgReceived": 59616,
                            "prefixAccepted": 3926,
                            "peerState": "Established",
                            "outMsgQueue": 0,
                            "underMaintenance": false,
                            "asn": 1
                        }
                    }
                }
            }
            """
            for vrf, vrf_data in summary['vrfs'].items():
                bgp_counters[vrf]['router_id'] = vrf_data['routerId']
                for peer, peer_data in vrf_data['peers'].items():
                    if peer_data['peerState'] == 'Idle':
                        is_enabled = True if peer_data['peerStateIdleReason'] != 'Admin' else False
                    else:
                        is_enabled = True
                    peer_info = {
                        'is_up': peer_data['peerState'] == 'Established',
                        'is_enabled': is_enabled,
                        'uptime': int(time.time() - peer_data['upDownTime'])
                    }
                    bgp_counters[vrf]['peers'][napalm_base.helpers.ip(peer)] = peer_info
        lines = []
        [lines.extend(x['output'].splitlines()) for x in output_neighbor_cmds]
        while lines:
            """
            Raw output from the command looks like the following:

              BGP neighbor is 1.1.1.1, remote AS 1, external link
                Description: Very info such descriptive
                BGP version 4, remote router ID 1.1.1.1, VRF my_vrf
                BGP state is Idle, Administratively shut down
                 IPv4 Unicast:         683        78
                 IPv6 Unicast:           0         0
              Local AS is 2, local router ID 2.2.2.2
            """
            neighbor_info = re.match(self._RE_BGP_INFO, lines.pop(0))
            # this line can be either description or rid info
            next_line = lines.pop(0)
            desc = re.match(self._RE_BGP_DESC, next_line)
            if desc is None:
                rid_info = re.match(self._RE_BGP_RID_INFO, next_line)
                desc = ''
            else:
                rid_info = re.match(self._RE_BGP_RID_INFO, lines.pop(0))
                desc = desc.group('description')
            lines.pop(0)
            v4_stats = re.match(self._RE_BGP_PREFIX, lines.pop(0))
            v6_stats = re.match(self._RE_BGP_PREFIX, lines.pop(0))
            local_as = re.match(self._RE_BGP_LOCAL, lines.pop(0))
            data = {
                'remote_as': int(neighbor_info.group('as')),
                'remote_id': napalm_base.helpers.ip(get_re_group(rid_info, 'rid', '0.0.0.0')),
                'local_as': int(local_as.group('as')),
                'description': py23_compat.text_type(desc),
                'address_family': {
                    'ipv4': {
                        'sent_prefixes': int(get_re_group(v4_stats, 'sent', -1)),
                        'received_prefixes': int(get_re_group(v4_stats, 'received', -1)),
                        'accepted_prefixes': -1
                    },
                    'ipv6': {
                        'sent_prefixes': int(get_re_group(v6_stats, 'sent', -1)),
                        'received_prefixes': int(get_re_group(v6_stats, 'received', -1)),
                        'accepted_prefixes': -1
                    }
                }
            }
            peer_addr = napalm_base.helpers.ip(neighbor_info.group('neighbor'))
            vrf = rid_info.group('vrf')
            if peer_addr not in bgp_counters[vrf]['peers']:
                bgp_counters[vrf]['peers'][peer_addr] = {
                    'is_up': False,  # if not found, means it was not found in the oper stats
                                     # i.e. neighbor down,
                    'uptime': 0,
                    'is_enabled': True
                }
            bgp_counters[vrf]['peers'][peer_addr].update(data)
        if 'default' in bgp_counters:
            bgp_counters['global'] = bgp_counters.pop('default')
        return dict(bgp_counters)

    def get_environment(self):
        def extract_temperature_data(data):
            for s in data:
                temp = s['currentTemperature'] if 'currentTemperature' in s else 0.0
                name = s['name']
                values = {
                   'temperature': temp,
                   'is_alert': temp > s['overheatThreshold'],
                   'is_critical': temp > s['criticalThreshold']
                }
                yield name, values

        sh_version_out = self.device.run_commands(['show version'])
        is_veos = sh_version_out[0]['modelName'].lower() == 'veos'
        commands = [
            'show environment cooling',
            'show environment temperature'
        ]
        if not is_veos:
            commands.append('show environment power')
            fans_output, temp_output, power_output = self.device.run_commands(commands)
        else:
            fans_output, temp_output = self.device.run_commands(commands)
        environment_counters = {
            'fans': {},
            'temperature': {},
            'power': {},
            'cpu': {}
        }
        cpu_output = self.device.run_commands(['show processes top once'],
                                              encoding='text')[0]['output']
        for slot in fans_output['fanTraySlots']:
            environment_counters['fans'][slot['label']] = {'status': slot['status'] == 'ok'}
        # First check FRU's
        for fru_type in ['cardSlots', 'powerSupplySlots']:
            for fru in temp_output[fru_type]:
                t = {name: value for name, value in extract_temperature_data(fru['tempSensors'])}
                environment_counters['temperature'].update(t)
        # On board sensors
        parsed = {n: v for n, v in extract_temperature_data(temp_output['tempSensors'])}
        environment_counters['temperature'].update(parsed)
        if not is_veos:
            for psu, data in power_output['powerSupplies'].items():
                environment_counters['power'][psu] = {
                    'status': data['state'] == 'ok',
                    'capacity': data['capacity'],
                    'output': data['outputPower']
                }
        cpu_lines = cpu_output.splitlines()
        # Matches either of
        # Cpu(s):  5.2%us,  1.4%sy,  0.0%ni, 92.2%id,  0.6%wa,  0.3%hi,  0.4%si,  0.0%st ( 4.16 > )
        # %Cpu(s):  4.2 us,  0.9 sy,  0.0 ni, 94.6 id,  0.0 wa,  0.1 hi,  0.2 si,  0.0 st ( 4.16 < )
        m = re.match('.*ni, (?P<idle>.*).id.*', cpu_lines[2])
        environment_counters['cpu'][0] = {
            '%usage': round(100 - float(m.group('idle')), 1)
        }
        # Matches either of
        # Mem:   3844356k total,  3763184k used,    81172k free,    16732k buffers ( 4.16 > )
        # KiB Mem:  32472080 total,  5697604 used, 26774476 free,   372052 buffers ( 4.16 < )
        mem_regex = '.*total,\s+(?P<used>\d+)[k\s]+used,\s+(?P<free>\d+)[k\s]+free,.*'
        m = re.match(mem_regex, cpu_lines[3])
        environment_counters['memory'] = {
            'available_ram': int(m.group('free')),
            'used_ram': int(m.group('used'))
        }
        return environment_counters

    def get_lldp_neighbors_detail(self, interface=''):

        lldp_neighbors_out = {}

        filters = []
        if interface:
            filters.append(interface)

        commands = []
        commands.append(
            'show lldp neighbors {filters} detail'.format(
                filters=' '.join(filters)
            )
        )

        lldp_neighbors_in = {}
        lldp_neighbors_in = self.device.run_commands(commands)[0].get('lldpNeighbors', {})

        for interface in lldp_neighbors_in:
            interface_neighbors = lldp_neighbors_in.get(interface).get('lldpNeighborInfo', {})
            if not interface_neighbors:
                # in case of empty infos
                continue

            # it is provided a list of neighbors per interface
            for neighbor in interface_neighbors:
                if interface not in lldp_neighbors_out.keys():
                    lldp_neighbors_out[interface] = []
                capabilities = neighbor.get('systemCapabilities')
                capabilities_list = list(capabilities.keys())
                capabilities_list.sort()
                lldp_neighbors_out[interface].append(
                    {
                        'parent_interface': interface,  # no parent interfaces
                        'remote_port':
                            neighbor.get('neighborInterfaceInfo', {}).get('interfaceId', u''),
                        'remote_port_description': u'',
                        'remote_system_name': neighbor.get('systemName', u''),
                        'remote_system_description': neighbor.get('systemDescription', u''),
                        'remote_chassis_id': napalm_base.helpers.mac(
                            neighbor.get('chassisId', u'')),
                        'remote_system_capab': py23_compat.text_type(', '.join(capabilities_list)),
                        'remote_system_enable_capab': py23_compat.text_type(', '.join(
                            [capability for capability in capabilities_list
                             if capabilities[capability]]))
                    }
                )<commit_msg>Check if LLDP chassisId is a MAC address before trying to convert it to a MAC address.<commit_after># Copyright 2015 Spotify AB. All rights reserved.
#
# The contents of this file are licensed under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.

"""
Napalm driver for Arista EOS.

Read napalm.readthedocs.org for more information.
"""
from __future__ import print_function
from __future__ import unicode_literals

# std libs
import re
import time

from datetime import datetime
from collections import defaultdict
from netaddr import IPAddress
from netaddr import IPNetwork

from netaddr.core import AddrFormatError

# third party libs
import pyeapi
from pyeapi.eapilib import ConnectionError

# NAPALM base
import napalm_base.helpers
from napalm_base.base import NetworkDriver
from napalm_base.utils import string_parsers
from napalm_base.utils import py23_compat
from napalm_base.exceptions import ConnectionException, MergeConfigException, \
                        ReplaceConfigException, SessionLockedException, CommandErrorException

import napalm_base.constants as c
# local modules
# here add local imports
# e.g. import napalm_eos.helpers etc.


class EOSDriver(NetworkDriver):
    """Napalm driver for Arista EOS."""

    SUPPORTED_OC_MODELS = []

    _RE_BGP_INFO = re.compile('BGP neighbor is (?P<neighbor>.*?), remote AS (?P<as>.*?), .*') # noqa
    _RE_BGP_RID_INFO = re.compile('.*BGP version 4, remote router ID (?P<rid>.*?), VRF (?P<vrf>.*?)$') # noqa
    _RE_BGP_DESC = re.compile('\s+Description: (?P<description>.*?)')
    _RE_BGP_LOCAL = re.compile('Local AS is (?P<as>.*?),.*')
    _RE_BGP_PREFIX = re.compile('(\s*?)(?P<af>IPv[46]) Unicast:\s*(?P<sent>\d+)\s*(?P<received>\d+)') # noqa
    _RE_SNMP_COMM = re.compile(r"""^snmp-server\s+community\s+(?P<community>\S+)
                                (\s+view\s+(?P<view>\S+))?(\s+(?P<access>ro|rw)?)
                                (\s+ipv6\s+(?P<v6_acl>\S+))?(\s+(?P<v4_acl>\S+))?$""", re.VERBOSE)

    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_session = None

        if optional_args is None:
            optional_args = {}

        self.transport = optional_args.get('eos_transport', 'https')

        if self.transport == 'https':
            self.port = optional_args.get('port', 443)
        elif self.transport == 'http':
            self.port = optional_args.get('port', 80)

        self.enablepwd = optional_args.get('enable_password', '')

    def open(self):
        """Implementation of NAPALM method open."""
        try:
            if self.transport in ('http', 'https'):
                connection = pyeapi.client.connect(
                    transport=self.transport,
                    host=self.hostname,
                    username=self.username,
                    password=<PASSWORD>,
                    port=self.port,
                    timeout=self.timeout
                )
            elif self.transport == 'socket':
                connection = pyeapi.client.connect(transport=self.transport)
            else:
                raise ConnectionException("Unknown transport: {}".format(self.transport))

            if self.device is None:
                self.device = pyeapi.client.Node(connection, enablepwd=self.enablepwd)
            # does not raise an Exception if unusable

            # let's try to run a very simple command
            self.device.run_commands(['show clock'], encoding='text')
        except ConnectionError as ce:
            # and this is raised either if device not avaiable
            # either if HTTP(S) agent is not enabled
            # show management api http-commands
            raise ConnectionException(ce.message)

    def close(self):
        """Implementation of NAPALM method close."""
        self.discard_config()

    def is_alive(self):
        return {
            'is_alive': True  # always true as eAPI is HTTP-based
        }

    def _load_config(self, filename=None, config=None, replace=True):
        if self.config_session is not None:
            raise SessionLockedException('Session is already in use by napalm')
        else:
            self.config_session = 'napalm_{}'.format(datetime.now().microsecond)

        commands = []
        commands.append('configure session {}'.format(self.config_session))

        if replace:
            commands.append('rollback clean-config')

        if filename is not None:
            with open(filename, 'r') as f:
                lines = f.readlines()
        else:
            if isinstance(config, list):
                lines = config
            else:
                lines = config.splitlines()

        for line in lines:
            line = line.strip()
            if line == '':
                continue
            if line.startswith('!'):
                continue
            commands.append(line)

        try:
            self.device.run_commands(commands)
        except pyeapi.eapilib.CommandError as e:
            self.discard_config()

            if replace:
                raise ReplaceConfigException(e.message)
            else:
                raise MergeConfigException(e.message)

    def load_replace_candidate(self, filename=None, config=None):
        """Implementation of NAPALM method load_replace_candidate."""
        self._load_config(filename, config, True)

    def load_merge_candidate(self, filename=None, config=None):
        """Implementation of NAPALM method load_merge_candidate."""
        self._load_config(filename, config, False)

    def compare_config(self):
        """Implementation of NAPALM method compare_config."""
        if self.config_session is None:
            return ''
        else:
            commands = ['show session-config named %s diffs' % self.config_session]
            result = self.device.run_commands(commands, encoding='text')[0]['output']

            result = '\n'.join(result.splitlines()[2:])

            return result.strip()

    def commit_config(self):
        """Implementation of NAPALM method commit_config."""
        commands = []
        commands.append('copy startup-config flash:rollback-0')
        commands.append('configure session {}'.format(self.config_session))
        commands.append('commit')
        commands.append('write memory')

        self.device.run_commands(commands)
        self.config_session = None

    def discard_config(self):
        """Implementation of NAPALM method discard_config."""
        if self.config_session is not None:
            commands = []
            commands.append('configure session {}'.format(self.config_session))
            commands.append('abort')
            self.device.run_commands(commands)
            self.config_session = None

    def rollback(self):
        """Implementation of NAPALM method rollback."""
        commands = []
        commands.append('configure replace flash:rollback-0')
        commands.append('write memory')
        self.device.run_commands(commands)

    def get_facts(self):
        """Implementation of NAPALM method get_facts."""
        commands = []
        commands.append('show version')
        commands.append('show hostname')
        commands.append('show interfaces')

        result = self.device.run_commands(commands)

        version = result[0]
        hostname = result[1]
        interfaces_dict = result[2]['interfaces']

        uptime = time.time() - version['bootupTimestamp']

        interfaces = [i for i in interfaces_dict.keys() if '.' not in i]
        interfaces = string_parsers.sorted_nicely(interfaces)

        return {
            'hostname': hostname['hostname'],
            'fqdn': hostname['fqdn'],
            'vendor': u'Arista',
            'model': version['modelName'],
            'serial_number': version['serialNumber'],
            'os_version': version['internalVersion'],
            'uptime': int(uptime),
            'interface_list': interfaces,
        }

    def get_interfaces(self):
        commands = []
        commands.append('show interfaces')
        output = self.device.run_commands(commands)[0]

        interfaces = {}

        for interface, values in output['interfaces'].items():
            interfaces[interface] = {}

            if values['lineProtocolStatus'] == 'up':
                interfaces[interface]['is_up'] = True
                interfaces[interface]['is_enabled'] = True
            else:
                interfaces[interface]['is_up'] = False
                if values['interfaceStatus'] == 'disabled':
                    interfaces[interface]['is_enabled'] = False
                else:
                    interfaces[interface]['is_enabled'] = True

            interfaces[interface]['description'] = values['description']

            interfaces[interface]['last_flapped'] = values.pop('lastStatusChangeTimestamp', None)

            interfaces[interface]['speed'] = int(values['bandwidth'] * 1e-6)
            interfaces[interface]['mac_address'] = napalm_base.helpers.convert(
                napalm_base.helpers.mac, values.pop('physicalAddress', u''))

        return interfaces

    def get_lldp_neighbors(self):
        commands = []
        commands.append('show lldp neighbors')
        output = self.device.run_commands(commands)[0]['lldpNeighbors']

        lldp = {}

        for n in output:
            if n['port'] not in lldp.keys():
                lldp[n['port']] = []

            lldp[n['port']].append(
                {
                    'hostname': n['neighborDevice'],
                    'port': n['neighborPort'],
                }
            )

        return lldp

    def get_interfaces_counters(self):
        commands = ['show interfaces']
        output = self.device.run_commands(commands)
        interface_counters = defaultdict(dict)
        for interface, data in output[0]['interfaces'].items():
            if data['hardware'] == 'subinterface':
                # Subinterfaces will never have counters so no point in parsing them at all
                continue
            counters = data.get('interfaceCounters', {})
            interface_counters[interface].update(
                tx_octets=counters.get('outOctets', -1),
                rx_octets=counters.get('inOctets', -1),
                tx_unicast_packets=counters.get('outUcastPkts', -1),
                rx_unicast_packets=counters.get('inUcastPkts', -1),
                tx_multicast_packets=counters.get('outMulticastPkts', -1),
                rx_multicast_packets=counters.get('inMulticastPkts', -1),
                tx_broadcast_packets=counters.get('outBroadcastPkts', -1),
                rx_broadcast_packets=counters.get('inBroadcastPkts', -1),
                tx_discards=counters.get('outDiscards', -1),
                rx_discards=counters.get('inDiscards', -1),
                tx_errors=counters.get('totalOutErrors', -1),
                rx_errors=counters.get('totalInErrors', -1)
            )
        return interface_counters

    def get_bgp_neighbors(self):

        def get_re_group(res, key, default=None):
            """ Small helper to retrive data from re match groups"""
            try:
                return res.group(key)
            except KeyError:
                return default

        NEIGHBOR_FILTER = 'bgp neighbors vrf all | include remote AS | remote router ID |IPv[46] Unicast:.*[0-9]+|^Local AS|Desc|BGP state'  # noqa
        output_summary_cmds = self.device.run_commands(
            ['show ipv6 bgp summary vrf all', 'show ip bgp summary vrf all'],
            encoding='json')
        output_neighbor_cmds = self.device.run_commands(
            ['show ip ' + NEIGHBOR_FILTER, 'show ipv6 ' + NEIGHBOR_FILTER],
            encoding='text')

        bgp_counters = defaultdict(lambda: dict(peers={}))
        for summary in output_summary_cmds:
            """
            Json output looks as follows
            "vrfs": {
                "default": {
                    "routerId": 1,
                    "asn": 1,
                    "peers": {
                        "1.1.1.1": {
                            "msgSent": 1,
                            "inMsgQueue": 0,
                            "prefixReceived": 3926,
                            "upDownTime": 1449501378.418644,
                            "version": 4,
                            "msgReceived": 59616,
                            "prefixAccepted": 3926,
                            "peerState": "Established",
                            "outMsgQueue": 0,
                            "underMaintenance": false,
                            "asn": 1
                        }
                    }
                }
            }
            """
            for vrf, vrf_data in summary['vrfs'].items():
                bgp_counters[vrf]['router_id'] = vrf_data['routerId']
                for peer, peer_data in vrf_data['peers'].items():
                    if peer_data['peerState'] == 'Idle':
                        is_enabled = True if peer_data['peerStateIdleReason'] != 'Admin' else False
                    else:
                        is_enabled = True
                    peer_info = {
                        'is_up': peer_data['peerState'] == 'Established',
                        'is_enabled': is_enabled,
                        'uptime': int(time.time() - peer_data['upDownTime'])
                    }
                    bgp_counters[vrf]['peers'][napalm_base.helpers.ip(peer)] = peer_info
        lines = []
        [lines.extend(x['output'].splitlines()) for x in output_neighbor_cmds]
        while lines:
            """
            Raw output from the command looks like the following:

              BGP neighbor is 1.1.1.1, remote AS 1, external link
                Description: Very info such descriptive
                BGP version 4, remote router ID 1.1.1.1, VRF my_vrf
                BGP state is Idle, Administratively shut down
                 IPv4 Unicast:         683        78
                 IPv6 Unicast:           0         0
              Local AS is 2, local router ID 2.2.2.2
            """
            neighbor_info = re.match(self._RE_BGP_INFO, lines.pop(0))
            # this line can be either description or rid info
            next_line = lines.pop(0)
            desc = re.match(self._RE_BGP_DESC, next_line)
            if desc is None:
                rid_info = re.match(self._RE_BGP_RID_INFO, next_line)
                desc = ''
            else:
                rid_info = re.match(self._RE_BGP_RID_INFO, lines.pop(0))
                desc = desc.group('description')
            lines.pop(0)
            v4_stats = re.match(self._RE_BGP_PREFIX, lines.pop(0))
            v6_stats = re.match(self._RE_BGP_PREFIX, lines.pop(0))
            local_as = re.match(self._RE_BGP_LOCAL, lines.pop(0))
            data = {
                'remote_as': int(neighbor_info.group('as')),
                'remote_id': napalm_base.helpers.ip(get_re_group(rid_info, 'rid', '0.0.0.0')),
                'local_as': int(local_as.group('as')),
                'description': py23_compat.text_type(desc),
                'address_family': {
                    'ipv4': {
                        'sent_prefixes': int(get_re_group(v4_stats, 'sent', -1)),
                        'received_prefixes': int(get_re_group(v4_stats, 'received', -1)),
                        'accepted_prefixes': -1
                    },
                    'ipv6': {
                        'sent_prefixes': int(get_re_group(v6_stats, 'sent', -1)),
                        'received_prefixes': int(get_re_group(v6_stats, 'received', -1)),
                        'accepted_prefixes': -1
                    }
                }
            }
            peer_addr = napalm_base.helpers.ip(neighbor_info.group('neighbor'))
            vrf = rid_info.group('vrf')
            if peer_addr not in bgp_counters[vrf]['peers']:
                bgp_counters[vrf]['peers'][peer_addr] = {
                    'is_up': False,  # if not found, means it was not found in the oper stats
                                     # i.e. neighbor down,
                    'uptime': 0,
                    'is_enabled': True
                }
            bgp_counters[vrf]['peers'][peer_addr].update(data)
        if 'default' in bgp_counters:
            bgp_counters['global'] = bgp_counters.pop('default')
        return dict(bgp_counters)

    def get_environment(self):
        def extract_temperature_data(data):
            for s in data:
                temp = s['currentTemperature'] if 'currentTemperature' in s else 0.0
                name = s['name']
                values = {
                   'temperature': temp,
                   'is_alert': temp > s['overheatThreshold'],
                   'is_critical': temp > s['criticalThreshold']
                }
                yield name, values

        sh_version_out = self.device.run_commands(['show version'])
        is_veos = sh_version_out[0]['modelName'].lower() == 'veos'
        commands = [
            'show environment cooling',
            'show environment temperature'
        ]
        if not is_veos:
            commands.append('show environment power')
            fans_output, temp_output, power_output = self.device.run_commands(commands)
        else:
            fans_output, temp_output = self.device.run_commands(commands)
        environment_counters = {
            'fans': {},
            'temperature': {},
            'power': {},
            'cpu': {}
        }
        cpu_output = self.device.run_commands(['show processes top once'],
                                              encoding='text')[0]['output']
        for slot in fans_output['fanTraySlots']:
            environment_counters['fans'][slot['label']] = {'status': slot['status'] == 'ok'}
        # First check FRU's
        for fru_type in ['cardSlots', 'powerSupplySlots']:
            for fru in temp_output[fru_type]:
                t = {name: value for name, value in extract_temperature_data(fru['tempSensors'])}
                environment_counters['temperature'].update(t)
        # On board sensors
        parsed = {n: v for n, v in extract_temperature_data(temp_output['tempSensors'])}
        environment_counters['temperature'].update(parsed)
        if not is_veos:
            for psu, data in power_output['powerSupplies'].items():
                environment_counters['power'][psu] = {
                    'status': data['state'] == 'ok',
                    'capacity': data['capacity'],
                    'output': data['outputPower']
                }
        cpu_lines = cpu_output.splitlines()
        # Matches either of
        # Cpu(s):  5.2%us,  1.4%sy,  0.0%ni, 92.2%id,  0.6%wa,  0.3%hi,  0.4%si,  0.0%st ( 4.16 > )
        # %Cpu(s):  4.2 us,  0.9 sy,  0.0 ni, 94.6 id,  0.0 wa,  0.1 hi,  0.2 si,  0.0 st ( 4.16 < )
        m = re.match('.*ni, (?P<idle>.*).id.*', cpu_lines[2])
        environment_counters['cpu'][0] = {
            '%usage': round(100 - float(m.group('idle')), 1)
        }
        # Matches either of
        # Mem:   3844356k total,  3763184k used,    81172k free,    16732k buffers ( 4.16 > )
        # KiB Mem:  32472080 total,  5697604 used, 26774476 free,   372052 buffers ( 4.16 < )
        mem_regex = '.*total,\s+(?P<used>\d+)[k\s]+used,\s+(?P<free>\d+)[k\s]+free,.*'
        m = re.match(mem_regex, cpu_lines[3])
        environment_counters['memory'] = {
            'available_ram': int(m.group('free')),
            'used_ram': int(m.group('used'))
        }
        return environment_counters

    def get_lldp_neighbors_detail(self, interface=''):

        lldp_neighbors_out = {}

        filters = []
        if interface:
            filters.append(interface)

        commands = []
        commands.append(
            'show lldp neighbors {filters} detail'.format(
                filters=' '.join(filters)
            )
        )

        lldp_neighbors_in = {}
        lldp_neighbors_in = self.device.run_commands(commands)[0].get('lldpNeighbors', {})

        for interface in lldp_neighbors_in:
            interface_neighbors = lldp_neighbors_in.get(interface).get('lldpNeighborInfo', {})
            if not interface_neighbors:
                # in case of empty infos
                continue

            # it is provided a list of neighbors per interface
            for neighbor in interface_neighbors:
                if interface not in lldp_neighbors_out.keys():
                    lldp_neighbors_out[interface] = []
                capabilities = neighbor.get('systemCapabilities')
                capabilities_list = list(capabilities.keys())
                capabilities_list.sort()
                remote_chassis_id = neighbor.get('chassisId', u'')
                if neighbor.get("chassisIdType", u'') == "macAddress":
                    remote_chassis_id = napalm_base.helpers.mac(remote_chassis_id)
                lldp_neighbors_out[interface].append(
                    {
                        'parent_interface': interface,  # no parent interfaces
                        'remote_port':
                            neighbor.get('neighborInterfaceInfo', {}).get('interfaceId', u''),
                        'remote_port_description': u'',
                        'remote_system_name': neighbor.get('systemName', u''),
                        'remote_system_description': neighbor.get('systemDescription', u''),
                        'remote_chassis_id': remote_chassis_id,
                        'remote_system_capab': py23_compat.text_type(', '.join(capabilities_list)),
                        'remote_system_enable_capab': py23_compat.text_type(', '.join(
                            [capability for capability in capabilities_list
                             if capabilities[capability]]))
                    }
                )
{'fraction_non_alphanumeric': 0.11497099509147701, 'fraction_numerical': 0.011601963409192326, 'mean_word_length': 1.8727727214459684, 'pattern_counts': {'":': 35, '<': 42, '<?xml version=': 0, '>': 46, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 10, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 8, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5170810', 'n_tokens_mistral': 13057, 'n_tokens_neox': 12390, 'n_words': 2733}