#!/usr/bin/python
#
# Copyright 2014,2015,2016  Cumulus Networks, Inc.
#
# This package is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 2 of the License
#
# This package is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
# On Debian systems, the complete text of the GNU General
# Public License version 2 can be found in "/usr/share/common-licenses/GPL-2".

import re
import os
import ctypes
import daemon
import daemon.pidlockfile
import lockfile
import time
import logging
import logging.handlers
import traceback
import sys
import subprocess
import errno
import socket
import struct
import array
import fcntl
import io
import ConfigParser
import signal
try:
    import bcmshell
except ImportError:
    pass

import cumulus.porttab

"""
This file makes the following assumptions about data structures:

struct ifreq
{
    union
    {
        char    ifrn_name[16];
    } ifr_ifrn;

    union {
        struct    sockaddr ifru_addr;
        struct    sockaddr ifru_dstaddr;
        struct    sockaddr ifru_broadaddr;
        struct    sockaddr ifru_netmask;
        struct    sockaddr ifru_hwaddr;
        short     ifru_flags;
        int       ifru_ivalue;
        int       ifru_mtu;
        struct    ifmap ifru_map; // 16 bytes long
        char      ifru_slave[16];
        char      ifru_newname[16];
        void __user *    ifru_data;
        struct    if_settings ifru_settings;
    } ifr_ifru;
};

typedef unsigned short sa_family_t;

struct sockaddr {
    sa_family_t  sa_family;
    char         sa_data[14];
};

struct ifconf {
    int                 ifc_len; /* size of buffer */
    union {
        char           *ifc_buf; /* buffer address */
        struct ifreq   *ifc_req; /* array of structures */
    };
};


// From linux/ethtool.h

struct ethtool_cmd {
    __u32   cmd;
    __u32   supported;      /* Features this interface supports */
    __u32   advertising;    /* Features this interface advertises */
    __u16   speed;          /* The forced speed, 10Mb, 100Mb, gigabit */
    __u8    duplex;         /* Duplex, half or full */
    __u8    port;           /* Which connector port */
    __u8    phy_address;
    __u8    transceiver;    /* Which transceiver to use */
    __u8    autoneg;        /* Enable or disable autonegotiation */
    __u32   maxtxpkt;       /* Tx pkts before generating tx int */
    __u32   maxrxpkt;       /* Rx pkts before generating rx int */
    __u32   reserved[4];
};

struct ethtool_value {
    __u32    cmd;
    __u32    data;
};
"""


# From linux/sockios.h
SIOCETHTOOL = 0x8946

# From linux/socket.h
AF_UNIX      = 1
AF_INET      = 2

# From linux/ethtool.h
ETHTOOL_GSET = 0x00000001 # Get settings
ETHTOOL_SSET = 0x00000002 # Set settings
ETHTOOL_GLINK = 0x0000000a # Get link status (ethtool_value)


try:
    import smbus
    smbus_available = True
except ImportError:
    smbus_available = False

logger = None
log_handlers = []
logs_throttled = {}
sock = None
sockfd = None

def log_throttle(msg, once=False, level='warning'):
    send_log = True
    if msg in logs_throttled.keys():
        throttled = logs_throttled[msg]
        if once:
            send_log = False
        elif throttled.expired:
            throttled = LogMessage(msg)
        else:
            send_log = throttled.repeat()
    else:
        throttled = LogMessage(msg)

    logs_throttled[msg] = throttled

    message = throttled.message

    if send_log:
        if level == 'warning':
            logger.warning(message)
        elif level == 'error':
            logger.error(message)
        elif level == 'info':
            logger.info(message)
        elif level == 'debug':
            logger.debug(message)

    return message

class BCMNotAvailable(Exception):
    pass

class EEPROMTimeout(Exception):
    pass

class UnsupportedPhy(Exception):
    pass

class NoSuchPort(Exception):
    pass

class RetimerTimeout(Exception):
    pass

class Port(object):
    '''
    Port - designed as a temporary mirror of the cumulus/portconfig physical
    port class
    '''
    IF_UP = 0x1 # interface is up
    disabled = False

    def __init__(self, *args, **kwargs):
        self.label = kwargs['label']
        self.driver = kwargs['driver']
        if 'config' in kwargs.keys():
            self.config = kwargs['config']
        else:
            self.config = None

        self._no_link_resets = 0

    def __str__(self):
        return 'port-%s' % self.label

    @property
    def exists(self):
        if (os.path.exists('/sys/class/net/swp%s' % self.label) or
            os.path.exists('/sys/class/net/swp%ss0' % self.label)):
            return True
        else:
            return False

    @property
    def eeprom_ready(self):
        return self.exists and not self.disabled and self.driver.present

    @property
    def sysfs_ready(self):
        return self.exists and not self.disabled

    @property
    def traffic_ready(self):
        return self.sysfs_ready and self.eeprom_ready and any(self.admin_up)

    def no_link_resets(self, clear=False):
        old = self._no_link_resets
        self._no_link_resets += 1
        if clear:
            old = 0
            self._no_link_resets = 0
        return old

    def _swp_flags(self, sub=''):
        return int(open('/sys/class/net/swp%s%s/flags' % (self.label, sub)).read(), 0)

    def _swp_carrier(self, sub=''):
        try:
            return int(open('/sys/class/net/swp%s%s/carrier' % (self.label, sub)).read(), 0)
        except IOError, e:
            if e.errno == errno.EINVAL:
                return 0
            raise e

    def _swp_speed(self, sub=''):
        base = 'swp%s%s' % (self.label, sub)

        if sock is None or sockfd is None:
            socket_init()

        try:
            # First get link params
            ecmd = array.array('B', struct.pack('I39s', ETHTOOL_GSET, '\x00'*39))
            ifreq = struct.pack('16sP', base, ecmd.buffer_info()[0])
            try:
                fcntl.ioctl(sockfd, SIOCETHTOOL, ifreq)
                res = ecmd.tostring()
                speed, duplex, auto = struct.unpack('12xHB3xB24x', res)
            except IOError:
                speed, duplex, auto = 65535, 255, 255

        except IOError, e:
            if e.errno == errno.EINVAL:
                # interface is not administratively up, carrier is invalid
                return 0
            raise e

        if speed == 65535:
            speed = 0

        return speed

    @property
    def _port_flags(self):
        base = 'swp%s' % self.label
        try:
            return (self._swp_flags(), )
        except IOError, e:
            if e.errno != errno.ENOENT:
                raise e

        try:
            return (self._swp_flags(sub='s0'), 
                    self._swp_flags(sub='s1'),
                    self._swp_flags(sub='s2'),
                    self._swp_flags(sub='s3'))
        except IOError, e:
            if e.errno != errno.ENOENT:
                raise e

        try:
            return (self._swp_flags(sub='s0'),
                    self._swp_flags(sub='s1'))
        except IOError, e:
            if e.errno != errno.ENOENT:
                raise e

        raise NoSuchPort('failed to get flags for port: %s' % self.label)

    @property
    def _port_carrier(self):
        base = 'swp%s' % self.label
        try:
            return (self._swp_carrier(), )
        except IOError, e:
            if e.errno != errno.ENOENT:
                raise e

        try:
            return (self._swp_carrier(sub='s0'), 
                    self._swp_carrier(sub='s1'),
                    self._swp_carrier(sub='s2'),
                    self._swp_carrier(sub='s3'))
        except IOError, e:
            if e.errno != errno.ENOENT:
                raise e

        try:
            return (self._swp_carrier(sub='s0'),
                    self._swp_carrier(sub='s1'))
        except IOError, e:
            if e.errno != errno.ENOENT:
                raise e

        raise NoSuchPort('failed to get carrier for port: %s' % self.label)

    @property
    def _port_speed(self):
        path = '/sys/class/net/swp%s' % (self.label)

        if os.path.exists(path):
        # Even in the case where the port i.e. swp1 doesn't exist
        # and sub interfaces are created, ethtool ioctl is returning
        # is returning 0, so checking existance of /sys/class/net/<interface>
        # to check existance of Port
            try:
                return (self._swp_speed(), )
            except IOError, e:
                if e.errno != errno.ENOENT:
                    raise e
        else:
            try:
                return (self._swp_speed(sub='s0'),
                    self._swp_speed(sub='s1'),
                    self._swp_speed(sub='s2'),
                    self._swp_speed(sub='s3'))
            except IOError, e:
                if e.errno != errno.ENOENT:
                    raise e
            try:
                return (self._swp_speed(sub='s0'),
                        self._swp_speed(sub='s1'))
            except IOError, e:
                if e.errno != errno.ENOENT:
                    raise e

        raise NoSuchPort('failed to get speed for port: %s' % self.label)

    def admin_down(self):
        base = 'swp%s' % self.label
        try:
            if len(self._port_carrier) == 1:
                subprocess.check_call(('ip', 'link', 'set', base, 'down'))
            else:
                for sub in range(len(self._port_carrier)):
                    subprocess.check_call(('ip link set %ss%d down' % (base, sub)).split())
        except subprocess.CalledProcessError, e:
                logger.warning('failed to admin down port %s' % self.label)

    @property
    def admin_up(self):
        return [ x & self.IF_UP != 0 for x in self._port_flags ]

    @property
    def carrier(self):
        return [ x != 0 for x in self._port_carrier ]

    @property
    def speed(self):
        return [ x  for x in self._port_speed ]

class PortConfig(object):
    pass

class PortDriver(object):

    def toggle_reset(self, timing):
        raise NotImplementedError

    def _set_reset(self, logical):
        raise NotImplementedError

    def _get_reset(self):
        raise NotImplementedError

    reset = property(_get_reset, _set_reset)

    def _set_pmod(self, power_mode):
        raise NotImplementedError

    def _get_pmod(self):
        raise NotImplementedError

    power_mode = property(_get_pmod, _set_pmod)

    @property
    def present(self):
        raise NotImplementedError

    @property
    def admin_up(self):
        raise NotImplementedError

    @property
    def eeprom(self):
        raise NotImplementedError

class FilePortDriver(PortDriver):
    def __init__(self, reset=None, lpmod=None, present=None, tx_disable=None,
                 tx_fault=None, rx_los=None, cable=None, tx_retimer=None,
                 rx_retimer=None):
        self._reset = reset
        self._lpmod = lpmod
        self._present = present
        self._tx_disable = tx_disable
        self._tx_fault = tx_fault
        self._rx_los = rx_los

        self.cable = cable

        self.tx_retimer = tx_retimer
        self.rx_retimer = rx_retimer

    def toggle_reset(self):
        if self._reset is not None:
            self.reset = 1
            time.sleep(.1)
            self.reset = 0
        elif self._tx_disable is not None:
            self.tx_disable = 1
            time.sleep(.1)
            self.tx_disable = 0

    @property
    def present(self):
        if self._present:
            return self._present.logical == 1
        else:
            return False

    def _set_reset(self, logical):
        if self._reset:
            self._reset.logical = logical

    def _get_reset(self):
        if self._reset:
            return self._reset.logical == 1
        else:
            return False

    reset = property(_get_reset, _set_reset)

    def _set_pmod(self, power_mode):
        if power_mode not in ('high', 'low'):
            raise ValueError('power mode must be "high" or "low"')

        if not self._lpmod:
            return

        if power_mode == 'high':
            self._lpmod.logical = 0
        else:
            self._lpmod.logical = 1

    def _get_pmod(self):
        if self._lpmod:
            if self._lpmod.logical == 1:
                return 'low'
            else:
                return 'high'
        else:
            return 'low'

    power_mode = property(_get_pmod, _set_pmod)

    @property
    def tx_fault(self):
        if self._tx_fault:
            return self._tx_fault.logical == 1
        else:
            return False

    @property
    def rx_los(self):
        if self._rx_los:
            return self._rx_los.logical == 1
        else:
            return False

    def _set_tx_disable(self, logical):
        if self._tx_disable:
            self._tx_disable.logical = logical

    def _get_tx_disable(self):
        if self.tx_disable:
            return self._tx_disable.logical == 1
        else:
            return False

    tx_disable = property(_get_tx_disable, _set_tx_disable)


class SysFSPortDriver(FilePortDriver):

    eeprom_dev_cache = {}
    directory_walk_cache = {}
    retimer_dev_cache = {}

    @classmethod
    def _find(self, directory, signal):

        if directory not in SysFSPortDriver.directory_walk_cache:
            SysFSPortDriver.directory_walk_cache[directory] = []

            for root, dirs, files in os.walk(directory):
                SysFSPortDriver.directory_walk_cache[directory].append((root, dirs, files))

        for root, dirs, files in SysFSPortDriver.directory_walk_cache[directory]:
            if signal in files:
                return os.path.join(root, signal)

    @classmethod
    def _find_eeprom(self, portnum):
        if not SysFSPortDriver.eeprom_dev_cache:
            for root, dirs, files in os.walk('/sys/class/eeprom_dev'):
                for eeprom_dir in dirs:
                    label = open(os.path.join(root, eeprom_dir, 'label'), 'r').read().strip()
                    SysFSPortDriver.eeprom_dev_cache[label] = SysFSEEPROM(os.path.join(root, eeprom_dir, 'device', 'eeprom'))

        target = 'port%u' % portnum

        if target in SysFSPortDriver.eeprom_dev_cache:
            return SysFSPortDriver.eeprom_dev_cache[target]

        raise NoSuchPort('no such port: port-%s' % portnum)

    @classmethod
    def _find_retimer(self, portnum, direction):
        if not SysFSPortDriver.retimer_dev_cache:
            for root, dirs, files in os.walk('/sys/class/retimer_dev'):
                for retimer_dir in dirs:
                    label = open(os.path.join(root, retimer_dir, 'label'), 'r').read().strip()
                    SysFSPortDriver.retimer_dev_cache[label] = os.path.join(root, retimer_dir, 'device')

        target = 'port%u_%s' % (portnum, direction)

        if target in SysFSPortDriver.retimer_dev_cache:
            return SysFSPortDriver.retimer_dev_cache[target]

        raise NoSuchPort('no such port: port-%s' % portnum)

class EEPROMOnlyPortDriver(SysFSPortDriver):
    def __init__(self, eeprom):
        cable = SFFCable(eeprom)
        present = TimeoutPresenceSignal(eeprom)
        SysFSPortDriver.__init__(self, reset=FalseSignal(), lpmod=FalseSignal(),
                                 present=present, cable=cable)

