#!/usr/bin/python
#
# Copyright 2014 Cumulus Networks, Inc. All rights reserved.
# Author: Roopa Prabhu, roopa@cumulusnetworks.com
#
# iface --
#    interface object
#

"""ifupdown2 network interface object

It is modeled based on the 'iface' section in /etc/network/interfaces
file. But can be extended to include any other network interface format
"""

from collections import OrderedDict
import logging
import json

class ifaceType():
    UNKNOWN = 0x0
    IFACE = 0x1
    BRIDGE_VLAN = 0x2


class ifaceRole():
    """ ifaceRole is used to classify the ifaceobj.role of
        MASTER or SLAVE where there is a bond or bridge
        with bond-slaves or bridge-ports.  A bond in a bridge
        is both a master and slave (0x3)
    """
    UNKNOWN = 0x0
    SLAVE = 0x1
    MASTER = 0x2

class ifaceLinkKind():
    """ ifaceLlinkKind is used to identify interfaces
        in the ifaceobj.link_kind attribute. Dependents of the bridge or
        bond have an ifaceobj.role attribute of SLAVE and the bridge or
        bond itself has ifaceobj.role of MASTER.
    """
    UNKNOWN = 0x0
    BRIDGE = 0x1
    BOND = 0x2
    VLAN = 0x4
    VXLAN = 0x8

class ifaceLinkType():
    LINK_UNKNOWN = 0x0
    LINK_SLAVE = 0x1
    LINK_MASTER = 0x2
    LINK_NA = 0x3

class ifaceDependencyType():
    """ Indicates type of dependency.

        This class enumerates types of dependency relationships
        between interfaces.

        iface dependency relationships can be classified
        into:
         - link
         - master/slave

        In a 'link' dependency relationship, dependency can be shared
        between interfaces. example: swp1.100 and
        swp1.200 can both have 'link' swp1. swp1 is also a dependency
        of swp1.100 and swp1.200. As you can see dependency
        swp1 is shared between swp1.100 and swp1.200.
         
        In a master/slave relationship like bridge and
        its ports: eg: bridge br0 and its ports swp1 and swp2.
        dependency swp1 and swp2 cannot be shared with any other
        interface with the same dependency relationship.
        ie, swp1 and swp2 cannot be in a slave relationship
        with another interface. Understanding the dependency type is
        required for any semantic checks between dependencies.

    """
    UNKNOWN = 0x0
    LINK = 0x1
    MASTER_SLAVE = 0x2

class ifaceStatus():
    """Enumerates iface status """

    UNKNOWN = 0x1
    SUCCESS = 0x2
    ERROR = 0x3
    NOTFOUND = 0x4

    @classmethod
    def to_str(cls, state):
        if state == cls.UNKNOWN:
            return 'unknown'
        elif state == cls.SUCCESS:
            return 'success'
        elif state == cls.ERROR:
            return 'error'
        elif state == cls.NOTFOUND:
            return 'notfound'
    
    @classmethod
    def from_str(cls, state_str):
        if state_str == 'unknown':
            return cls.UNKNOWN
        elif state_str == 'success':
            return cls.SUCCESS
        elif state_str == 'error':
            return cls.ERROR

class ifaceState():
    """Enumerates iface state """

    UNKNOWN = 0x1
    NEW = 0x2
    PRE_UP = 0x3
    UP = 0x4
    POST_UP = 0x5
    PRE_DOWN = 0x6
    DOWN = 0x7
    POST_DOWN = 0x8

    # Pseudo states
    QUERY_CHECKCURR = 0x9
    QUERY_RUNNING = 0xa

    @classmethod
    def to_str(cls, state):
        if state == cls.UNKNOWN:
            return 'unknown'
        elif state == cls.NEW:
            return 'new'
        elif state == cls.PRE_UP:
            return 'pre-up'
        elif state == cls.UP:
            return 'up'
        elif state == cls.POST_UP:
            return 'post-up'
        elif state == cls.PRE_DOWN:
            return 'pre-down'
        elif state == cls.DOWN:
            return 'down'
        elif state == cls.POST_DOWN:
            return 'post-down'
        elif state == cls.QUERY_CHECKCURR:
            return 'query-checkcurr'
        elif state == cls.QUERY_RUNNING:
            return 'query-running'

    @classmethod
    def from_str(cls, state_str):
        if state_str == 'unknown':
            return cls.UNKNOWN
        elif state_str == 'new':
            return cls.NEW
        elif state_str == 'pre-up':
            return cls.PRE_UP
        elif state_str == 'up':
            return cls.UP
        elif state_str == 'post-up':
            return cls.POST_UP
        elif state_str == 'pre-down':
            return cls.PRE_DOWN
        elif state_str == 'down':
            return cls.DOWN
        elif state_str == 'post-down':
            return cls.POST_DOWN
        elif state_str == 'query-checkcurr':
            return cls.QUERY_CHECKCURR
        elif state_str == 'query-running':
            return cls.QUERY_RUNNING