class SysFSEEPROM(object):
    def __init__(self, path):
        self.path = path

    def _getslice(self, sl):
        # eeproms are slow, only read exactly the number of bytes requested.
        # (no buffering)
        eeprom = io.open(self.path, 'rb', buffering=0)
        eeprom.seek(sl.start)
        try:
            return eeprom.read(sl.stop - sl.start)
        except IOError, e:
            if e.errno == errno.ETIMEDOUT:
                raise EEPROMTimeout('timeout reading eeprom at: %s' % self.path)
                return ''
            else:
                raise e

    def _setslice(self, sl, value):
        eeprom = io.open(self.path, 'wb', buffering=0)
        eeprom.seek(sl.start)
        try:
            return eeprom.write(struct.pack('B', (sl.stop - sl.start)))
        except IOError, e:
            if e.errno == errno.ETIMEDOUT:
                raise EEPROMTimeout('timeout writing eeprom at: %s' % self.path)
            else:
                raise e

    def __getitem__(self, key):
        if isinstance(key, slice):
            return self._getslice(key)
        else:
            return self._getslice(slice(key, key + 1))

    def __setitem__(self, key, value):
        if isinstance(key, slice):
            self._setslice(key, value)
        else:
            return self._setslice(slice(key, key + 1), value)

    @property
    def bus(self):
        base = os.path.split(self.path)[0]
        devpath = os.path.join(base, '..', 'i2c-dev')
        for directory in os.listdir(devpath):
            if directory.startswith('i2c-'):
                return int(directory[4:])

        raise NoI2CBus('no i2c bus path for: %s' % self.path)

class GPIOPortDriver(SysFSPortDriver):
    pass

class Signal(object):
    pass

class ReadOnlySignal(Signal):
    value = None

    def _set_value(self, value):
        pass

    def _get_value(self):
        return self.value

    level = property(_get_value, _set_value)
    logical = level

class TrueSignal(ReadOnlySignal):
    value = 1

class FalseSignal(ReadOnlySignal):
    value = 0

class TimeoutPresenceSignal(Signal):
    def __init__(self, eeprom):
        self.eeprom = eeprom

    def _set_value(self, value):
        pass

    def _get_value(self):
        try:
            self.eeprom[0]
            return True
        except EEPROMTimeout, e:
            return False

    level = property(_get_value, _set_value)
    logical = level

class BinaryFileSignal(Signal):
    def __init__(self, path, ctype, bit, active='high'):
        self.path = path
        self.ctype = ctype
        self.bit = bit

        if active not in ('high', 'low'):
            raise RuntimeError('invalid active level: %s' % active)

        self.active = active

    @property
    def cache(self):
        # not implemented - to be used to coalesce single-bit reads/writes to
        # multi-bit files.  For example, when a file contains 32 bits that
        # represent signals from 32 ports.
        return None

    @property
    def mask(self):
        return 1 << self.bit

    def _read(self):
        f = open(self.path, 'r')
        value = int(f.read(), base=0)
        f.close()
        return value

    def _write(self, value):
        f = open(self.path, 'w')
        f.write('0x%x\n' % self.ctype(value).value)
        f.close()

    def _rmw(self, value):
        # XXX consider file locking on self.path
        self._write((self._read() & ~self.mask) | (value & self.mask))

    def _physical(self, value):
        # conditionally invert our mask bit in a given value
        if self.active == 'high':
            return value
        elif self.active == 'low':
            return value ^ self.mask
        
        raise RuntimeError('invalid active level: %s' % active)

    def _convert(self, value):
        # convert a myriad of logical levels to binary with our mask bit set to
        # 1 or 0
        if value in ('1', True, 'high', 'asserted'):
            return self.mask
        elif value in ('0', False, 'low', 'deasserted'):
            return 0
        else:
            return value

    def _set_level(self, value):
        self._rmw(self._convert(value))

    def _get_level(self):
        if self._read() & self.mask == 0:
            return 0
        else:
            return 1

    level = property(_get_level, _set_level)

    def _set_logical(self, value):
        self._set_level(self._physical(self._convert(value)))

    def _get_logical(self):
        level = self._get_level()
        if self.active == 'high':
            return level
        else:
            return ~level & 1

    logical = property(_get_logical, _set_logical)

class GPIOSignal(BinaryFileSignal):
    _base = '/sys/class/gpio'

    def _export(self, gpiopin):
        try:
            unexportf = open(self._base + '/unexport', 'w')
            unexportf.write('%u\n' % gpiopin)
            unexportf.close()
        except IOError, e:
            if e.errno == errno.EINVAL: # Invalid Argument (probably already unexported)
                pass
            else:
                raise e
            
        exportf = open(self._base + '/export', 'w')
        exportf.write('%u\n' % gpiopin)
        exportf.close()
        
    def _set_direction(self, gpiopin, direction):
        dirpath = self._base + '/gpio%u/direction' % gpiopin

        # Avoid unnecesary writes to direction.  They will cause gpio/value to
        # reset to 0.
        current_direction = open(dirpath).read().strip()
        if direction != current_direction:
            dirf = open(dirpath, 'w')
            dirf.write(direction + '\n')
            dirf.close()

    def __init__(self, gpiopin, active='high', direction='in'):
        self._export(gpiopin)
        self._set_direction(gpiopin, direction)
        BinaryFileSignal.__init__(self, self._base + '/gpio%u/value' % gpiopin,
                                  ctype=ctypes.c_bool, bit=0, active=active)

class GPIOSignalNamed(BinaryFileSignal):
    _base = '/sys/class/gpio'

    def __init__(self, gpiopin, active):
        path = self._base + '/' + gpiopin + '/value'
        BinaryFileSignal.__init__(self, path, ctype=ctypes.c_bool, bit=0,
                                  active=active)

class SFFDecoder(object):
    @classmethod
    def decode_connector(self, eeprom):
        data = eeprom[self.ADDR_CONNECTOR_CODE]
        if len(data) != 1:
            return None

        concode = struct.unpack('B', data)[0]
        return self.CONNECTOR_CODE_MAP.get(concode, 'unknown')

    @classmethod
    def decode_lpoverride(self, eeprom):
        if not self.ADDR_POWER_CTL:
            return None

        data = eeprom[self.ADDR_POWER_CTL]
        lpov = struct.unpack('B', data)[0] & self.POWER_CTL_LPOVERRIDE_MASK

        return self.POWER_CTL_LPOVERRIDE_MAP.get(lpov, 'unknown')

    @classmethod
    def encode_lpoverride(self, eeprom, value):
        if not self.ADDR_POWER_CTL:
            return

        if value not in self.POWER_CTL_LPOVERRIDE_RMAP:
            raise RuntimeError('invalid lpoverride value: %s' % value)
            return 

        data = eeprom[self.ADDR_POWER_CTL]
        lpov = struct.unpack('B', data)[0] & self.POWER_CTL_LPOVERRIDE_MASK
        lpov &= ~self.POWER_CTL_LPOVERRIDE_MASK
        lpov |= self.POWER_CTL_LPOVERRIDE_RMAP[value]

        eeprom[self.ADDR_POWER_CTL] = struct.pack('B', lpov)

    @classmethod
    def decode_length(self, eeprom):
        raise NotImplementedError()

    @classmethod
    def decode_vendor_oui(self, eeprom):
        oui = eeprom[self.ADDR_OUI:self.ADDR_OUI + self.LEN_OUI]
        if len(oui) != self.LEN_OUI:
            log_throttle('failed reading vendor OUI')
            return None
        return '%02x:%02x:%02x' % struct.unpack('BBB', oui)

    @classmethod
    def decode_vendor_pn(self, eeprom):
        return eeprom[self.ADDR_PN:self.ADDR_PN + self.LEN_PN].strip()

    @classmethod
    def decode_vendor_name(self, eeprom):
        return eeprom[self.ADDR_NAME:self.ADDR_NAME + self.LEN_NAME].strip()


class SFPDecoder(SFFDecoder):
    ADDR_CONNECTOR_CODE = 2
    ADDR_NAME = 20
    LEN_NAME = 16
    ADDR_OUI = 37
    LEN_OUI = 3
    ADDR_PN = 40
    LEN_PN = 16
    CONNECTOR_CODE_MAP = { 0x00 : 'unknown',
                           0x01 : 'sc',
                           0x07 : 'lc',
                           0x0b : 'optical-pigtail',
                           0x21 : 'copper-pigtail',
                           0x22 : 'rj45' }
    ADDR_ETH10G_CODE = 3
    ETH10G_CODE_MAP = { 4 : '10g-sr' , # 10GBase-SR (multi-mode fiber)
                        5 : '10g-lr' , # 10GBase-LR (single-mode fiber)
                        6 : '10g-lrm', # 10GBase-LRM (long reach multi-mode fiber)
                        7 : '10g-er' } # 10GBase-ER (extended reach single-mode fiber)
    ADDR_ETH1G_CODE = 6
    ETH1G_CODE_MAP =  { 0 : '1000base-sx', # 1GBase-SX (multi-mode fiber)
                        1 : '1000base-lx', # 1GBase-LX (single-mode fiber)
                        2 : '1000base-cx', # 1GBase-CX (balanced copper)
                        3 : '1000base-t' } # 1GBase-T (twisted pair)
    ETH_CODE_DEFAULT = '10g-cr'
    ADDR_POWER_CTL = None

    @classmethod
    def _decode_ethcodes(self, eeprom, addr, ethcode_map):
        data = eeprom[addr]
        if len(data) != 1:
            return None

        ethcodes = struct.unpack('B', data)[0]

        return [ ethcode_map[x] for x in ethcode_map.keys()
                 if (1 << x) & ethcodes ]

    @classmethod
    def decode_ethcodes(self, eeprom):
        ethcodes = tuple(sorted(
            self._decode_ethcodes(eeprom, self.ADDR_ETH1G_CODE, self.ETH1G_CODE_MAP) +
            self._decode_ethcodes(eeprom, self.ADDR_ETH10G_CODE, self.ETH10G_CODE_MAP)))

        # It's quite normal to assume copper as a fallback ethcode.
        if len(ethcodes) == 0:
            return (self.ETH_CODE_DEFAULT, )

        return ethcodes

class QSFPDecoder(SFFDecoder):
    ADDR_CONNECTOR_CODE = 130 # external connector (8-bit enum)
    ADDR_NAME = 148
    LEN_NAME = 16
    ADDR_OUI = 165
    LEN_OUI = 3
    ADDR_PN = 168
    LEN_PN = 16
    CONNECTOR_CODE_MAP = { 0x00 : 'unknown',
                           0x0c : 'mpo',
                           0x20 : 'hssdc2',
                           0x21 : 'copper-pigtail',
                           0x22 : 'rj45',
                           0x23 : 'none' }

    # Compliance codes are at address 131 as an 8-bit bitmap. If bit 7 is set,
    # we need to look at extended compliance codes which has enum values.
    # A cable could have more than one compliance code,
    # e.g., 100G QSFP28 DAC has 2 compliance codes, 40G CR4 and 100G CR4.

    ADDR_ETH_CODE = 131 # ethernet compliance code (8-bit, mask)
    ETH_CODE_MAP = { 0 : '40g-active',     # 40G active (xlppi)
                     1 : '40g-lr4',        # 40G LR4
                     2 : '40g-sr4',        # 40G SR4
                     3 : '40g-cr4',        # 40G CR4
                     4 : '10g-sr',         # 10G SR
                     5 : '10g-lr',         # 10G LR
                     6 : '10g-lrm' }       # 10G LRM

    ETH_CODE_EXTENDED = (1 << 7)
    ADDR_ETH_EXTENDED_CODE = 192 # ethernet extended compliance code (8-bit, enum)
    ETH_EXTENDED_CODE_MAP = {  1  : '100g-active',     # 100G AOC or 25G AUI C2M AOC (active optical cable,BER = 5x10^-5)
                               2  : '100g-sr4',        # 100G SR4 or 25G SR
                               3  : '100g-lr4',        # 100G LR4
                               4  : '100g-er4',        # 100G ER4
                               5  : '100g-sr10',       # 100G SR10
                               6  : '100g-cwdm4',      # 100G CWDM4 MSA with FEC
                               7  : '100g-psm4',       # 100G PSM4 Parallel SMF
                               8  : '100g-active-cu',  # 100G ACC or 25G AUI C2M ACC (active copper cable, BER = 5x10^-5)
                               9  : '100g-cwdm4',      # 100G CWDM4 MSA without FEC
                               11 : '100g-cr4',        # 100G CR4 or 25G CR CA-L
                               12 : '25g-cr',          # 25G CR CA-S
                               13 : '25g-cr',          # 25G CR CA-N
                               16 : '40g-er4',         # 40G ER4
                               17 : '4x10g-sr',        # 4 x 10G SR
                               18 : '40g-psm4',        # 40G PSM4 Parallel SMF
                               19 : 'p1i1-2d1',        # G959.1 profile P1I1-2D1 (10709 MBd, 2km, 1310nm SM)
                               20 : 'p1s1-2d2',        # G959.1 profile P1S1-2D2 (10709 MBd, 40km, 1550nm SM)
                               21 : 'p1l1-2d2',        # G959.1 profile P1L1-2D2 (10709 MBd, 80km, 1550nm SM)
                               22 : '10g-t',           # 10GBASE-T with SFI electrical interface
                               23 : '100g-clr4',       # 100G CLR4
                               24 : '100g-active',     # 100G AOC or 25G AUI C2M AOC (active optical cable,BER < 10^-12)
                               25 : '100g-active-cu'   # 100G ACC or 25G AUI C2M ACC (active copper cable, BER < 10^-12)
    # 10 : reserved
    # 14-15 : reserved
    # 26 - 255 : reserved
    }

    ADDR_ASSY_LENGTH = 146 # cable assembly (copper or active) length, units of meters
    ETH_CODE_DEFAULT = '40g-cr4'
    ETH_EXTENDED_CODE_DEFAULT = '100g-cr4'

    # Some buggy cables (CM-5102) return all zeros or all ones.  Lets try to do
    # something predictible and assume a default.
    ETH_CODE_QUIRKS = { 0    : ETH_CODE_DEFAULT,
                        0xff : ETH_CODE_DEFAULT }
    ETH_CODE_SANCTIONED_HACKS = { }

    ETH_EXTENDED_CODE_QUIRKS = { 0    : ETH_EXTENDED_CODE_DEFAULT,}
    ETH_EXTENDED_CODE_SANCTIONED_HACKS = { }

    # Some cables (CM-7480) with ambiguous ethcodes need an override
    ETH_CODE_ZERO_OVERRIDES = { '00:90:65,FCBG410QB1C03-FC' : '40g-active',
                                '00:90:65,FCBG410QB1C05-FC' : '40g-active',
                                '00:90:65,FCBG410QB1C10-FC' : '40g-active',
                                '00:90:65,FCBG410QB1C15-FC' : '40g-active',
                                '00:90:65,FCBG410QB1C20-FC' : '40g-active',
                                '00:90:65,FCBG410QB1C30-FC' : '40g-active',
                                '00:90:65,FCBG410QB1C50-FC' : '40g-active',
                                '00:90:65,FCBG410QB1CX0-FC' : '40g-active' }
    ADDR_POWER_CTL = 93
    POWER_CTL_LPOVERRIDE_MASK = 0x3
    POWER_CTL_LPOVERRIDE_MAP = { 0 : 'high',
                                 1 : 'override-high',
                                 2 : 'low',
                                 3 : 'override-low' }
    POWER_CTL_LPOVERRIDE_RMAP = { v: k for k, v in POWER_CTL_LPOVERRIDE_MAP.items() }

    @classmethod
    def decode_ethcodes_base(self, ethcodes, hacks_dict, quirks_dict, eth_map_dict, ext):
        if ethcodes in hacks_dict.keys():
            hack = hacks_dict[ethcodes]
            return (hack,)

        if ethcodes in quirks_dict.keys():
            quirk = quirks_dict[ethcodes]
            log_throttle('enabling ethcode quirk: 0x%02x -> %s' % (ethcodes, quirk), level='info')
            return (quirk,)

        if ext == 0:
            return tuple(sorted([ eth_map_dict[x] for x in range(7)
                              if (1 << x) & ethcodes ]))
        elif ext == 1:
            return tuple([eth_map_dict[ethcodes]], )
        else:
            return tuple()

    @classmethod
    def decode_ethcodes(self, eeprom):
        data = eeprom[self.ADDR_ETH_CODE]
        if len(data) != 1:
            return None

        ethcodes = struct.unpack('B', data)[0]

        if ethcodes == 0:
            ouipn = '%s,%s' % (self.decode_vendor_oui(eeprom),
                               self.decode_vendor_pn(eeprom))
            if ouipn in self.ETH_CODE_ZERO_OVERRIDES.keys():
                override = self.ETH_CODE_ZERO_OVERRIDES[ouipn]
                log_throttle('enabling zero ethcode override: %s -> %s'
                             % (ouipn, override),
                             once=True, level='info')
                return (override,)

        base = self.decode_ethcodes_base(ethcodes, self.ETH_CODE_SANCTIONED_HACKS,
                                       self.ETH_CODE_QUIRKS, self.ETH_CODE_MAP, 0)

        extended = ()
        if (ethcodes & self.ETH_CODE_EXTENDED):
            data = eeprom[self.ADDR_ETH_EXTENDED_CODE]
            if len(data) == 1:
                ethcodes = struct.unpack('B', data)[0]
                extended = self.decode_ethcodes_base(ethcodes, self.ETH_EXTENDED_CODE_SANCTIONED_HACKS,
                                        self.ETH_EXTENDED_CODE_QUIRKS, self.ETH_EXTENDED_CODE_MAP, 1)
        return (extended + base)

class SFFCable(object):
    ADDR_IDENTIFIER = 0x0
    IDENTIFIER_MAP = { 0x03 : SFPDecoder,
                       0x0d : QSFPDecoder,
                       0x11 : QSFPDecoder }

    def __init__(self, eeprom):
        self.eeprom = eeprom
        self._ethcode_override_map = None

    def override_ethcodes(self, override_map):
        self._ethcode_override_map = override_map

    @property
    def check(self):
        try:
            test = self.eeprom[self.ADDR_IDENTIFIER]
            return True
        except Exception:
            return False
        return False

    @property
    def decoder(self):
        data = self.eeprom[self.ADDR_IDENTIFIER]
        if len(data) != 1:
            logger.error('failed to get identifier, got %u bytes' % len(data))
            return None

        identifier = struct.unpack('B', data)[0]
        if identifier not in self.IDENTIFIER_MAP.keys():
            logger.error('invalid SFF identifier: 0x%02x' % identifier)
            return None

        return self.IDENTIFIER_MAP[identifier]

    @property
    def ouipn(self):
        decoder = self.decoder
        if not decoder:
            return

        return '%s,%s' % (decoder.decode_vendor_oui(self.eeprom),
                          decoder.decode_vendor_pn(self.eeprom))

    @property
    def vendorpn(self):
        decoder = self.decoder
        if not decoder:
            return

        return '%s,%s' % (decoder.decode_vendor_name(self.eeprom),
                          decoder.decode_vendor_pn(self.eeprom))

    @property
    def ethcodes(self):
        if self._ethcode_override_map is not None:
            if self._ethcode_override_map.has_key('port'):
                override = self._ethcode_override_map['port']
                log_throttle('enabling port ethcode override: %s'
                             % (override[0]), once=True, level='info')
                return override
            elif self._ethcode_override_map.has_key(self.vendorpn):
                override = self._ethcode_override_map[self.vendorpn]
                log_throttle('enabling vendorpn ethcode override: %s -> %s'
                             % (self.vendorpn, override[0]), once=True,
                                level='info')
                return override

        decoder = self.decoder
        if not decoder:
            return tuple()

        return decoder.decode_ethcodes(self.eeprom)

    @property
    def connector(self):
        decoder = self.decoder
        if not decoder:
            return tuple()

        return decoder.decode_connector(self.eeprom)

    def _get_lpoverride(self):
        decoder = self.decoder
        if not decoder:
            return None

        return decoder.decode_lpoverride(self.eeprom)

    def _set_lpoverride(self, value):
        decoder = self.decoder
        if not decoder:
            return None

        return decoder.encode_lpoverride(self.eeprom, value)

    lpoverride = property(_get_lpoverride, _set_lpoverride)

    @property
    def length(self):
        raise NotImplementedError

    @property
    def vendor_oui(self):
        decoder = self.decoder
        if not decoder:
            return None

        return decoder.decode_vendor_oui(self.eeprom)

    @property
    def vendor_pn(self):
        decoder = self.decoder
        if not decoder:
            return None

        return decoder.decode_vendor_pn(self.eeprom)

    @property
    def vendor_name(self):
        decoder = self.decoder
        if not decoder:
            return None

        return decoder.decode_vendor_name(self.eeprom)

class Phy(object):
    C45_CTRL_ADDR       = 0
    C45_CTRL_RESET      = 0x0080
    C45_CTRL_POWER_DOWN = 0x0008
    C45_CTRL_AUTONEG    = 0x0010

    C45_CTRL_SPEED_MASK = 0x4020
    C45_CTRL_SPEED_100  = 0x0020
    C45_CTRL_SPEED_1000 = 0x4000

    C45_CTRL_DUPLEX_FULL = 0x0001

    C45_STATUS_ADDR     = 1
    C45_STATUS_LINK     = 0x0400

    C45_ID1_ADDR        = 2
    C45_ID2_ADDR        = 3

    C45_AN_CTL_ADDR     = 4
    C45_AN_CTL_MASK     = 0xe00f
    C45_AN_CTL_ALL      = 0x4001 # 10/100 full duplex only

    C45_1000BT_CTL_ADDR = 9
    C45_1000BT_CTL_MASK = 0x00ff
    C45_1000BT_CTL_ALL  = 0x000e # 1000 full duplex only, auto master

    C45_VENDOR_ADDR     = 16
    C45_VENDOR_ADDR2    = 17

    C45_PAGE_SELECT     = 22
    C45_PAGE_COPPER     = 0x0000
    C45_PAGE_FIBER      = 0x0100

    C45_VENDOR_STATUS_EXT = 27

    @property
    def phyid(self):
        return (self.read(self.C45_ID1_ADDR), self.read(self.C45_ID2_ADDR))

    @property
    def probe(self):
        phyid = self.phyid
        return phyid[0] == self.id1 and phyid[1] in self.id2

    @property
    def link(self):
        # Read back-to-back to get current link status.
        status = self.read(self.C45_STATUS_ADDR)
        status = self.read(self.C45_STATUS_ADDR)
        return (status & self.C45_STATUS_LINK) != 0

    def reset(self, wait=True):
        reg = self.read(self.C45_CTRL_ADDR)
        self.write(self.C45_CTRL_ADDR, reg | self.C45_CTRL_RESET)

        if wait:
            while self.read(self.C45_CTRL_ADDR) & self.C45_CTRL_RESET != 0:
                logger.debug('waiting for phy reset')
                time.sleep(.1)

    def set_power(self, power):
        reg = self.read(self.C45_CTRL_ADDR)

        if power and (reg & self.C45_CTRL_POWER_DOWN):
            self.write(self.C45_CTRL_ADDR, reg & ~self.C45_CTRL_POWER_DOWN)
            return True
        elif not power and (reg & self.C45_CTRL_POWER_DOWN == 0):
            self.write(self.C45_CTRL_ADDR, reg | self.C45_CTRL_POWER_DOWN)
            return True

        return False

    def set_1000auto(self):
        changed = False

        reg = self.read(self.C45_CTRL_ADDR)
        mask = ~(self.C45_CTRL_SPEED_MASK | self.C45_CTRL_DUPLEX_FULL)
        new = (reg & mask) | (self.C45_CTRL_SPEED_1000 | self.C45_CTRL_DUPLEX_FULL)
        if reg != new:
            self.write(self.C45_CTRL_ADDR, new)
            logger.debug('speed select changed from 0x%04x to 0x%04x' % (reg, new))
            changed = True

        reg = self.read(self.C45_1000BT_CTL_ADDR)
        if (reg & self.C45_1000BT_CTL_MASK) != self.C45_1000BT_CTL_ALL:
            new = (reg & ~self.C45_1000BT_CTL_MASK) | self.C45_1000BT_CTL_ALL
            logger.debug('1000bt changed from 0x%04x to 0x%04x' % (reg, new))
            self.write(self.C45_1000BT_CTL_ADDR, new)
            changed = True

        return changed

    def set_100fd(self):
        reg = self.read(self.C45_CTRL_ADDR)
        mask = ~(self.C45_CTRL_SPEED_MASK | self.C45_CTRL_DUPLEX_FULL)
        newreg = (reg & mask) | (self.C45_CTRL_SPEED_100 | self.C45_CTRL_DUPLEX_FULL)

        if reg != newreg:
            self.write(self.C45_CTRL_ADDR, newreg)
            return True

        return False

    def enable_mdix(self):
        reg = self.read(self.C45_VENDOR_ADDR)
        newreg = reg | self.C45_VENDOR_MDIX

        if reg != newreg:
            self.write(self.C45_VENDOR_ADDR, newreg)
            return True

        return False

    @property
    def power(self):
        return (self.read(self.C45_CTRL_ADDR) & self.C45_CTRL_POWER_DOWN) == 0

    def select_page(self, page):
        self.write(self.C45_PAGE_SELECT, page)

    def set_an(self, page, an):
        assert(page in ('copper', 'fiber'))

        if page != 'copper':
            self.select_page(self.C45_PAGE_FIBER)

        reg = self.read(self.C45_CTRL_ADDR)
        changed = False

        if an:
            if (reg & self.C45_CTRL_AUTONEG) == 0:
                self.write(self.C45_CTRL_ADDR, reg | self.C45_CTRL_AUTONEG)
                changed = True
        else:
            if (reg & self.C45_CTRL_AUTONEG) != 0:
                self.write(self.C45_CTRL_ADDR, reg & ~self.C45_CTRL_AUTONEG)
                changed = True

        if page != 'copper':
            self.select_page(self.C45_PAGE_COPPER)
        return changed

class I2CPhy(Phy):
    def __init__(self, i2c):
        self.i2c = i2c

    def read(self, addr):
        data = self.i2c.read_word_data(self.i2c_addr, addr)
        return data

    def write(self, addr, data):
        rv = self.i2c.write_word_data(self.i2c_addr, addr, data)
        return rv