class ifaceJsonEncoder(json.JSONEncoder):
    def default(self, o):
        retconfig = {}
        if o.config:
            retconfig = dict((k, (v[0] if len(v) == 1 else v))
                             for k,v in o.config.items())
        return OrderedDict({'name' : o.name,
                            'addr_method' : o.addr_method,
                            'addr_family' : o.addr_family,
                            'auto' : o.auto,
                            'config' : retconfig})

class ifaceJsonDecoder():
    @classmethod
    def json_to_ifaceobj(cls, ifaceattrdict):
        ifaceattrdict['config'] = OrderedDict([(k, (v if isinstance(v, list)
                                                else [v]))
                                for k,v in ifaceattrdict.get('config',
                                            OrderedDict()).items()])
        return iface(attrsdict=ifaceattrdict)

class iface():
    """ ifupdown2 iface object class
    
    Attributes:
        **name**      Name of the interface 

        **addr_family**     Address family eg, inet, inet6. Can be None to
                            indicate both address families

        **addr_method**     Address method eg, static, manual or None for
                            static address method

        **config**          dictionary of config lines for this interface

        **state**           Configuration state of an interface as defined by
                            ifaceState

        **status**          Configuration status of an interface as defined by
                            ifaceStatus

        **flags**           Internal flags used by iface processing

        **priv_flags**      private flags owned by module using this class

        **module_flags**    module flags owned by module using this class

        **refcnt**          reference count, indicating number of interfaces
                            dependent on this iface

        **lowerifaces**     list of interface names lower to this interface or
                            this interface depends on

        **upperifaces**     list of interface names upper to this interface or
                            the interfaces that depend on this interface 

        **auto**            True if interface belongs to the auto class

        **classes**         List of classes the interface belongs to

        **env**             shell environment the interface needs during
                            execution

        **raw_config**      raw interface config from file
    """

    # flag to indicate that the object was created from pickled state
    _PICKLED = 0x00000001
    HAS_SIBLINGS = 0x00000010
    IFACERANGE_ENTRY = 0x00000100
    IFACERANGE_START = 0x00001000

    version = '0.1'

    def __init__(self, attrsdict={}):
        self._set_attrs_from_dict(attrsdict)
        self._config_status = {}
        """dict with config status of iface attributes"""
        self.state = ifaceState.NEW
        """iface state (of type ifaceState) """
        self.status = ifaceStatus.UNKNOWN
        """iface status (of type ifaceStatus) """
        self.status_str = None
        """iface status str (string representing the status) """
        self.flags = 0x0
        """iface flags """
        self.priv_flags = 0x0
        """iface module flags dictionary with module name: flags"""
        self.module_flags = {}
        """iface priv flags. can be used by the external object manager """
        self.refcnt = 0
        """iface refcnt (incremented for each dependent this interface has) """
        self.lowerifaces = None 
        """lower iface list (in other words: slaves of this interface """
        self.upperifaces = None
        """upper iface list (in other words: master of this interface """
        self.classes = []
        """interface classes this iface belongs to """
        self.env = None
        """environment variable dict required for this interface to run"""
        self.raw_config = []
        """interface config/attributes in raw format (eg: as it appeared in the interfaces file)"""
        self.linkstate = None
        """linkstate of the interface"""
        self.type = ifaceType.UNKNOWN
        """interface type"""
        self.priv_data = None
        self.role = ifaceRole.UNKNOWN
        self.realname = None
        self.link_type = ifaceLinkType.LINK_UNKNOWN
        self.link_kind = ifaceLinkKind.UNKNOWN

        # The below attribute is used to disambiguate between various
        # types of dependencies
        self.dependency_type = ifaceDependencyType.UNKNOWN

    def _set_attrs_from_dict(self, attrdict):
        self.auto = attrdict.get('auto', False)
        self.name = attrdict.get('name')
        self.addr_family = attrdict.get('addr_family')
        self.addr_method = attrdict.get('addr_method')
        self.config = attrdict.get('config', OrderedDict())

    def inc_refcnt(self):
        """ increment refcnt of the interface. Usually used to indicate that
        it has dependents """
        self.refcnt += 1

    def dec_refcnt(self):
        """ decrement refcnt of the interface. Usually used to indicate that
        it has lost its dependent """
        self.refcnt -= 1

    def is_config_present(self):
        """ returns true if the interface has user provided config,
        false otherwise """
        addr_method = self.addr_method
        if addr_method and addr_method in ['dhcp', 'dhcp6', 'loopback']:
            return True
        if not self.config:
            return False
        else:
            return True

    def set_class(self, classname):
        """ appends class to the interfaces class list """
        self.classes.append(classname)

    def set_state_n_status(self, state, status):
        """ sets state and status of an interface """
        self.state = state
        self.status = status

    def set_flag(self, flag):
        self.flags |= flag

    def clear_flag(self, flag):
        self.flags &= ~flag

    def add_to_upperifaces(self, upperifacename):
        """ add to the list of upperifaces """
        if self.upperifaces:
            if upperifacename not in self.upperifaces:
                self.upperifaces.append(upperifacename)
        else:
            self.upperifaces = [upperifacename]

    def get_attr_value(self, attr_name):
        """ add to the list of upperifaces """
        return self.config.get(attr_name)
    
    def get_attr_value_first(self, attr_name):
        """ get first value of the specified attr name """
        attr_value_list = self.config.get(attr_name)
        if attr_value_list:
            return attr_value_list[0]
        return None

    def get_attrs_value_first(self, attrs):
        """ get first value of the first attr in the list.
            Useful when you have multiple attrs representing the
            same thing.
        """
        for attr in attrs:
            attr_value_list = self.config.get(attr)
            if attr_value_list:
                return attr_value_list[0]
        return None

    def get_attr_value_n(self, attr_name, attr_index):
        """ get n'th value of the specified attr name """
        attr_value_list = self.config.get(attr_name)
        if attr_value_list:
            try:
                return attr_value_list[attr_index]
            except:
                return None
        return None

    def get_env(self):
        """ get shell environment variables the interface must execute in """
        if not self.env:
            self.generate_env()
        return self.env

    def generate_env(self):
        """ generate shell environment variables dict interface must execute
        in. This is used to support legacy ifupdown scripts
        """
        env = {}
        config = self.config
        env['IFACE'] = self.name
        for attr, attr_value in config.items():
            attr_env_name = 'IF_%s' %attr.upper().replace("-", "_")
            env[attr_env_name] = attr_value[0]
        self.env = env

    def update_config(self, attr_name, attr_value):
        """ add attribute name and value to the interface config """
        self.config.setdefault(attr_name, []).append(attr_value)

    def replace_config(self, attr_name, attr_value):
        """ add attribute name and value to the interface config """
        self.config[attr_name] = [attr_value]

    def delete_config(self, attr_name):
        """ add attribute name and value to the interface config """
        try:
            del self.config[attr_name]
        except:
            pass

    def update_config_dict(self, attrdict):
        self.config.update(attrdict)

    def update_config_with_status(self, attr_name, attr_value, attr_status=0):
        """ add attribute name and value to the interface config and also
        update the config_status dict with status of this attribute config """
        if not attr_value:
            attr_value = ''
        self.config.setdefault(attr_name, []).append(attr_value)
        self._config_status.setdefault(attr_name, []).append(attr_status)
        # set global iface state
        if attr_status == 1:
            self.status = ifaceStatus.ERROR
        elif self.status != ifaceStatus.ERROR:
            # Not already error, mark success
            self.status = ifaceStatus.SUCCESS

    def check_n_update_config_with_status_many(self, ifaceobjorig, attr_names,
                                               attr_status=0):
        # set multiple attribute status to zero
        # also updates status only if the attribute is present
        for attr_name in attr_names:
            if not ifaceobjorig.get_attr_value_first(attr_name):
               continue
            self.config.setdefault(attr_name, []).append('')
            self._config_status.setdefault(attr_name, []).append(attr_status)

    def get_config_attr_status(self, attr_name, idx=0):
        """ get status of a attribute config on this interface.
        
        Looks at the iface _config_status dict"""
        return self._config_status.get(attr_name, [])[idx]

    def compare(self, dstiface):
        """ compares iface object with iface object passed as argument

        Returns True if object self is same as dstiface and False otherwise """

        if self.name != dstiface.name: return False
        if self.type != dstiface.type: return False
        if self.addr_family != dstiface.addr_family: return False
        if self.addr_method != dstiface.addr_method: return False
        if self.auto != dstiface.auto: return False
        if self.classes != dstiface.classes: return False
        if len(self.config) != len(dstiface.config):
            return False
        if any(True for k in self.config if k not in dstiface.config):
            return False
        if any(True for k,v in self.config.items()
                    if v != dstiface.config.get(k)): return False
        return True


    def __getstate__(self):
        odict = self.__dict__.copy()
        del odict['state']
        del odict['status']
        del odict['lowerifaces']
        del odict['upperifaces']
        del odict['refcnt']
        del odict['_config_status']
        del odict['flags']
        del odict['priv_flags']
        del odict['module_flags']
        del odict['raw_config']
        del odict['linkstate']
        del odict['env']
        del odict['link_type']
        del odict['link_kind']
        del odict['role']
        del odict['dependency_type']
        return odict

    def __setstate__(self, dict):
        self.__dict__.update(dict)
        self._config_status = {}
        self.state = ifaceState.NEW
        self.status = ifaceStatus.UNKNOWN
        self.refcnt = 0
        self.flags = 0
        self.lowerifaces = None
        self.upperifaces = None
        self.linkstate = None
        self.env = None
        self.role = ifaceRole.UNKNOWN
        self.priv_flags = 0
        self.module_flags = {}
        self.raw_config = []
        self.flags |= self._PICKLED
        self.link_type = ifaceLinkType.LINK_NA
        self.link_kind = ifaceLinkKind.UNKNOWN
        self.dependency_type = ifaceDependencyType.UNKNOWN

    def dump_raw(self, logger):
        indent = '  '
        if self.auto:
            print ('auto %s' %self.name)
        print (self.raw_config[0])
        for i in range(1, len(self.raw_config)):
            print(indent + self.raw_config[i])
        
    def dump(self, logger):
        indent = '\t'
        logger.info(self.name + ' : {')
        logger.info(indent + 'family: %s' %self.addr_family)
        logger.info(indent + 'method: %s' %self.addr_method)
        logger.info(indent + 'flags: %x' %self.flags)
        logger.info(indent + 'state: %s'
                %ifaceState.to_str(self.state))
        logger.info(indent + 'status: %s'
                %ifaceStatus.to_str(self.status))
        logger.info(indent + 'refcnt: %d' %self.refcnt)
        d = self.lowerifaces
        if d:
            logger.info(indent + 'lowerdevs: %s' %str(d))
        else:
            logger.info(indent + 'lowerdevs: None')

        logger.info(indent + 'config: ')
        config = self.config
        if config:
            logger.info(indent + indent + str(config))
        logger.info('}')

    def dump_pretty(self, with_status=False,
                    successstr='success', errorstr='error',
                    unknownstr='unknown', use_realname=False):
        indent = '\t'
        outbuf = ''
        if use_realname and self.realname:
            name = '%s' %self.realname
        else:
            name = '%s' %self.name
        if self.auto:
            outbuf += 'auto %s\n' %name
        ifaceline = ''
        if self.type == ifaceType.BRIDGE_VLAN:
            ifaceline += 'vlan %s' %name
        else:
            ifaceline += 'iface %s' %name
        if self.addr_family:
            ifaceline += ' %s' %self.addr_family
        if self.addr_method:
            ifaceline += ' %s' %self.addr_method
        if with_status:
            status_str = None
            if (self.status == ifaceStatus.ERROR or
                    self.status == ifaceStatus.NOTFOUND):
                if self.status_str:
                    ifaceline += ' (%s)' %self.status_str
                status_str = errorstr
            elif self.status == ifaceStatus.SUCCESS:
                status_str = successstr
            if status_str:
               outbuf += '{0:65} {1:>8}'.format(ifaceline, status_str) + '\n'
            else:
                outbuf += ifaceline + '\n'
            if self.status == ifaceStatus.NOTFOUND:
                outbuf = (outbuf.encode('utf8')
                    if isinstance(outbuf, unicode) else outbuf)
                print (outbuf + '\n')
                return
        else:
            outbuf += ifaceline + '\n'
        config = self.config
        if config:
            for cname, cvaluelist in config.items():
                idx = 0
                for cv in cvaluelist:
                    if with_status:
                        s = self.get_config_attr_status(cname, idx)
                        if s == -1:
                            status_str = unknownstr
                        elif s == 1:
                            status_str = errorstr
                        elif s == 0:
                            status_str = successstr
                        outbuf += (indent + '{0:55} {1:>10}'.format(
                                   '%s %s' %(cname, cv), status_str)) + '\n'
                    else:
                        outbuf += indent + '%s %s\n' %(cname, cv)
                    idx += 1
        if with_status:
            outbuf = (outbuf.encode('utf8')
                        if isinstance(outbuf, unicode) else outbuf)
        print (outbuf)