class MV88E1111Phy(I2CPhy):
    revisions = { 0xc10c : '1',
                  0xc20c : '2' }
    i2c_addr = 0x56
    id1 = 0x4101
    id2 = revisions.keys()

    MV88E1111_EXT_STATUS_ADDR = 27
    MV88E1111_EXT_STATUS_HWCONFIG_MASK = 0x0f00
    MV88E1111_EXT_STATUS_HWCONFIG_1GX_NOCLK_NOAN = 0x0c00
    MV88E1111_EXT_STATUS_HWCONFIG_SGMII_NOCLK = 0x0400

    C45_VENDOR_ENERGY_DETECT = 0x0003
    C45_VENDOR_MAC_ALWAYS_UP = 0x0800
    C45_VENDOR_MDIX = 0x6000

    C45_VENDOR_LINK = 0x0004

    C45_VENDOR_STATUS_EXT_AN_BYPASS = 0x0010

    @property
    def revision(self):
        return self.revisions.get(self.phyid[1], 'Unknown')

    @property
    def link(self):
        status = self.read(self.C45_VENDOR_ADDR2)
        return (status & self.C45_VENDOR_LINK) != 0

    def dump_regs(self):
        out = []
        for page in (self.C45_PAGE_FIBER, self.C45_PAGE_COPPER):
            self.select_page(page)
            for reg in range(11) + range(15, 31):
                logger.debug('page: %u reg: %u value: 0x%04x' % (page, reg, self.read(reg)))

    def enable_energy_detect_plus(self):
        reg = self.read(self.C45_VENDOR_ADDR)

        # enable energy detect plus, allow mac interface to power down
        if (reg & (self.C45_VENDOR_ENERGY_DETECT |
                   self.C45_VENDOR_MAC_ALWAYS_UP) !=
            self.C45_VENDOR_ENERGY_DETECT):

            reg &= ~self.C45_VENDOR_MAC_ALWAYS_UP
            reg |= self.C45_VENDOR_ENERGY_DETECT
            self.write(self.C45_VENDOR_ADDR, reg)
            return True

        return False

    def set_autoneg_bypass(self, enabled, page=None):
        rv = False

        if page is None:
            rv |= self.set_autoneg_bypass(enabled, page=self.C45_PAGE_COPPER)
            rv |= self.set_autoneg_bypass(enabled, page=self.C45_PAGE_FIBER)
            return rv

        if page != self.C45_PAGE_COPPER:
            self.select_page(page)

        reg = self.read(self.C45_VENDOR_STATUS_EXT)

        if enabled:
            if (reg & self.C45_VENDOR_STATUS_EXT_AN_BYPASS) == 0:
                self.write(self.C45_VENDOR_STATUS_EXT,
                           reg & self.C45_VENDOR_STATUS_EXT_AN_BYPASS)
                rv = True
        else:
            if (reg & self.C45_VENDOR_STATUS_EXT_AN_BYPASS) != 0:
                self.write(self.C45_VENDOR_STATUS_EXT,
                           reg & ~self.C45_VENDOR_STATUS_EXT_AN_BYPASS)
                rv = True

        if page != 0:
            self.select_page(0)

        return rv

    def sgmii_mode(self, port=None):
        reg = self.read(self.MV88E1111_EXT_STATUS_ADDR)
        hwconfig = reg & self.MV88E1111_EXT_STATUS_HWCONFIG_MASK

        if hwconfig == 0:
            logger.debug('resetting MV88E1111 phy')
            port.driver.toggle_reset()
            self.reset()

        if (hwconfig != self.MV88E1111_EXT_STATUS_HWCONFIG_SGMII_NOCLK):
            reg &= ~self.MV88E1111_EXT_STATUS_HWCONFIG_MASK
            reg |= self.MV88E1111_EXT_STATUS_HWCONFIG_SGMII_NOCLK
            self.write(self.MV88E1111_EXT_STATUS_ADDR, reg)
            return True

        return False

    def gbic_noan_mode(self, port=None):
        reg = self.read(self.MV88E1111_EXT_STATUS_ADDR)
        hwconfig = reg & self.MV88E1111_EXT_STATUS_HWCONFIG_MASK

        if hwconfig == 0:
            logger.debug('resetting MV88E1111 phy')
            port.driver.toggle_reset()
            self.reset()

        if (hwconfig != self.MV88E1111_EXT_STATUS_HWCONFIG_1GX_NOCLK_NOAN):
            reg &= ~self.MV88E1111_EXT_STATUS_HWCONFIG_MASK
            reg |= self.MV88E1111_EXT_STATUS_HWCONFIG_1GX_NOCLK_NOAN
            self.write(self.MV88E1111_EXT_STATUS_ADDR, reg)
            return True

        return False


class Retimer(object):
    def __init__(self, port, path):
        self.port = port
        self.path = path
        self.ctype=ctypes.c_uint32
        self.speed = []

    @property
    def set_speed(self):
        raise NotImplementedError

class DS100DF410(Retimer):

    CDR_OVERRIDE = 0x20
    RAW_DATA_INV = 0XE0

    def __init__(self, port, path):
        Retimer.__init__(self, port, path)

        self.channel_path = self.path + '/channels'
        self.pfd_path = self.path + '/pfd_prbs_dfe'
        self.override_path = self.path + '/override'
        self.reset_path = self.path + '/reset'

        if sock is None or sockfd is None:
            socket_init()

    def _read(self, sysfs_path):
        f = open(sysfs_path, 'r')
        value = int(f.read(), base=0)
        f.close()
        return value

    def _write(self, sysfs_path, value):
        f = open(sysfs_path, 'w')
        f.write('0x%x\n' % self.ctype(value).value)

    def _set_speed_1G(self):
        pfd = self._read(self.pfd_path)
        override = self._read(self.override_path)

        # For 1GbE applications, it is required to bypass the CDR by
        # setting the override bit 5 of register 0x09 to 1, and set
        # the data mux bits [7:5] to 3'b000 of register 0x1E.

        override |= self.CDR_OVERRIDE
        pfd &= ~ self.RAW_DATA_INV

        self._write(self.pfd_path, pfd)
        self._write(self.override_path, override)

    def _set_speed_10G_40G(self):
        pfd = self._read(self.pfd_path)
        override = self._read(self.override_path)

        override &= ~ self.CDR_OVERRIDE
        pfd |= self.RAW_DATA_INV

        self._write(self.pfd_path, pfd)
        self._write(self.override_path, override)

    def _set_channel(self, sub):
        if sub == 's0':
            channel = 0x04
        elif sub == 's1':
            channel = 0x05
        elif sub == 's2':
            channel = 0x06
        elif sub == 's3':
            channel = 0x07
        elif sub == '':
            channel = 0x0c
        else:
            raise NoSuchPort('failed to get channel for port: swp%u%s' % (self.port, sub))

        self._write(self.channel_path, channel)

    def _set_channel_reset(self, sub):
        self._write(self.reset_path, 0x0f)

    def _set_channel_speed(self, sub, speed):
        self._set_channel(sub)
        if speed == 1000:
            self._set_speed_1G()
        else:
            self._set_speed_10G_40G()

    def set_speed(self, speed):
        if len (speed) > 1 :
            for i in range(len(speed)):
                sub = 's%s' % i
                self._set_channel_speed(sub, speed[i])
        else:
            self._set_channel_speed('', speed)

        self.speed = speed

class SimpleCPLDSFPDriver(SysFSPortDriver):
    _base = None

    def __init__(self, control_bit, port):
        SysFSPortDriver.__init__(self, reset=FalseSignal(),
                                       lpmod=FalseSignal(),
                                       present=BinaryFileSignal(self._base + '/sfp_present',
                                                                ctype=ctypes.c_uint64,
                                                                bit=control_bit,
                                                                active='high'),
                                       tx_disable=BinaryFileSignal(self._base + '/sfp_tx_enable',
                                                                ctype=ctypes.c_uint64,
                                                                bit=control_bit,
                                                                active='low'),
                                       tx_fault=BinaryFileSignal(self._base + '/sfp_tx_fault',
                                                                 ctype=ctypes.c_uint64,
                                                                 bit=control_bit,
                                                                 active='high'),
                                       rx_los=BinaryFileSignal(self._base + '/sfp_rx_los',
                                                               ctype=ctypes.c_uint64,
                                                               bit=control_bit,
                                                               active='low'),
                                       cable=SFFCable(self._find_eeprom(port)))

class SimpleCPLDQSFPDriver(SysFSPortDriver):
    _base = None

    def __init__(self, control_bit, port):
        SysFSPortDriver.__init__(self, reset=BinaryFileSignal(self._base + '/qsfp_reset',
                                                              ctype=ctypes.c_uint32,
                                                              bit=control_bit,
                                                              active='low'),
                                       lpmod=BinaryFileSignal(self._base + '/qsfp_lpmod',
                                                              ctype=ctypes.c_uint32,
                                                              bit=control_bit,
                                                              active='high'),
                                       present=BinaryFileSignal(self._base + '/qsfp_present',
                                                                ctype=ctypes.c_uint32,
                                                                bit=control_bit,
                                                                active='low'),
                                       tx_disable=FalseSignal(),
                                       tx_fault=FalseSignal(),
                                       rx_los=FalseSignal(),
                                       cable=SFFCable(self._find_eeprom(port)))

class CelSmallstoneQSFPDriver(SimpleCPLDQSFPDriver):
    _base = '/sys/devices/ffe05000.localbus/ffb40000.CPLD23'

class CelRedstoneQSFPDriver(SysFSPortDriver):
    _base = '/sys/devices/ffe05000.localbus/ffb40000.CPLD234'

    def __init__(self, sysfs_file, port):
        SysFSPortDriver.__init__(self, reset=BinaryFileSignal(self._base + '/' + sysfs_file,
                                                              ctype=ctypes.c_uint8,
                                                              bit=0,
                                                              active='low'),
                                       lpmod=BinaryFileSignal(self._base + '/' + sysfs_file,
                                                              ctype=ctypes.c_uint8,
                                                              bit=1,
                                                              active='high'),
                                       present=BinaryFileSignal(self._base + '/' + sysfs_file,
                                                                ctype=ctypes.c_uint8,
                                                                bit=2,
                                                                active='low'),
                                       cable=SFFCable(self._find_eeprom(port)))

class CelSmallstoneXPQSFPDriver(SimpleCPLDQSFPDriver):
        _base = '/sys/devices/platform/cel_smallstone_xp_cpld.0'
        def __init__(self, control_bit, port):
                SysFSPortDriver.__init__(self, reset=BinaryFileSignal(self._base + '/qsfp_reset',
                                                                      ctype=ctypes.c_uint32,
                                                                      bit=control_bit,
                                                                      active='high'),
                                         lpmod=BinaryFileSignal(self._base + '/qsfp_lp_mode',
                                                                ctype=ctypes.c_uint32,
                                                                bit=control_bit,
                                                                active='high'),
                                         present=BinaryFileSignal(self._base + '/qsfp_present',
                                                                  ctype=ctypes.c_uint32,
                                                                  bit=control_bit,
                                                                  active='high'),
                                         cable=SFFCable(self._find_eeprom(port)))

class CelRedstoneXPQSFPDriver(SimpleCPLDQSFPDriver):
    _base = '/sys/devices/platform/cel_redstone_xp_cpld.0'
    def __init__(self, control_bit, port):
        SysFSPortDriver.__init__(self, reset=BinaryFileSignal(self._base + '/qsfp_reset',
                                                              ctype=ctypes.c_uint8,
                                                              bit=control_bit,
                                                              active='high'),
                                       lpmod=BinaryFileSignal(self._base + '/qsfp_lp_mode',
                                                              ctype=ctypes.c_uint8,
                                                              bit=control_bit,
                                                              active='high'),
                                       present=BinaryFileSignal(self._base + '/qsfp_present',
                                                                ctype=ctypes.c_uint8,
                                                                bit=control_bit,
                                                                active='high'),
                                       cable=SFFCable(self._find_eeprom(port)))


class AS6701FixedQSFPDriver(SysFSPortDriver):
    _base = '/sys/devices/soc.0/ffe03100.i2c'

    def _find(self, signal):
        for root, dirs, files in os.walk(self._base):
            if signal in files:
                return os.path.join(root, signal)

    def __init__(self, control_bit, port):
        SysFSPortDriver.__init__(self, reset=BinaryFileSignal(self._find('qsfp_reset'),
                                                              ctype=ctypes.c_uint32,
                                                              bit=control_bit,
                                                              active='low'),
                                       lpmod=None,
                                       present=BinaryFileSignal(self._find('qsfp_present'),
                                                                ctype=ctypes.c_uint32,
                                                                bit=control_bit,
                                                                active='low'),
                                       cable=SFFCable(self._find_eeprom(port)),
                                       tx_retimer=DS100DF410(port, self._find_retimer(port, 'tx')),
                                       rx_retimer=DS100DF410(port, self._find_retimer(port, 'rx')))


class AS6701ModuleQSFPDriver(GPIOPortDriver):
    _reset_map = [ 1, 0, 3, 2, 5, 4 ];
    _lpmod_map = range(6, 12)
    _modsel_map = range(12, 18)
    _present_map = range(18, 24)
    _interrupt_map = range(24, 30)
    def __init__(self, module, module_port, port):
        module_base_pin = 184 - (module * 40)
        GPIOPortDriver.__init__(self, reset=GPIOSignal(module_base_pin + self._reset_map[module_port],
                                                       active='low',
                                                       direction='out'),
                                       lpmod=None,
                                       present=GPIOSignal(module_base_pin + self._present_map[module_port],
                                                          active='low',
                                                          direction='in'),
                                       cable=SFFCable(self._find_eeprom(port)),
                                       tx_retimer=DS100DF410(port, self._find_retimer(port, 'tx')),
                                       rx_retimer=DS100DF410(port, self._find_retimer(port, 'rx')))


class AS5712QSFPDriver(SimpleCPLDQSFPDriver):
    _base = '/sys/devices/platform/accton_as5712_54x_cpld.0/'
    def __init__(self, control_bit, port):
        SysFSPortDriver.__init__(self, reset=BinaryFileSignal(self._base + '/qsfp_reset',
                                                              ctype=ctypes.c_uint8,
                                                              bit=control_bit,
                                                              active='high'),
                                       lpmod=BinaryFileSignal(self._base + '/qsfp_lp_mode',
                                                              ctype=ctypes.c_uint8,
                                                              bit=control_bit,
                                                              active='high'),
                                       present=BinaryFileSignal(self._base + '/qsfp_present',
                                                                ctype=ctypes.c_uint8,
                                                                bit=control_bit,
                                                                active='high'),
                                       cable=SFFCable(self._find_eeprom(port)))

class AS5812QSFPDriver(SimpleCPLDQSFPDriver):
    _base = '/sys/devices/platform/accton_as5812_54t_cpld.0/'
    def __init__(self, control_bit, port):
        SysFSPortDriver.__init__(self, reset=BinaryFileSignal(self._base + '/qsfp_reset',
                                                              ctype=ctypes.c_uint8,
                                                              bit=control_bit,
                                                              active='high'),
                                       lpmod=BinaryFileSignal(self._base + '/qsfp_lp_mode',
                                                              ctype=ctypes.c_uint8,
                                                              bit=control_bit,
                                                              active='high'),
                                       present=BinaryFileSignal(self._base + '/qsfp_present',
                                                                ctype=ctypes.c_uint8,
                                                                bit=control_bit,
                                                                active='high'),
                                       cable=SFFCable(self._find_eeprom(port)))

class AS6712QSFPDriver(SimpleCPLDQSFPDriver):
        _base = '/sys/devices/platform/accton_as6712_32x_cpld.0'
        def __init__(self, control_bit, port):
                SysFSPortDriver.__init__(self, reset=BinaryFileSignal(self._base + '/qsfp_reset',
                                                                      ctype=ctypes.c_uint32,
                                                                      bit=control_bit,
                                                                      active='high'),
                                         lpmod=BinaryFileSignal(self._base + '/qsfp_lp_mode',
                                                                ctype=ctypes.c_uint32,
                                                                bit=control_bit,
                                                                active='high'),
                                         present=BinaryFileSignal(self._base + '/qsfp_present',
                                                                  ctype=ctypes.c_uint32,
                                                                  bit=control_bit,
                                                                  active='high'),
                                         cable=SFFCable(self._find_eeprom(port)))

class DellS6000QSFPDriver(SimpleCPLDQSFPDriver):
    _base = '/sys/devices/platform/dell_s6000_cpld.0'

class DellS6010QSFPDriver(SimpleCPLDQSFPDriver):
    _base = '/sys/devices/platform/dell_s6010_cpld.0'

class DellS4000SFPDriver(SysFSPortDriver):
    _base = '/sys/devices/platform/dell_s4000_cpld.0'
    def __init__(self, control_bit, port):
        SysFSPortDriver.__init__(self, present=BinaryFileSignal(self._base + '/sfp_present',
                                                                ctype=ctypes.c_uint64,
                                                                bit=control_bit,
                                                                active='high'),
                                       tx_disable=BinaryFileSignal(self._base + '/sfp_tx_enable',
                                                                ctype=ctypes.c_uint64,
                                                                bit=control_bit,
                                                                active='low'),
                                       tx_fault=BinaryFileSignal(self._base + '/sfp_tx_fault',
                                                                 ctype=ctypes.c_uint64,
                                                                 bit=control_bit,
                                                                 active='high'),
                                       rx_los=BinaryFileSignal(self._base + '/sfp_rx_los',
                                                               ctype=ctypes.c_uint64,
                                                               bit=control_bit,
                                                               active='low'),
                                       cable=SFFCable(self._find_eeprom(port)))

class DellS3000SFPDriver(SysFSPortDriver):
    _base = '/sys/devices/platform/dell_s3000_cpld.0'
    def __init__(self, control_bit, port):
        eeprom = SysFSPortDriver._find_eeprom(port)
        # mod_abs isn't exposed by this CPLD driver, use EEPROM timeout to detect presence
        SysFSPortDriver.__init__(self, present=TimeoutPresenceSignal(eeprom),
                                       tx_disable=BinaryFileSignal(self._base + '/sfp_tx_enable',
                                                                ctype=ctypes.c_uint8,
                                                                bit=control_bit,
                                                                active='low'),
                                       tx_fault=FalseSignal(),
                                       rx_los=FalseSignal(),
                                       cable=SFFCable(eeprom))

class CelE1031SFPDriver(DellS3000SFPDriver):
    _base = '/sys/devices/platform/cel_e1031_cpld.0'

class DellS4000QSFPDriver(SysFSPortDriver):
    _base = '/sys/devices/platform/dell_s4000_cpld.0'
    def __init__(self, control_bit, port):
        SysFSPortDriver.__init__(self, reset=BinaryFileSignal(self._base + '/qsfp_reset',
                                                              ctype=ctypes.c_uint8,
                                                              bit=control_bit,
                                                              active='high'),
                                       lpmod=BinaryFileSignal(self._base + '/qsfp_lp_mode',
                                                              ctype=ctypes.c_uint8,
                                                              bit=control_bit,
                                                              active='high'),
                                       present=BinaryFileSignal(self._base + '/qsfp_present',
                                                                ctype=ctypes.c_uint8,
                                                                bit=control_bit,
                                                                active='high'),
                                       cable=SFFCable(self._find_eeprom(port)))

class DellS4048TQSFPDriver(SysFSPortDriver):
    _base = '/sys/devices/platform/dell_s4048t_cpld.0'
    def __init__(self, control_bit, port):
        SysFSPortDriver.__init__(self, reset=BinaryFileSignal(self._base + '/qsfp_reset',
                                                              ctype=ctypes.c_uint8,
                                                              bit=control_bit,
                                                              active='high'),
                                       lpmod=BinaryFileSignal(self._base + '/qsfp_lp_mode',
                                                              ctype=ctypes.c_uint8,
                                                              bit=control_bit,
                                                              active='high'),
                                       present=BinaryFileSignal(self._base + '/qsfp_present',
                                                                ctype=ctypes.c_uint8,
                                                                bit=control_bit,
                                                                active='high'),
                                       cable=SFFCable(self._find_eeprom(port)))

class QuantaLY2RQSFPDriver(GPIOPortDriver):
    def __init__(self, base_pin, port):
        GPIOPortDriver.__init__(self, reset=GPIOSignal(base_pin + 3,
                                                       active='low',
                                                       direction='out'),
                                      lpmod=None,
                                      present=GPIOSignal(base_pin + 2,
                                                         active='low',
                                                         direction='in'),
                                      cable=SFFCable(self._find_eeprom(port)))

class QuantaLY8QSFPDriver(GPIOPortDriver):
    def __init__(self, port):
        name = 'qsfp%s' %port
        GPIOPortDriver.__init__(self, reset=GPIOSignalNamed(name + '_reset',
                                                        active='low'),
                                      lpmod=GPIOSignalNamed(name + '_lpmode',
                                                        active='high'),
                                      present=GPIOSignalNamed(name + '_present',
                                                        active='low'),
                                      cable=SFFCable(self._find_eeprom(port)))

class QuantaLY9RangeleyQSFPDriver(SysFSPortDriver):
    _base = '/sys/devices/pci0000:00/0000:00:1f.3'

    def _find(self, signal):
        return SysFSPortDriver._find(self._base, signal)

    def __init__(self, port):
        name = 'qsfp%s' %port
        SysFSPortDriver.__init__(self,
                                 reset=BinaryFileSignal(self._find(name + '_reset'),
                                                             ctype=ctypes.c_uint32,
                                                             bit=0,
                                                             active='low'),
                                  lpmod=BinaryFileSignal(self._find(name + '_lpmode'),
                                                             ctype=ctypes.c_uint32,
                                                             bit=0,
                                                             active='high'),
                                  present=BinaryFileSignal(self._find(name + '_present'),
                                                             ctype=ctypes.c_uint32,
                                                             bit=0,
                                                             active='low'),
                                  cable=SFFCable(self._find_eeprom(port)))

class QuantaLY6QSFPDriver(SimpleCPLDQSFPDriver):

    def __init__(self, base, control_bit, port, reset, lp_mode, present):
        SysFSPortDriver.__init__(self, reset=BinaryFileSignal(self._find(base, reset),
                                                              ctype=ctypes.c_uint16,
                                                              bit=control_bit,
                                                              active='high'),
                                       lpmod=BinaryFileSignal(self._find(base, lp_mode),
                                                              ctype=ctypes.c_uint16,
                                                              bit=control_bit,
                                                              active='high'),
                                       present=BinaryFileSignal(self._find(base, present),
                                                                ctype=ctypes.c_uint16,
                                                                bit=control_bit,
                                                                active='high'),
                                       cable=SFFCable(self._find_eeprom(port)))

class DellZ9100ZQSFPDriver(SimpleCPLDQSFPDriver):
    _base = '/sys/bus/platform/devices/z9100_cpld.0'
    def __init__(self, control_bit, port):
                SysFSPortDriver.__init__(self, reset=BinaryFileSignal(self._base + '/zqsfp_reset',
                                                                      ctype=ctypes.c_uint32,
                                                                      bit=control_bit,
                                                                      active='high'),
                                         lpmod=BinaryFileSignal(self._base + '/zqsfp_lpmod',
                                                                ctype=ctypes.c_uint32,
                                                                bit=control_bit,
                                                                active='high'),
                                         present=BinaryFileSignal(self._base + '/zqsfp_present',
                                                                  ctype=ctypes.c_uint32,
                                                                  bit=control_bit,
                                                                  active='high'),
                                         cable=SFFCable(self._find_eeprom(port)))

class CelSeastoneZQSFPDriver(SimpleCPLDQSFPDriver):
    _base = '/sys/devices/platform/cel_seastone_cpld.0'
    def __init__(self, control_bit, port):
                SysFSPortDriver.__init__(self, reset=BinaryFileSignal(self._base + '/qsfp_reset',
                                                                      ctype=ctypes.c_uint32,
                                                                      bit=control_bit,
                                                                      active='high'),
                                         lpmod=BinaryFileSignal(self._base + '/qsfp_lp_mode',
                                                                ctype=ctypes.c_uint32,
                                                                bit=control_bit,
                                                                active='high'),
                                         present=BinaryFileSignal(self._base + '/qsfp_present',
                                                                  ctype=ctypes.c_uint32,
                                                                  bit=control_bit,
                                                                  active='high'),
                                         cable=SFFCable(self._find_eeprom(port)))

class BCMConfig(PortConfig):
    _compliance_map = { '100g-active': 'cr',
                        '100g-lr4'   : 'sr',
                        '100g-sr4'   : 'sr',
                        '100g-cr4'   : 'cr',
                        '40g-active' : 'sr',
                        '40g-lr4'    : 'sr',
                        '40g-sr4'    : 'sr',
                        '40g-cr4'    : 'cr',
                        '10g-sr'     : 'sr',
                        '10g-lr'     : 'sr',
                        '10g-lrm'    : 'sr',
                        '10g-er'     : 'sr',
                        '10g-cr'     : 'cr',
                        '1000base-sx': 'sr',
                        '1000base-lx': 'lr',
                        '1000base-cx': 'cr',
                        '1000base-t' : 'sr' }
    # XGMII is the default BCM SDK interface mode, but should never be
    # configured.
    _switchd_map = { 'XGMII' : None,
                     'XLAUI' : 'xlaui',
                     'SFI'   : 'cr',
                     'XFI'   : 'sr',
                     'CR'    : 'cr',
                     'CR2'   : 'cr',
                     'CR4'   : 'cr',
                     'SR'    : 'sr',
                     'SR2'   : 'sr',
                     'SR4'   : 'sr',
                     'LR'    : 'lr',
                     'LR2'   : 'lr',
                     'LR4'   : 'lr',
                     'ZR'    : 'lr' }

    _iftype_map = { 'XGMII' : 6 ,
                    'XLAUI' : 15,
                    'SFI'   : 9 ,
                    'XFI'   : 10,
                    'CR'    : 13,
                    'CR4'   : 14,
                    'SR'    : 16,
                    'SR4'   : 28,
                    'LR'    : 26,
                    'LR4'   : 27,
                    'ZR'    : 30,
                    'SR2'   : 38,
                    'CR2'   : 40,
                    'LR2'   : 55 }

    def __init__(self, port_num, port_type, phyless, chip):
        self.port_num = port_num
        self.port_type = port_type
        self.phyless = phyless
        self.chip = chip

        self._bcmspeed_re = re.compile(r'\(([0-9.]+G)\)', re.MULTILINE)

        if self.port_type not in ('SFP+', 'QSFP+', 'QSFP28'):
            raise NotImplementedError

        if self.phyless != True:
            raise NotImplementedError

        if self.chip not in ('Trident', 'Trident2','Trident2Plus' ,'Tomahawk'):
            raise NotImplementedError

    def is_switchd_running(self):
        # Return code is 0, when switchd is running and we can access bcmshell.
        if (int(subprocess.call(["systemctl", "-q",  "is-active", "switchd"])) != 0):
            return False
        return True

    def bcm_interfaces_trident(self):
        porttab = cumulus.porttab.porttab(use_hw=False)
        try:
            return (porttab.linux2bcm('swp%u' % self.port_num), )
        except cumulus.porttab.NoSuchLinuxInterface:
            pass

        try:
            return tuple([ porttab.linux2bcm('swp%us%u' % (self.port_num, x)) for x in range(4) ])
        except cumulus.porttab.NoSuchLinuxInterface:
            pass

        try:
            return tuple([ porttab.linux2bcm('swp%us%u' % (self.port_num, x)) for x in range(2) ])
        except cumulus.porttab.NoSuchLinuxInterface:
            pass

        return None

    def bcm_interfaces_tomahawk(self):
        porttab = None
        if self.is_switchd_running():
            porttab = cumulus.porttab.porttab(use_hw=True)
        else:
            porttab = cumulus.porttab.porttab(use_hw=False)
        try:
            return (porttab.linux2logical('swp%u' % self.port_num), )
        except cumulus.porttab.NoSuchLinuxInterface:
            pass

        try:
            return tuple([ porttab.linux2logical('swp%us%u' % (self.port_num, x)) for x in range(4) ])
        except cumulus.porttab.NoSuchLinuxInterface:
            pass

        try:
            return tuple([ porttab.linux2logical('swp%us%u' % (self.port_num, x)) for x in range(2) ])
        except cumulus.porttab.NoSuchLinuxInterface:
            pass

        return None

    def _bcmcmd_port_id(self, bcm):
        if self.chip.startswith('Tomahawk'):
            return '0x%x' % (1 << int(bcm))
        else:
            return bcm

    @property
    def bcm_interfaces(self):
        if self.chip.startswith('Trident'):
            return self.bcm_interfaces_trident()
        elif self.chip.startswith('Tomahawk'):
            return self.bcm_interfaces_tomahawk()
        return None

    @property
    def gang_size(self):
        assert(self.port_type.startswith('SFP'))

        porttab = cumulus.porttab.porttab()

        # Find lane2 and lane3 for our MAC core by rounding down to a multiple
        # of four.
        second_member = ((self.port_num - 1) & ~0x3) + 2
        third_member = second_member + 1

        try:
            intf = 'swp%u' % third_member
            porttab.linux2bcm(intf)
        except cumulus.porttab.NoSuchLinuxInterface:
            # third member doesn't exist, therefore our lanes are ganged /4
            return 4

        try:
            intf = 'swp%u' % second_member
            porttab.linux2bcm(intf)
        except cumulus.porttab.NoSuchLinuxInterface:
            # second member doesn't exist, therefore our lanes are ganged /2
            return 2

        return 1

    @property
    def num_lanes(self):
        # How many lanes are assigned to each interface in a quad.

        if self.port_type.startswith('QSFP'):
            num_interfaces = len(self.linux_interfaces)
            if num_interfaces == 4:
                # must be a qsfp->sfp breakout cable
                return 1 # one lane per interface

            if num_interfaces == 2:
                # must be a 100G to 2x50G breakout cable
                return 2 # two lanes per interface

            # must be a qsfp with no breakout
            return 4 # four lanes per interface
        else:
            return self.gang_size

    @property
    def linux_interfaces(self):
        porttab = cumulus.porttab.porttab(use_hw=False)
        try:
            intf = 'swp%u' % self.port_num
            porttab.linux2bcm(intf)
            return (intf, )
        except cumulus.porttab.NoSuchLinuxInterface:
            pass

        try:
            intfs = [ 'swp%us%u' % (self.port_num, x) for x in range(4) ]
            for intf in intfs:
                porttab.linux2bcm(intf)
            return tuple(intfs)
        except cumulus.porttab.NoSuchLinuxInterface:
            pass

        try:
            intfs = [ 'swp%us%u' % (self.port_num, x) for x in range(2) ]
            for intf in intfs:
                porttab.linux2bcm(intf)
            return tuple(intfs)
        except cumulus.porttab.NoSuchLinuxInterface:
            pass

        return None

    def _set_swrxlos(self, mode):
        interfaces = self.bcm_interfaces
        if interfaces is None:
            return

        on = self._ifmode2swrxlos(mode, len(interfaces))
        bcm = bcmshell.bcmshell()
        for intf in interfaces:
            try:
                bcm.run('phy control %s swrxlos=%u' %
                        (self._bcmcmd_port_id(intf), on))
            except IOError:
                raise BCMNotAvailable

    def _ifmode2swrxlos(self, mode, num_lanes):
        assert(self.port_type in ('SFP+', 'QSFP+', 'QSFP28'))

        if mode == None:
            return False

        if self.port_type == 'SFP+':
            return False

        assert(self.port_type.startswith('QSFP'))
        if mode.upper() in ('SR', 'LR'):
            media = 'fiber'
        else:
            media = 'copper'

        if media == 'fiber':
            return True

        return False

    swrxlos = property(None, _set_swrxlos)

    def bcm_speed(self, intf):
        bcm = bcmshell.bcmshell()
        try:
            output = bcm.run('port %s speed'
                             % self._bcmcmd_port_id(intf))
        except IOError:
            raise BCMNotAvailable

        try:
            speed = self._bcmspeed_re.search(output).group(1)
        except AttributeError as e:
            raise RuntimeError('failed to match bcm speed output: %s: %s'
                               % (output, str(e)))

        if speed[-1] == 'G':
            speed = int(float(speed[:-1]) * 1000)
        else:
            speed = int(float(speed[:-1]) * 100)

        assert(speed in (100, 1000, 10000, 25000, 40000, 50000, 100000))

        return speed

    def _get_ifmode(self):
        interfaces = self.linux_interfaces
        if interfaces is None:
            return 'unknown'

        intf = interfaces[0]

        try:
            ifmode = open('/cumulus/switchd/config/interface/%s/interface_mode' % intf, 'r').read().strip()
        except (IOError, SystemError), e:
            if not (hasattr(e, 'errno') and e.errno in (errno.ENOENT, errno.ENOTCONN, errno.ECONNABORTED,
                                                        errno.EINVAL, errno.ENXIO)):
                (exc_type, exc_value, exc_traceback) = sys.exc_info()
                logger.exception(traceback.format_tb(exc_traceback))
            return 'unknown'
        except Exception, e:
            (exc_type, exc_value, exc_traceback) = sys.exc_info()
            logger.exception(traceback.format_tb(exc_traceback))
            return 'unknown'

        return self._switchd_map.get(ifmode, 'unknown')

    def _ifmode2switchd(self, mode, num_lanes, speed):
        if mode == None:
            return None

        if self.chip == 'Trident':
            mapper = self._ifmode2switchd_trident
        elif self.chip.startswith('Trident2'):
            mapper = self._ifmode2switchd_trident2
        elif self.chip == 'Tomahawk':
            mapper = self._ifmode2switchd_tomahawk

        return mapper(mode, num_lanes, speed)

    def _ifmode2switchd_tomahawk(self, mode, num_lanes, speed):
        switchd_mode = mode.upper()

        if speed == 10000:
            if switchd_mode in ('CR', 'SFI'):
                switchd_mode = 'SFI'
            else:
                switchd_mode = 'XFI'
        else:
            if switchd_mode.startswith('X') or switchd_mode == 'LR':
                switchd_mode = 'SR'

        return switchd_mode

    def _ifmode2switchd_trident2(self, mode, num_lanes, speed):
        switchd_mode = mode.upper()

        # Account for 40G breakout cables by translating between 4-lane and
        # 1-lane modes.  This is chip (internal phy driver) specific.

        if num_lanes == 4:
            if switchd_mode in ('CR', 'SR', 'LR'):
                switchd_mode += '4'
            elif switchd_mode in ('XFI', 'XGMII'):
                switchd_mode = 'SR4'
            elif switchd_mode in ('SFI',):
                switchd_mode = 'CR4'
        elif num_lanes == 2:
            if switchd_mode in ('CR', 'SR', 'LR'):
                switchd_mode += '2'
        elif num_lanes == 1:
            if switchd_mode in ('XLAUI', 'SR', 'LR'):
                switchd_mode = 'XFI'
            elif switchd_mode in ('CR',):
                switchd_mode = 'SFI'
        else:
            raise RuntimeError('too many lanes: %d' % num_lanes)

        return switchd_mode

    def _ifmode2switchd_trident(self, mode, num_lanes, speed):
        switchd_mode = self._ifmode2switchd_trident2(mode, num_lanes, speed)
        # SR4 is not valid for the Trident internal phy driver.  SR is the
        # equivalent and is handled correctly for ports using 1 or 4 lanes.
        if switchd_mode == 'SR4':
            switchd_mode = 'SR'

        return switchd_mode

    def _set_ifmode(self, mode):
        linux_interfaces = self.linux_interfaces
        if linux_interfaces is None:
            return

        bcm_interfaces = self.bcm_interfaces
        if bcm_interfaces is None:
            return


        for linux_intf, bcm_intf in zip(linux_interfaces, bcm_interfaces):
            bcm_speed = self.bcm_speed(bcm_intf)
            switchd_mode = self._ifmode2switchd(mode, self.num_lanes, bcm_speed)

            try:
                switchd_old = open('/cumulus/switchd/config/interface/%s/interface_mode' %
                                   linux_intf, 'r').read().strip()
            except (IOError, SystemError), e:
                if not (hasattr(e, 'errno') and e.errno in (errno.ENOENT, errno.ENOTCONN, errno.ECONNABORTED,
                                                            errno.EINVAL, errno.ENXIO)):
                    (exc_type, exc_value, exc_traceback) = sys.exc_info()
                    logger.exception(traceback.format_tb(exc_traceback))
                return
            except Exception, e:
                (exc_type, exc_value, exc_traceback) = sys.exc_info()
                logger.exception(traceback.format_tb(exc_traceback))
                return

            # Avoid expensive no-op interface changes.  It takes a long time
            # and will flap the link.
            if switchd_old == switchd_mode:
                continue

            logger.debug('set ifmode: %s %s->%s' % (linux_intf, switchd_old, switchd_mode))

            try:
                ifmode = open('/cumulus/switchd/config/interface/%s/interface_mode'
                              % linux_intf, 'w')
                ifmode.write(switchd_mode)
                ifmode.close()
            except (IOError, SystemError), e:
                if not (hasattr(e, 'errno') and e.errno in (errno.ENOENT, errno.ENOTCONN, errno.ECONNABORTED,
                                                            errno.EINVAL, errno.ENXIO)):
                    (exc_type, exc_value, exc_traceback) = sys.exc_info()
                    logger.exception(traceback.format_tb(exc_traceback))
                return
            except Exception, e:
                (exc_type, exc_value, exc_traceback) = sys.exc_info()
                logger.exception(traceback.format_tb(exc_traceback))
                return

    ifmode = property(_get_ifmode, _set_ifmode)

    def comply(self, compliance_code, dry_run=False):
        compliance_ifmode = self._compliance_map[compliance_code]
        self.swrxlos = compliance_ifmode
        if compliance_ifmode != self.ifmode:
            if not dry_run:
                self.ifmode = compliance_ifmode
            return True

        return False

    def ifmode2iftype(self, ifmode):
        switchd_mode = self._ifmode2switchd(ifmode, self.num_lanes, None)
        return self._iftype_map[switchd_mode]

    def iftype(self, compliance_code):
        compliance_ifmode = self._compliance_map[compliance_code]
        return self.ifmode2iftype(compliance_ifmode)

class UnknownPlatform(RuntimeError):
    pass

class PortWD(object):
    '''
    Base class for the portwd daemon
    '''
    def config(self):
        raise NotImplementedError()

    def run_once(self):
        raise NotImplementedError()

    def run(self, oneshot=False):
        if oneshot:
            logger.info('started, one shot for %d ports' % len(self.ports))
            self.run_once()
        else:
            logger.info('started, watching %d ports' % len(self.ports))
            while True:
                self.run_once()
                time.sleep(self.poll_interval)

        return True

class PortWDIdle(PortWD):
    ports = tuple()
    poll_interval = 1024
    def run_once(self):
        return True

class LogMessage(object):
    '''
    A throttled log message.  Messages are logged with exponential backoff.
    '''
    def __init__(self, msg, expire=60*60, ts=None):
        self.msg = msg
        if ts:
            self.ts = ts
        else:
            self.ts = time.time()

        self.expire = self.ts + expire

        self._seq = 1
        self._repeat = 0

    def repeat(self):
        now = time.time()
        
        self._repeat += 1

        if now > self.ts + (2 ** self._seq):
            self._seq += 1
            return self.message

        return None

    @property
    def message(self):
        if self._repeat == 0:
            return self.msg
        else:
            return self.msg + ' (repeated %u times)' % self._repeat

    @property
    def expired(self):
        return time.time() > self.expire

class PortWDRun(PortWD):
    '''
    portwd - port watch event daemon
    '''
    poll_interval = 5 # seconds
    port_reset_timing = 0.1 # seconds to hold reset low

    def __init__(self, configfile='/etc/cumulus/portwd.conf'):
        self._enable_ifmode = False
        self._enable_clobber_lpmod = False
        self._blacklist_mv88e1111_rev1 = False
        self._force_num_ports = None

        if not smbus_available:
            logger.warning('python-smbus not available, RJ45 SFP module link'
                           'status will not be accurate')

        self.ports = tuple()
        self._platform_detect()
        self._install_overrides(configfile)

        self._setup_hooks()

    def _install_overrides(self, configfile):
        if not os.path.isfile(configfile):
            return

        config = ConfigParser.SafeConfigParser()
        # Make options case sensitive
        config.optionxform = str
        config.read(configfile)

        cable_overrides = dict(config.items('cables'))
        port_overrides = dict(config.items('ports'))

        if len(cable_overrides):
            logger.info('config file overrides cable settings:')

            for (cable, setting) in cable_overrides.items():
                logger.info('%s -> %s' % (cable, setting))

            for port in self.ports:
                port.driver.cable.override_ethcodes(
                    { k : (v,) for (k, v) in cable_overrides.items() })

        if len(port_overrides):
            logger.info('config file overrides port settings:')

            for (label, setting) in port_overrides.items():
                logger.info('%s -> %s' % (label, setting))

            for port in self.ports:
                if port_overrides.has_key(port.label):
                    port.driver.cable.override_ethcodes(
                        { 'port' : (port_overrides[port.label],) })

    def _smallstone_ports(self):
        return tuple([ Port(label='%s' % (x + 1),
                            driver=CelSmallstoneQSFPDriver(control_bit=x, port=x + 1),
                            config=BCMConfig(x + 1, 'QSFP+',
                                             phyless=True, chip='Trident2')) for x in range(32) ])

    def _redstone_ports(self):
        return (Port(label="49", driver=CelRedstoneQSFPDriver(sysfs_file='qsfp1_control', port=49),
                     config=BCMConfig(49, 'QSFP+', phyless=True, chip='Trident')),
                Port(label="50", driver=CelRedstoneQSFPDriver(sysfs_file='qsfp2_control', port=50),
                     config=BCMConfig(50, 'QSFP+', phyless=True, chip='Trident')),
                Port(label="51", driver=CelRedstoneQSFPDriver(sysfs_file='qsfp3_control', port=51),
                     config=BCMConfig(51, 'QSFP+', phyless=True, chip='Trident')),
                Port(label="52", driver=CelRedstoneQSFPDriver(sysfs_file='qsfp4_control', port=52),
                     config=BCMConfig(52, 'QSFP+', phyless=True, chip='Trident')))


    def _smallstonexp_ports(self):
        return tuple([ Port(label='%s' % (x + 1),
                            driver=CelSmallstoneXPQSFPDriver(control_bit=x, port=x + 1),
                            config=BCMConfig(x + 1, 'QSFP+',
                                             phyless=True, chip='Trident2')) for x in range(32) ])

    def _redstonexp_ports(self):
        return tuple([ Port(label='%s' % x, driver=CelRedstoneXPQSFPDriver(control_bit=(x - 49), port=x),
                            config=BCMConfig(x, 'QSFP+',
                                             phyless=True, chip='Trident2')) for x in range(49,55) ])

    def _as6701_ports(self):
        return tuple([ Port(label='%s' % (x + 1), driver=AS6701FixedQSFPDriver(control_bit=x, port=x + 1))
                       for x in range(20) ] +
                     [ Port(label='%s' % (x + 1), driver=AS6701ModuleQSFPDriver(module=0, module_port=x-20, port=x + 1))
                       for x in range(20,26) ] +
                     [ Port(label='%s' % (x + 1), driver=AS6701ModuleQSFPDriver(module=1, module_port=x-26, port=x + 1))
                       for x in range(26,32) ])


    def _as5712_ports(self):
        # TODO: Need to map these as the silkscreen has changed.
        return tuple([ Port(label='%s' % x, driver=AS5712QSFPDriver(control_bit=(x - 49), port=x),
                            config=BCMConfig(x, 'QSFP+',
                                             phyless=True, chip='Trident2')) for x in range(49,55) ])
    def _as5812_ports(self):
        return tuple([ Port(label='%s' % x, driver=AS5812QSFPDriver(control_bit=(x - 49), port=x),
                            config=BCMConfig(x, 'QSFP+',
                                             phyless=True, chip='Trident2Plus')) for x in range(49,55) ])

    def _as6712_ports(self):
        return tuple([ Port(label='%s' % (x + 1),
                            driver=AS6712QSFPDriver(control_bit=x, port=x + 1),
                            config=BCMConfig(x + 1, 'QSFP+',
                                             phyless=True, chip='Trident2')) for x in range(32) ])
    def _ly2r_ports(self):
        return (Port(label='49', driver=QuantaLY2RQSFPDriver(base_pin=168, port=49),
                     config=BCMConfig(49, 'QSFP+', phyless=True, chip='Trident')),
                Port(label='50', driver=QuantaLY2RQSFPDriver(base_pin=176, port=50),
                     config=BCMConfig(50, 'QSFP+', phyless=True, chip='Trident')),
                Port(label='51', driver=QuantaLY2RQSFPDriver(base_pin=184, port=51),
                     config=BCMConfig(51, 'QSFP+', phyless=True, chip='Trident')),
                Port(label='52', driver=QuantaLY2RQSFPDriver(base_pin=192, port=52),
                     config=BCMConfig(52, 'QSFP+', phyless=True, chip='Trident')))

    def _ly6_ports(self, base):
        return tuple([ Port(label='%s' % (x + 1), driver=QuantaLY6QSFPDriver(base, control_bit=x,
                        port=x + 1, reset='qsfp1_reset', lp_mode='qsfp1_lp_mode', present='qsfp1_present'),
                        config=BCMConfig(x + 1, 'QSFP+', phyless=True, chip='Trident2'))
                       for x in range(16) ] +
                     [ Port(label='%s' % (x + 1), driver=QuantaLY6QSFPDriver(base, control_bit=(x-16),
                        port=x + 1, reset='qsfp2_reset', lp_mode='qsfp2_lp_mode', present='qsfp2_present'),
                        config=BCMConfig(x + 1, 'QSFP+', phyless=True, chip='Trident2'))
                       for x in range(16,32) ])

    def _ly6_p2020_ports(self):
        base = '/sys/devices/soc.0/ffe03000.i2c'
        return self._ly6_ports(base)

    def _ly6_rangeley_ports(self):
        base = '/sys/devices/pci0000:00/0000:00:1f.3'
        return self._ly6_ports(base)

    def _ly8_ports(self):
        return tuple([ Port(label='%s' % x, driver=QuantaLY8QSFPDriver(port=x),
                            config=BCMConfig(x, 'QSFP+', phyless=True, chip='Trident2')) for x in range(49,53) ])

    def _ly9_rangeley_ports(self):
        return tuple([ Port(label='%s' % x, driver=QuantaLY9RangeleyQSFPDriver(port=x),
                            config=BCMConfig(x, 'QSFP+', phyless=True, chip='Trident2')) for x in range(49,53) ])

    def _s6000_ports(self):
        # account for silk-screen changes in cpld control bits
        control_bit_map = (1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,17,16,
                           19,18,21,20,23,22,25,24,27,26,29,28,31,30)
        return tuple([ Port(label='%s' % (x + 1),
                            driver=DellS6000QSFPDriver(control_bit=control_bit_map[x], port=x + 1),
                            config=BCMConfig(x + 1, 'QSFP+', phyless=True, chip='Trident2'))
                       for x in range(32) ])

    def _s6010_ports(self):
        return tuple([ Port(label='%s' % (x + 1),
                            driver=DellS6010QSFPDriver(control_bit=x, port=x + 1),
                            config=BCMConfig(x + 1, 'QSFP+',
                                             phyless=True, chip='Trident2Plus')) for x in range(32) ])

    def _s3000_ports(self, driver_class):
        control_bit_map = (1,0,3,2)
        return tuple([ Port(label='%s' % x,
                            driver=driver_class(control_bit=control_bit_map[x - 49],
                                                port=x),
                            config=BCMConfig(x, 'SFP+', phyless=True, chip='Trident'))
                       for x in range(49,53) ])

    def _s4000_ports(self):
        return tuple([ Port(label='%s' % x, driver=DellS4000SFPDriver(control_bit=(x - 1), port=x))
                       for x in range(1,49) ] +
                     [ Port(label='%s' % x, driver=DellS4000QSFPDriver(control_bit=(x - 49), port=x),
                            config=BCMConfig(x, 'QSFP+',
                                             phyless=True, chip='Trident2')) for x in range(49,55) ])

    def _s4048t_ports(self):
        return tuple([ Port(label='%s' % x, driver=DellS4048TQSFPDriver(control_bit=(x - 49), port=x),
                            config=BCMConfig(x, 'QSFP+',
                                             phyless=True, chip='Trident2Plus')) for x in range(49,55) ])

    def _z9100_ports(self):
        return tuple([ Port(label='%s' % (x + 1),
                            driver=DellZ9100ZQSFPDriver(control_bit=x, port=x + 1),
                            config=BCMConfig(x + 1, 'QSFP28',
                                             phyless=True, chip='Tomahawk')) for x in range(32) ])

    def _seastone_ports(self):
        return tuple([ Port(label='%s' % (x + 1),
                            driver=CelSeastoneZQSFPDriver(control_bit=x, port=x + 1),
                            config=BCMConfig(x + 1, 'QSFP28',
                                             phyless=True, chip='Tomahawk')) for x in range(32) ])

    def _platform_detect(self):
        platform = subprocess.check_output('/usr/bin/platform-detect').strip()
        if platform == 'cel,smallstone':
            self._enable_ifmode = True
            self.ports = self._smallstone_ports()
            logger.info('Celestica Smallstone detected')
        elif platform == 'cel,redstone':
            self._enable_ifmode = True
            self.ports = self._redstone_ports()
            logger.info('Celestica Redstone detected')
        elif platform == 'cel,smallstone_xp':
            self._enable_ifmode = True
            self.ports = self._smallstonexp_ports()
            logger.info('Celestica Smallstone XP detected')
        elif platform == 'cel,redstone_xp':
            self._enable_ifmode = True
            self.ports = self._redstonexp_ports()
            logger.info('Celestica Redstone XP detected')
        elif platform == 'quanta,ly8_rangeley':
            self._enable_ifmode = True
            self._force_num_ports = 54
            self.ports = self._ly8_ports()
            logger.info('Quanta LY8 detected')
        elif platform == 'quanta,ly9_rangeley':
            self._enable_ifmode = True
            self.ports = self._ly9_rangeley_ports()
            logger.info('Quanta LY9 Rangeley detected')
        elif platform == 'accton,as5610_52x':
            self._enable_clobber_lpmod = True
            self.ports = tuple()
            logger.info('Accton AS5610 detected')
        elif platform == 'accton,as5712_54x':
            self.ports = self._as5712_ports()
            logger.info('Accton AS5712 detected')
        elif platform == 'accton,as5812_54t':
            self.ports = self._as5812_ports()
            logger.info('Accton AS5812 detected')
        elif platform == 'accton,as6712_32x':
            self._enable_ifmode = True
            self.ports = self._as6712_ports()
            logger.info('Accton AS6712 detected')
        elif platform == 'accton,as6701_32x':
            self.ports = self._as6701_ports()
            logger.info('Accton AS6701 detected')
        elif platform == 'dell,s6000_s1220':
            self._enable_ifmode = True
            self.ports = self._s6000_ports()
            logger.info('Dell S6000 detected')
        elif platform == 'dell,s3000_c2338':
            self._enable_ifmode = True
            self.ports = self._s3000_ports(DellS3000SFPDriver)
            logger.info('Dell S3000 detected')
        elif platform == 'cel,e1031':
            self._enable_ifmode = True
            self.ports = self._s3000_ports(CelE1031SFPDriver)
            logger.info('Celestica E1031 detected')
        elif platform == 'dell,s6010_c2538':
            self._enable_ifmode = True
            self.ports = self._s6010_ports()
            logger.info('Dell S6010 detected')
        elif platform == 'dell,s4000_c2338':
            self._enable_ifmode = True
            self.ports = self._s4000_ports()
            logger.info('Dell S4000 detected')
        elif platform == 'dell,s4048t_c2338':
            self._enable_ifmode = True
            self.ports = self._s4048t_ports()
            logger.info('Dell S4048T detected')
        elif platform == 'quanta,ly2r':
            self._enable_ifmode = True
            self.ports = self._ly2r_ports()
            logger.info('Quanta LY2R detected')
        elif platform == 'quanta,ly6_p2020':
            self._enable_ifmode = True
            self.ports = self._ly6_p2020_ports()
            logger.info('Quanta LY6 P2020 detected')
        elif platform == 'quanta,ly6_rangeley':
            self._enable_ifmode = True
            self.ports = self._ly6_rangeley_ports()
            logger.info('Quanta LY6 Rangeley detected')
        elif platform == 'dell,z9100_c2538':
            self._enable_ifmode = True
            self.ports = self._z9100_ports()
            logger.info('Dell Z9100 detected')
        elif platform == 'cel,seastone':
            self._enable_ifmode = True
            self.ports = self._seastone_ports()
            logger.info('Celestica Seastone detected')

        if platform == 'dell,s4000_c2338':
            logger.info('Applying workaround for MV88E1111 rev1 phys')
            self._blacklist_mv88e1111_rev1 = True

        self._discover_generic_ports()

    def _discover_generic_ports(self):
        generic_ports = []
        for root, dirs, files in os.walk('/sys/class/eeprom_dev'):
            for eeprom_dir in dirs:
                label = open(os.path.join(root, eeprom_dir, 'label'), 'r').read().strip()
                if not label.startswith('port'):
                    continue

                label = label[4:]
                portnum = int(label)

                if (self._force_num_ports is not None and
                    portnum > self._force_num_ports):
                    logger.warning('ignoring invalid port: swp%d' % portnum)
                    continue

                if label not in [ x.label for x in self.ports ]:
                    driver = EEPROMOnlyPortDriver(SysFSPortDriver._find_eeprom(portnum))
                    port = Port(label=label, driver=driver, port=portnum, config=None)
                    generic_ports.append(port)

        if generic_ports:
            self.ports = tuple(generic_ports + list(self.ports))

    @property
    def _switchd_pid(self):
        try:
            readypid = open('/var/run/switchd.ready').read().strip()
        except:
            readypid = 0

        return readypid

    def _setup_hooks(self):
        self._state_admin_up = [ False ] * len(self.ports)
        self._state_ethcodes = [ False ] * len(self.ports)
        self._state_presence = [ False ] * len(self.ports)
        self._state_switchd_pid = 0

        self._admin_up_hooks = []
        self._ethcodes_hooks = []
        self._switchd_restart_hooks = []
        self._presence_hooks = []
        self._generic_hooks = []

        if len(self.ports):
            self._admin_up_hooks.append(self._rj45_phy_power_check)
            self._admin_up_hooks.append(self._clear_no_link_resets)
            self._generic_hooks.append(self._rj45_phy_configure)
            # XXX - debugging
            #self._generic_hooks.append(self._rj45_link_status)

            if any([ True for x in self.ports if any((x.driver.tx_retimer, x.driver.rx_retimer)) ]):
                self._generic_hooks.append(self._retimer_configure)

            if self._enable_ifmode:
                self._ethcodes_hooks.append(self._ifmode_hook)
                self._switchd_restart_hooks.append(self._ifmode_hook)

            if self._enable_clobber_lpmod:
                self._generic_hooks.append(self._clobber_lpmod)

            if self._blacklist_mv88e1111_rev1:
                self._presence_hooks.append(self._reset_blacklist)

            self._presence_hooks.append(self._clear_no_link_resets)

    def _poll_admin_up(self):
        # returns all ports where administrative state has changed since the last poll
        changed = set()
        for port_index in range(len(self.ports)):
            port = self.ports[port_index]

            if not port.sysfs_ready:
                continue

            if port.admin_up != self._state_admin_up[port_index]:
                changed.add(port)

            self._state_admin_up[port_index] = port.admin_up

        return changed

    def _poll_ethcodes(self):
        # returns all ports where the cable ethcodes has changed since the last
        # poll
        changed = set()
        for port_index in range(len(self.ports)):
            port = self.ports[port_index]

            if port.eeprom_ready:
                try:
                    ethcodes = port.driver.cable.ethcodes
                except EEPROMTimeout, e:
                    log_throttle('error reading ethcodes for port %s: %s.' %
                                 (port.label, str(e)), level='warning')
                    continue

                length = None

                if ethcodes is None:
                    logger.warning('unable to read ethcodes for port: %s' % port.label)

                if (ethcodes != self._state_ethcodes[port_index]):
                    if ethcodes:
                        logger.debug('cable change on %s to %s' %
                                     (str(self.ports[port_index]), ', '.join(ethcodes)))
                    else:
                        logger.debug('cable removed on %s' %
                                     str(self.ports[port_index]))

                    changed.add(self.ports[port_index])

            else:
                ethcodes = None

            self._state_ethcodes[port_index] = ethcodes

        return changed

    def _poll_presence(self):
        # returns all ports where the cable presence has changed since the last
        # poll
        changed = set()
        for port_index in range(len(self.ports)):
            port = self.ports[port_index]
            present = port.driver.present

            if present != self._state_presence[port_index]:
                if present:
                    logger.debug('cable inserted on %s' % str(self.ports[port_index]))
                else:
                    logger.debug('cable removed on %s' % str(self.ports[port_index]))

                changed.add(self.ports[port_index])

            self._state_presence[port_index] = present

        return changed

    def _poll_switchd_pid(self):
        # returns all ports that have been affected by a switchd restart
        switchd_pid = self._switchd_pid

        try:
            if switchd_pid == 0:
                return set()

            if switchd_pid != self._state_switchd_pid:
                return set(self.ports)
            else:
                return set()
        finally:
            self._state_switchd_pid = switchd_pid

    def _call_hooks(self, ports, hooks):
        return [ hook(ports) for hook in hooks ]

    def _reset_ports(self, ports, verbose=False):
        reset = set()
        for port in ports:
            port.driver.reset = 1
            reset.add(port)

        time.sleep(self.port_reset_timing)

        for port in reset:
            port.driver.reset = 0

        if verbose and len(reset):
            logger.debug('reset: ' + ', '.join([ str(x) for x in reset ]))

    def _ifmode_config(self, ports):
        configs = []

        for port in ports:
            if port.config is None or port.config.bcm_interfaces is None:
                continue

            if not port.driver.present:
                compliance_code = '40g-cr4'
            else:
                try:
                    compliance_codes = port.driver.cable.ethcodes
                except EEPROMTimeout, e:
                    log_throttle('failed to get compliance codes for port %s: %s'
                                       % (port.label, str(e)), level='warning')
                    compliance_codes = None

                if not compliance_codes or len(compliance_codes) < 1:
                    compliance_code = '40g-cr4'
                else:
                    compliance_code = compliance_codes[0]

            for intf in port.config.bcm_interfaces:
                configs.append('serdes_if_type_%s=%d' %
                               (intf, port.config.iftype(compliance_code)))

        return '\n'.join(configs)

    def _shutdown(self, ports):
        target_ports = []
        for port in self._ports_check_eeprom(ports):
            if (port.driver.cable.connector == 'rj45' or
                '1000base-t' in port.driver.cable.ethcodes):
                target_ports.append(port)

        rv = True
        for port in target_ports:
            if not self._rj45_stop(port):
                rv = False

        return rv

    def _probe_phy(self, bus):
        if not smbus_available:
            return None

        i2c = smbus.SMBus(bus)
        try:
            phy = MV88E1111Phy(i2c)
            if not phy.probe:
                log_throttle('unknown phy: 0x%04x 0x%04x' % phy.phyid)
                return None
        except IOError:
            log_throttle('failed reading 1000Base-T phy on bus: %s' % bus)
            return None

        return phy

    def _validate_phy(self, port, phy):
        if self._blacklist_mv88e1111_rev1 and phy.revision == '1':
            logger.error('MV88E1111 with revision 1 silicon detected in '
                         'port %s.  Disabling port.' % port.label)
            port.admin_down()
            port.disabled = True
            return False
        else:
            return True

    def _configure_phy(self, port):
        phy = self._probe_phy(port.driver.cable.eeprom.bus)
        if not phy:
            return

        if not self._validate_phy(port, phy):
            return

        needs_reset = False

        try:
            if 100 in port.speed or 1000 in port.speed:
                if not True in port.carrier:
                    if port.no_link_resets() == 0:
                        logger.info('no link reset for port: %s' % port.label)
                        port.driver.toggle_reset()
                        phy.reset()
                        needs_reset = False

            if 100 in port.speed:
                if phy.sgmii_mode(port):
                    logger.info('set SGMII mode for port: %s' % port.label)
                    phy.reset()
                if phy.set_an('fiber', False):
                    logger.info('disable fiber autoneg for port: %s' % port.label)
                    needs_reset = True
                if phy.set_an('copper', False):
                    logger.info('disable copper autoneg for port: %s' % port.label)
                    needs_reset = True
                if phy.set_100fd():
                    logger.info('force 100M full-duplex for port: %s' % port.label)
                    needs_reset = True
            elif 1000 in port.speed:
                if phy.gbic_noan_mode(port):
                    logger.info('set GBIC no-autoneg mode for port: %s' % port.label)
                    phy.reset()
                if phy.set_an('fiber', False):
                    logger.info('disable fiber autoneg for port: %s' % port.label)
                    needs_reset = True
                if phy.set_1000auto():
                    logger.info('set 1000 auto for port: %s' % port.label)
                    needs_reset = True
            else:
                logger.debug('rj45 port not in 100 or 1G mode: %s' % port.label)
                phy.set_power(False)
        except UnsupportedPhy, e:
            if phy.power:
                logger.warning(str(e))
                logger.warning('powering down port: %s' % port.label)
                phy.set_power(False)
                return

        if phy.enable_energy_detect_plus():
            logger.info('enabled power save on port: %s' % port.label)
            needs_reset = True

        if needs_reset:
            logger.info('settings changed, resetting phy on port: %s' % port.label)
            phy.reset()

    def _ports_check_eeprom(self, ports):
        for port in ports:
            if not port.eeprom_ready:
                logger.debug('port not eeprom ready: %s' % port.label)
                continue

            if port.driver.cable.check:
                yield port
            else:
                log_throttle('eeprom check failed for port: %s' % port.label)
                continue

    def _rj45_link_status(self, ports):
        target_ports = []
        for port in self._ports_check_eeprom(ports):
            if (port.driver.cable.connector == 'rj45' or
                '1000base-t' in port.driver.cable.ethcodes):
                target_ports.append(port)

        if not len(target_ports):
            return

        status = []
        for port in target_ports:
            phy = self._probe_phy(port.driver.cable.eeprom.bus)
            if phy:
                status.append('%s(%s)' % (port.label, phy.link))
            else:
                status.append('%s(unknown)' % port.label)

            #logger.debug('phy dump for port %s' % port.label)
            #phy.dump_regs()

        logger.debug('rj45_link_status: ' + ', '.join(status))

    def _rj45_phy_configure(self, ports):
        target_ports = []
        for port in self._ports_check_eeprom(ports):
            if (port.traffic_ready and
                (port.driver.cable.connector == 'rj45' or
                 '1000base-t' in port.driver.cable.ethcodes)):
                target_ports.append(port)

        if not len(target_ports):
            return

        logger.debug('rj45_phy_configure: %s' % ', '.join([ str(x) for x in target_ports ]))

        for port in target_ports:
            self._configure_phy(port)

    def _clear_no_link_resets(self, ports):
        for port in ports:
            port.no_link_resets(clear=True)

    def _rj45_phy_power_check(self, ports):
        target_ports = []
        for port in self._ports_check_eeprom(ports):
            if (port.driver.cable.connector == 'rj45' or
                '1000base-t' in port.driver.cable.ethcodes):
                target_ports.append(port)

        if not len(target_ports):
            return

        logger.debug('rj45_phy_power_check: %s' % ', '.join([ str(x) for x in target_ports ]))

        for port in target_ports:
            phy = self._probe_phy(port.driver.cable.eeprom.bus)
            if not phy:
                logger.debug('phy probe failed: %s' % port.label)
                continue

            if not self._validate_phy(port, phy):
                continue

            if 100 in port.speed or 1000 in port.speed:
                up = any(port.admin_up)
            else:
                up = False

            if phy.set_power(up):
                logger.info('power %s rj45 port: %s' % (up and 'up' or 'down', port.label))

    def _rj45_stop(self, port):
        phy = self._probe_phy(port.driver.cable.eeprom.bus)
        if not phy:
            logger.info('phy probe failed: %s' % port.label)
            return False

        if not self._validate_phy(port, phy):
            logger.info('phy validate failed: %s' % port.label)
            return False

        try:
            phy.set_power(False)
            logger.info('power down rj45 port: %s' % port.label)
        except Exception, e:
            logger.info('failed to power down rj45 port: %s (%s)' % (port.label, str(e)))
            return False

    def _retimer_configure(self, ports):
        target_ports = [ port for port in ports
                         if (port.traffic_ready and
                             (port.driver.tx_retimer or port.driver.rx_retimer)) ]
        if not len(target_ports):
            return

        logger.debug('retimer_configure: %s' % ', '.join([ str(x) for x in target_ports ]))

        for port in target_ports:
            current_speed = port.speed

            if cmp(current_speed,  port.driver.rx_retimer.speed):
                logger.info('Configuring retimer for speed change from %s to %s on port-%s'
                            % (port.driver.rx_retimer.speed, current_speed, port.label))
                port.driver.rx_retimer.set_speed(current_speed)
                port.driver.tx_retimer.set_speed(current_speed)

    def _reset_blacklist(self, ports):
        for port in ports:
            if not port.driver.present:
                logger.debug('removing port from disabled set: %s' % port.label)
                port.disabled = False

    def _clobber_lpmod(self, ports):
        target_ports = []
        for port in self._ports_check_eeprom(ports):
            if '40g-lr4' in port.driver.cable.ethcodes:
                target_ports.append(port)

        if not len(target_ports):
            return

        logger.debug('clobber lpmod: %s' % ', '.join([ str(x) for x in target_ports ]))

        for port in target_ports:
            if port.driver.cable.lpoverride != 'override-high':
                logger.debug('overriding lpmod pin for port: %s' % port.label)
                port.driver.cable.lpoverride = 'override-high'

    def _ifmode_hook(self, ports):
        target_ports = []
        for port in self._ports_check_eeprom(ports):
             if port.config:
                 target_ports.append(port)

        if not len(target_ports):
            return

        logger.debug('config ifmode: %s' % ', '.join([ str(x) for x in target_ports ]))

        for port in target_ports:
            compliance_codes = port.driver.cable.ethcodes
            if compliance_codes and len(compliance_codes) > 0:
                if port.config.comply(compliance_codes[0], dry_run=True):
                    logger.debug('reset port: %s' % port.label)
                    port.driver.reset = 1
                    port.config.comply(compliance_codes[0])
                    port.driver.reset = 0
            else:
                logger.debug('reset port: %s' % port.label)
                port.driver.reset = 1
                port.config.ifmode = 'cr'
                port.driver.reset = 0


    def _reset_fallback(self, ports):
        reset = set()

        for port in ports:
            # If the port, or any sub-port has carrier, skip
            if True in port.carrier:
                continue
            # If none of the ports are administratively up, skip
            if not (True in port.admin_up):
                continue
            # If no cable is present, skip (this is the most expensive check)
            if not port.driver.present:
                continue

            reset.add(port)

        verbose = log_throttle(':'.join([ str(x) for x in reset ]), level=None)

        if verbose:
            for port in reset:
                logger.info('port administratively up with cable present but no carrier: ' + str(port))

        self._reset_ports(reset, verbose)

    def config(self):
        return self._ifmode_config(self.ports)

    def shutdown(self):
        logger.info('shutdown')
        return self._shutdown(self.ports)

    def run_once(self):
        try:
            if len(self._switchd_restart_hooks):
                switchd_changed = self._poll_switchd_pid()
                if len(switchd_changed):
                    self._call_hooks(switchd_changed, self._switchd_restart_hooks)

            if len(self._admin_up_hooks):
                admin_changed = self._poll_admin_up()
                if len(admin_changed):
                    self._call_hooks(admin_changed, self._admin_up_hooks)

            if len(self._presence_hooks):
                presence_changed = self._poll_presence()
                if len(presence_changed):
                    self._call_hooks(presence_changed, self._presence_hooks)

            if len(self._ethcodes_hooks):
                ethcodes_changed = self._poll_ethcodes()
                if len(ethcodes_changed):
                    self._call_hooks(ethcodes_changed, self._ethcodes_hooks)

            # generic hooks are called every poll interval
            if len(self._generic_hooks):
                self._call_hooks(self.ports, self._generic_hooks)

        except EEPROMTimeout, e:
            log_throttle('unhandled EEPROM timeout: %s.' % str(e), level='warning')

        return True

def start_logging(syslog=True, console=False, quiet=False):
    global logger
    global log_handlers
    logger = logging.getLogger('portwd')
    fmt = logging.Formatter(fmt='portwd: %(message)s')

    if console:
        console = logging.StreamHandler(sys.stdout)
        console.setFormatter(fmt)
        logger.addHandler(console)
        log_handlers.append(console)

    if syslog:
        syslog = logging.handlers.SysLogHandler(address='/dev/log')
        syslog.setFormatter(fmt)
        logger.addHandler(syslog)
        log_handlers.append(syslog)

    if quiet:
        logger.setLevel(logging.ERROR)
    else:
        logger.setLevel(logging.INFO)

def stop_logging():
    global logger
    global log_handlers
    for handler in log_handlers:
        handler.close()
        logger.removeHandler(handler)
    log_handlers = []

def socket_init():
    ''' Initialize the socket '''
    globals()["sock"] = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    globals()["sockfd"] = globals()["sock"].fileno()

def socket_shutdown():
    ''' Shut down the socket '''
    if globals()["sock"] is not None:
        globals()["sock"].close()
    globals()["sock"] = None
    globals()["sockfd"] = None

def service():
    start_logging()

    try:
        portwd = PortWDRun()
    except UnknownPlatform, e:
        logger.info('not monitoring on platform: %s' % str(e))
        portwd = PortWDIdle()

    try:
        portwd.run_once()
    except cumulus.porttab.NotConfigured:
        logger.error('porttab not configured, exiting.')
        return -1
    except BCMNotAvailable:
        logger.error('switchd not running, exiting.')
        return -1
    except NoSuchPort, e:
        logger.info('missing swp interfaces, exiting.')
        return -1

    socket_shutdown()
    stop_logging()

    dc = daemon.DaemonContext(working_directory='/run',
                              pidfile=daemon.pidlockfile.PIDLockFile('/run/portwd.pid'))
    with dc:
        start_logging()

        def exit_normally(signum, frame):
            logger.info('SIGTERM, exiting')
            exit(0)

        signal.signal(signal.SIGTERM, exit_normally)

        try:
            portwd.run()
        except Exception, e:
            (exc_type, exc_value, exc_traceback) = sys.exc_info()
            logger.exception(traceback.format_tb(exc_traceback))
            return -1

        socket_shutdown()
        logger.info('exiting')
        return 0

def oneshot():
    start_logging(syslog=False, console=True)

    try:
        PortWDRun().run(oneshot=True)
    except UnknownPlatform, e:
        logger.info('not monitoring on platform: %s' % str(e))
    except cumulus.porttab.NotConfigured:
        logger.info('porttab not configured')
        return -1

    return 0

def config():
    start_logging(syslog=True, console=False, quiet=True)

    try:
        sys.stdout.write(PortWDRun().config())
    except UnknownPlatform:
        pass
    except cumulus.porttab.NotConfigured:
        logger.error('porttab not configured')
        return -1
    return 0

def shutdown():
    start_logging(syslog=True, console=False, quiet=False)

    try:
        PortWDRun().shutdown()
    except UnknownPlatform:
        pass
    except cumulus.porttab.NotConfigured:
        logger.error('porttab not configured')
        return -1
    return 0

if __name__ == '__main__':
    if len(sys.argv) <= 1:
        sys.exit(service())

    if sys.argv[1] == '--oneshot':
        sys.exit(oneshot())
    elif sys.argv[1] == '--config':
        sys.exit(config())
    elif sys.argv[1] == '--shutdown':
        sys.exit(shutdown())
    else:
        sys.stderr.write('Usage: portwd (--oneshot|--config|--shutdown)\n')
        sys.exit(-1)
