#! /usr/bin/python
# Copyright 2015 Cumulus Networks LLC, all rights reserved
# Copyright 2015,2016,2018 Cumulus Networks, Inc.  All rights reserved


try:
    import exceptions
    import warnings
    import sys
    import traceback
    import time
    import argparse
    import re
    import os
    import syslog
    import subprocess
    import signal
    import imp
    import json
    import poe_base
except ImportError, e:
    raise ImportError (str(e) + "- required module not found")

platform_detect = '/usr/bin/platform-detect'
platform_root = '/usr/share/cumulus-platform'
base_root = '/usr/lib/python2.7/dist-packages'

g_log=None
g_state = None
g_poe = None
g_dbg = None

START_LLDP_POWER=poe_base.MAX_POE_POWER
MIN_LLDP_POWER=1000

class PoeState():
    state_root = '/run/cache/cumulus/unit_state/POE'
    POE_STATE_NOT_RUNNING='BAD'
    POE_STATE_OK='OK'
    POE_STATE_BAD='BAD'

    def __init__(self):
        self.state = self.POE_STATE_NOT_RUNNING
        self.error_ports = []

    def add_error_port(self, swp):
        if not swp in self.error_ports:
            self.error_ports.append(swp)

    def remove_error_port(self, swp):
        if swp in self.error_ports:
            self.error_ports.remove(swp)

    def update_state(self, stopped=False):
        if stopped:
            self.state = self.POE_STATE_NOT_RUNNING
        else:
            if len(self.error_ports):
                self.state = self.POE_STATE_BAD
            else:
                self.state = self.POE_STATE_OK
        self._save()

    def _save(self):
        if not os.path.exists(self.state_root):
            try:
                os.makedirs(self.state_root)
                return
            except:
                pass
        state_file = os.path.join(self.state_root, 'state')
        try:
            open(state_file, 'w').write(self.state+'\n')
        except:
            g_log.error('Could not write to state file: %s' % (state_file))

class PoedLog(object):
    def __init__(self, use_syslog=True, use_console=False):
        self.use_syslog = use_syslog
        self.use_console = use_console

    def error(self, msg):
        self._log(syslog.LOG_ERR, 'ERROR : %s' % (msg))

    def warn(self, msg):
        self._log(syslog.LOG_WARNING, 'WARN : %s' % (msg))

    def info(self, msg):
        self._log(syslog.LOG_INFO, 'INFO : %s' % (msg))

    def dbg(self, msg):
        self._log(syslog.LOG_DEBUG, 'DBG : %s' % (msg))

    def _log(self, level, msg):
        if self.use_syslog:
            syslog.syslog(level, msg)
        if level == syslog.LOG_ERR:
            sys.stderr.write('%s : %s' % (sys.argv[0][sys.argv[0].rfind('/')+1:], msg))
        if self.use_console:
            sys.stdout.write(msg)

class LldpCfgPort(object):
    def __init__(self, lldp_cfg, lldp_port):
        self.lldp_cfg = lldp_cfg
        self.lldp_port = lldp_port
        self._tlv_oui = None
        self.requested_power = None
        self.allocated_power = None
        self.tlv_cmd = None
        self.power_cmd = None
        self._update_power = False
        self._update_tlv = False

    def update_port_power(self, requested_power, allocated_power):
        if self.requested_power != requested_power or self.allocated_power != allocated_power:
            self.requested_power = requested_power
            self.allocated_power = allocated_power
            self.power_cmd = ['lldpcli', 'configure', 'ports', '%s' % self.lldp_port.poe_port.swp,
              'dot3', 'power',  'pse', 'class', 'class-%u' % self.lldp_port.lldp_pd_class, 'supported', 'enabled',
              'powerpairs', 'spare', 'type', '2', 'source', 'primary', 'priority',
              self.lldp_port.lldp_priority, 'requested', '%u' % self.requested_power, 'allocated', '%u' % self.allocated_power]
            self.update_power = True

    def _update_tlv_cmd(self):
        self.tlv_cmd = None
        if self.tlv_oui:
            self.tlv_cmd = ['lldpcli', 'configure', 'ports', self.lldp_port.poe_port.swp, 'lldp', 'custom-tlv', 'oui', '00,01,42', 'subtype', '01', 'oui-info', self.tlv_oui]
        self.update_tlv = True

    @property
    def update_power(self):
        return self._update_power
    @update_power.setter
    def update_power(self, value):
        if value != self._update_power:
            self.dbg('update_power %s => %s' % (self._update_power, value))
            self._update_power = value
            self.lldp_cfg.update_needed = True

    @property
    def update_tlv(self):
        return self._update_tlv
    @update_tlv.setter
    def update_tlv(self, value):
        if value != self._update_tlv:
            self.dbg('update_tlv %s => %s' % (self._update_tlv, value))
            self._update_tlv = value
            self.lldp_cfg.update_needed = True

    @property
    def tlv_oui(self):
        return self._tlv_oui
    @tlv_oui.setter
    def tlv_oui(self, value):
        if value != self._tlv_oui:
            self.dbg('tlv_oui %s => %s' % (self._tlv_oui, value))
            self._tlv_oui = value
            self._update_tlv_cmd()

    def dbg(self, msg):
        self.lldp_port.dbg(msg)

class PoeLldpCfg(object):
    poe_lldp_cfg_file = '/etc/lldpd.d/poed.conf'

    def __init__(self):
        self.ports = {}
        self.update_needed = False
        self._lldp_err = False

    def update_port_tlv(self, lldp_port):
        port_cfg = self._get_create_port_cfg(lldp_port)
        tlv_oui = None
        if lldp_port.poe_port.port_ready:
            if lldp_port.poe_port.hw_pair_mode == lldp_port.PORT_MODE_4_PAIR:
                tlv_oui = '0D'
            else:
                tlv_oui = '01' if lldp_port.poe_port.support_4pair else '00'
        port_cfg.tlv_oui = tlv_oui

    def update_port_power(self, lldp_port, requested_power, allocated_power):
        port_cfg = self._get_create_port_cfg(lldp_port)
        port_cfg.update_port_power(requested_power, allocated_power)

    def reset_port(self, lldp_port):
        self.ports.pop(lldp_port.poe_port.swp, None)
        self.update_needed = True

    def _mk_cfg(self, cmd_list):
        new_cmd = ''
        for cmd in cmd_list[1:]:
            new_cmd += ' ' + cmd
        return new_cmd

    def _get_create_port_cfg(self, lldp_port):
        port_cfg = self._get_port_cfg(lldp_port)
        if not port_cfg:
            port_cfg = LldpCfgPort(self, lldp_port)
            self.ports[lldp_port.poe_port.swp] = port_cfg
        return port_cfg

    def _get_port_cfg(self, lldp_port):
        return self.ports.get(lldp_port.poe_port.swp, None)

    def update(self):
        update_needed = False
        paused = False
        for swp, port_cfg in self.ports.items():
            if port_cfg.update_power and port_cfg.power_cmd:
                try:
                    if not paused:
                        paused = True
                        g_log.dbg('lldp pause')
                        subprocess.check_output(['lldpcli', 'pause'])
                    subprocess.check_output(port_cfg.power_cmd)
                    port_cfg.lldp_port.dbg(str(port_cfg.power_cmd))
                    port_cfg.update_power = False
                    update_needed = True
                    continue
                except subprocess.CalledProcessError:
                    g_log.dbg('lldp cmd error')
                    pass

            if port_cfg.update_tlv and port_cfg.tlv_cmd:
                try:
                    if not paused:
                        paused = True
                        g_log.dbg('lldp pause')
                        subprocess.check_output(['lldpcli', 'pause'])
                    subprocess.check_output(['lldpcli', 'unconfigure', 'ports', swp, 'lldp', 'custom-tlv'])
                    subprocess.check_output(port_cfg.tlv_cmd)
                    port_cfg.lldp_port.dbg(str(port_cfg.tlv_cmd))
                    port_cfg.update_tlv = False
                    update_needed = True
                except subprocess.CalledProcessError:
                    g_log.dbg('lldp cmd error')
                    # this happens when lldpd is not running
                    pass
        if paused:
            g_log.dbg('lldp resume')
            try:
                subprocess.check_output(['lldpcli', 'resume'])
                self._lldp_err = False
            except subprocess.CalledProcessError:
                # most likely lldpd is not running
                if not self._lldp_err:
                    g_log.error('Unable to execute LLDP command. Is lldpd running?')
                    self._lldp_err = True

        if self.update_needed or update_needed:
            g_log.dbg('lldp cfg update')
            f = open(self.poe_lldp_cfg_file, 'w')
            for swp, port_cfg in self.ports.items():
                if port_cfg.power_cmd:
                    f.write('%s\n' % self._mk_cfg(port_cfg.power_cmd))
                if port_cfg.tlv_cmd:
                    f.write(' unconfigure ports %s lldp custom-tlv\n' % swp)
                    f.write('%s\n' % self._mk_cfg(port_cfg.tlv_cmd))
            f.close()
            self.update_needed = False

    def force_port_update(self, lldp_port):
        port_cfg = self._get_create_port_cfg(lldp_port)
        port_cfg.update_power = True
        port_cfg.update_tlv = True

class PoeLldpPort(object):
    PORT_LLDP_STATE_DOWN='down'
    PORT_LLDP_STATE_UP='up'
    PORT_LLDP_STATE_POWERING='powering'

    PORT_MODE_2_PAIR='2-pair'
    PORT_MODE_4_PAIR='4-pair'

    lldp_priority_map = { 'unknown':-1, 'low':poe_base.PORT_CFG_PRIORITY_LOW, 'high':poe_base.PORT_CFG_PRIORITY_HIGH,
                          'critical':poe_base.PORT_CFG_PRIORITY_CRITICAL }

    def __init__(self, poe_system, poe_port):
        self.poe_system = poe_system
        self.poe_port = poe_port
        self._lldp_port_ready = False
        self._update_needed = False
        self.init_lldp_state()

    def init_lldp_state(self):
        self._lldp_state = self.PORT_LLDP_STATE_DOWN
        self._lldp_pd_class = None
        self._lldp_extra_class = None
        self._lldp_priority = 'low'
        self._requested_power = 0
        self._allocated_power = 0
        self._requested_pair_mode = self.PORT_MODE_2_PAIR
        self._advertised_pair_mode = self.PORT_MODE_2_PAIR
        self.poe_system.lldp_cfg.reset_port(self)
        self._update_limit = 30
        self._update_count = 0

    def reinit_lldp_state(self):
        g_log.dbg('%s: reinit LLDP' % self.poe_port.swp)
        self.init_lldp_state()
        self.poe_port.exit_l2_mode()
        self.poe_port.set_hw_pair_mode(self.requested_pair_mode)

    def handle_lldp_info(self, lldp_info):
        self.lldp_port_ready = self.poe_port.port_ready
        if self.lldp_state != self.PORT_LLDP_STATE_DOWN and self.poe_port.hw_status != self.poe_port.PORT_HW_STATUS_CONNECTED:
            ''' port has transitioned to not_connected
            '''
            self.reinit_lldp_state()
        elif self.lldp_state != self.PORT_LLDP_STATE_DOWN and not self.poe_port.port_ready:
            ''' port has transitioned to down, but is still connected
            '''
            self.lldp_state = self.PORT_LLDP_STATE_DOWN
            self._update_limit = 30
        elif self.lldp_state == self.PORT_LLDP_STATE_DOWN and self.poe_port.port_ready:
            ''' port has transitioned to ready, get lldp started
            '''
            self.lldp_state = self.PORT_LLDP_STATE_UP
            self.poe_system.lldp_cfg.update_port_tlv(self)
            self.poe_system.lldp_cfg.force_port_update(self)
        else:
            ''' port was already connected
            '''
            if self._update_limit:
                self._update_count += 1
                if self._update_count == self._update_limit:
                    self.poe_system.lldp_cfg.force_port_update(self)
                    self._update_count = 0
            tlvs = lldp_info.get('unknown-tlvs')
            try:
                lldp_port_power = lldp_info.get('port')[0].get('power')
                if lldp_port_power:
                    self.lldp_state = self.PORT_LLDP_STATE_POWERING
                    power_item = lldp_port_power[0]
                    self.requested_power = min(self.poe_port.max_power, int(power_item.get('requested', [{'value':'0'}])[0].get('value', '0')))
                    class_item = power_item.get('class')
                    class_extra = power_item.get('power-class-ext')
                    if class_item and len(class_item):
                        self.lldp_pd_class = int(class_item[0].get('value').split(' ')[1])
                    if class_extra and len(class_extra):
                        self.lldp_extra_class = int(class_extra[0].get('value').split(' ')[1])
                    priority_item = power_item.get('priority')
                    if priority_item and len(priority_item):
                        self.lldp_priority = priority_item[0].get('value').lower()
            except Exception, e:
                pass

            if tlvs:
                ''' look for a tlv with oui=00,01,42 and value=5
                '''
                for tlv in tlvs:
                    unk_tlvs = tlv.get('unknown-tlv')
                    for unk_tlv in unk_tlvs:
                        if unk_tlv.get('oui') == '00,01,42':
                            self._update_limit = 0
                            requested_pair_mode = self.requested_pair_mode
                            value = unk_tlv.get('value', '0')
                            if value == '05' or value == '5':
                                if self.poe_port.support_4pair:
                                    ''' the device is asking for 4 pair power
                                    '''
                                    requested_pair_mode = self.PORT_MODE_4_PAIR
                            elif value == '1' or value == '01':
                                requested_pair_mode = self.PORT_MODE_2_PAIR
                            self.requested_pair_mode = requested_pair_mode

    def update(self):
        if self.update_needed:
            if self.requested_pair_mode != self.poe_port.hw_pair_mode:
                self.poe_port.set_hw_pair_mode(self.requested_pair_mode)

            if self.lldp_port_ready:
                if self.advertised_pair_mode != self.poe_port.hw_pair_mode:
                    self.advertised_pair_mode = self.poe_port.hw_pair_mode
                    self.poe_system.lldp_cfg.update_port_tlv(self)

                if self.lldp_state == self.PORT_LLDP_STATE_POWERING:
                    if self.allocated_power != self.requested_power:
                        if self.requested_power <= self.poe_port.max_power:
                            self.allocated_power = max(self.requested_power, MIN_LLDP_POWER)
                            req = max(self.requested_power, MIN_LLDP_POWER)
                            self.dbg('set L2 params: r-%u, a-%u' % (req, self.allocated_power))
                            self.poe_system.set_l2_pm_data(self.poe_port, req, self.allocated_power)
                self.poe_system.lldp_cfg.update_port_power(self, self.requested_power, self.allocated_power)
        self.update_needed = False

    def __str__(self):
        if self.lldp_state == self.PORT_LLDP_STATE_POWERING:
            return '%.1f W' % (self.allocated_power / 1000.0)
        return ''

    def dbg(self, msg):
        self.poe_port.dbg(msg)

    @property
    def requested_power(self):
        return self._requested_power
    @requested_power.setter
    def requested_power(self, value):
        if value != self._requested_power:
            self.dbg('requested_power %s => %s' % (self._requested_power, value))
            self._update_needed = True
            self._requested_power = value

    @property
    def allocated_power(self):
        return self._allocated_power
    @allocated_power.setter
    def allocated_power(self, value):
        if value != self._allocated_power:
            self.dbg('allocated_power %s => %s' % (self._allocated_power, value))
            self._update_needed = True
            self._allocated_power = value

    @property
    def requested_pair_mode(self):
        return self._requested_pair_mode
    @requested_pair_mode.setter
    def requested_pair_mode(self, value):
        if value != self._requested_pair_mode:
            self.dbg('requested_pair_mode %s => %s' % (self._requested_pair_mode, value))
            self._requested_pair_mode = value
            self._update_needed = True

    @property
    def advertised_pair_mode(self):
        return self._advertised_pair_mode
    @advertised_pair_mode.setter
    def advertised_pair_mode(self, value):
        if value != self._advertised_pair_mode:
            self.dbg('advertised_pair_mode %s => %s' % (self._advertised_pair_mode, value))
            self._advertised_pair_mode = value

    @property
    def lldp_state(self):
        return self._lldp_state
    @lldp_state.setter
    def lldp_state(self, value):
        if value != self._lldp_state:
            self.dbg('lldp_state "%s" => "%s"' % (self._lldp_state, value))
            self._lldp_state = value

    @property
    def lldp_port_ready(self):
        return self._lldp_port_ready
    @lldp_port_ready.setter
    def lldp_port_ready(self, value):
        if value != self._lldp_port_ready:
            self.dbg('lldp_port_ready "%s" => "%s"' % (self._lldp_port_ready, value))
            self._lldp_port_ready = value
            self._update_needed = True

    @property
    def lldp_pd_class(self):
        if self._lldp_pd_class != None:
            return self._lldp_pd_class
        return 4

    @lldp_pd_class.setter
    def lldp_pd_class(self, value):
        if value != self._lldp_pd_class and value in range(5):
            self.dbg('lldp_pd_class %u => %u' % (self.lldp_pd_class, value))
            self._lldp_pd_class = value
            self._update_needed = True

    @property
    def lldp_extra_class(self):
        if self._lldp_extra_class is not None:
            return self._lldp_extra_class
        return 4

    @lldp_extra_class.setter
    def lldp_extra_class(self, value):
        if value != self._lldp_pd_class and value in range(9):
            self._lldp_extra_class = value
        else:
            self._lldp_extra_class = self._lldp_pd_class

    @property
    def lldp_priority(self):
        return self._lldp_priority

    @lldp_priority.setter
    def lldp_priority(self, value):
        hw_val = self.lldp_priority_map.get(value, -1)
        if hw_val >= 0 and value != self._lldp_priority:
            self.dbg('lldp_priority %s => %s' % (self._lldp_priority, value))
            self._lldp_priority = value
            self.poe_system.set_port_priority(self.poe_port, hw_val)
            self._update_needed = True

    @property
    def update_needed(self):
        return self._update_needed
    @update_needed.setter
    def update_needed(self, value):
        self._update_needed = value

class PoePort(object):

    PORT_HW_STATUS_NONE='none'
    PORT_HW_STATUS_FAULT='fault'
    PORT_HW_STATUS_DISABLED='disabled'
    PORT_HW_STATUS_DENIED='power-denied'
    PORT_HW_STATUS_CONNECTED='connected'

    PORT_LINK_STATE_UP='up'
    PORT_LINK_STATE_DOWN='down'

    def __init__(self, poe_system, port_id, port_params):
        self.poe_system = poe_system
        self.port_id = port_id
        self.swp = port_params.swp
        self.support_4pair = port_params.support_4pair
        self.base_power = port_params.base_power
        self.max_power = max(port_params.base_power, port_params.max_power)
        self.update_hw_pair_mode = False
        self._hw_pair_mode = PoeLldpPort.PORT_MODE_2_PAIR
        self._hw_status = self.PORT_HW_STATUS_NONE
        self._link_state = self.PORT_LINK_STATE_DOWN
        self.lldp_port = PoeLldpPort(poe_system, self)
        self.status = {}

    def update_hw_status(self, new_status):
        self.status = new_status
        if self.update_hw_pair_mode:
            result = self.poe_system.poe.get_port_4pair_state(self.port_id)
            self.dbg(result)
            self.hw_pair_mode = result.get(self.port_id, {}).get('pair_mode')
            self.dbg('read hw pair mode %s' % self.hw_pair_mode)
            if self.hw_pair_mode == self.lldp_port.requested_pair_mode:
                self.update_hw_pair_mode = False

        ps = self.hw_status
        ns = new_status.get('status')

        if ns != ps:
            self.hw_status = ns
            if ns == self.PORT_HW_STATUS_FAULT:
                g_log.error('POE error on %s: "%s"\n' % (self.swp, new_status.get('error_str')))
                g_state.add_error_port(self.swp)
                return
            if ps == self.PORT_HW_STATUS_FAULT:
                g_state.remove_error_port(self.swp)
            if ns == self.PORT_HW_STATUS_DISABLED:
                g_log.info('POE operation is administratively disabled on port %s\n' % (self.swp))
            if ns == self.PORT_HW_STATUS_DENIED:
                g_log.warn( 'POE power denied to device on port %s\n' % (self.swp))
            if ps == self.PORT_HW_STATUS_DISABLED:
                g_log.info('POE operation enabled on port %s\n' % (self.swp))
            if ns == self.PORT_HW_STATUS_CONNECTED:
                g_log.info('POE device is connected to port %s\n' % (self.swp))
            if ps == self.PORT_HW_STATUS_CONNECTED:
                g_log.info('POE device has been removed from port %s\n' % (self.swp))
                self.lldp_port.reinit_lldp_state()

    def update_link_status(self):
        link = self.PORT_LINK_STATE_DOWN
        try:
            if open('/sys/class/net/%s/carrier' % self.swp, 'r').read().strip() == '1':
                link = self.PORT_LINK_STATE_UP
        except Exception, e:
            pass
        self.link_state = link
        return link == self.PORT_LINK_STATE_UP

    def update_lldp_status(self, lldp_info={}):
        self.lldp_port.handle_lldp_info(lldp_info)
        self.lldp_port.update()

    def set_hw_pair_mode(self, pair_mode):
        self.dbg('set_hw_pair_mode: %s' % pair_mode)
        if pair_mode == PoeLldpPort.PORT_MODE_4_PAIR:
            self.dbg('  set_enable_4pair_power()')
            self.poe_system.poe.set_enable_4pair_power(self.port_id)
        else:
            self.dbg('  set_disable_4pair_power()')
            self.poe_system.poe.set_disable_4pair_power(self.port_id)
        self.update_hw_pair_mode = True

    def exit_l2_mode(self):
        self.poe_system.poe.exit_port_l2_mode(self.port_id)

    def get_running_state(self):
        allocated = 0
        if self.hw_status == self.PORT_HW_STATUS_CONNECTED:
            allocated = self.lldp_port.allocated_power
        status = {
            "swp":self.swp, "hw_status":self.hw_status, "lldp_status":str(self.lldp_port),
            "max_port_power":self.max_power, "lldp_allocated_power":allocated,
            "lldp_requested_power":self.lldp_port.requested_power,
            "four_pair_mode_enabled":(self.hw_pair_mode == PoeLldpPort.PORT_MODE_4_PAIR),
        }
        for option in ['pd_type', 'pd_class']:
            val = self.status.get(option)
            if val:
                status[option] = val
            # if we received a pd_class via lldp use that instead of HW value
            if option == 'pd_class' and self.lldp_port._lldp_extra_class != None:
                status[option] = str(self.lldp_port._lldp_extra_class)
        return status

    @property
    def port_ready(self):
        return self.hw_status == self.PORT_HW_STATUS_CONNECTED and self.link_state == self.PORT_LINK_STATE_UP

    @property
    def hw_status(self):
        return self._hw_status

    @hw_status.setter
    def hw_status(self, value):
        if value != self.hw_status:
            self.dbg('hw_status: %s => %s' % (self._hw_status, value))
            self._hw_status = value

    @property
    def hw_pair_mode(self):
        return self._hw_pair_mode

    @hw_pair_mode.setter
    def hw_pair_mode(self, value):
        if value and value != self.hw_pair_mode:
            self.dbg('hw_pair_mode: %s => %s' % (self._hw_pair_mode, value))
            self._hw_pair_mode = value
            self.lldp_port.update_needed = True

    @property
    def link_state(self):
        return self._link_state

    @link_state.setter
    def link_state(self, value):
        if value != self.link_state:
            self.dbg('link_state: %s => %s' % (self._link_state, value))
            self._link_state = value

    def dbg(self, msg):
        if g_dbg and self.swp in g_dbg and self.hw_status == self.PORT_HW_STATUS_CONNECTED:
            g_log.dbg('%5s: %s\n' % (self.swp, str(msg)))

class PoeSystem(object):
    def __init__(self, poe_params, poe):
        self.poe = poe
        self.consumed_power = 0.0
        self.power_limit = 0.0
        self.ports = {}
        self.swps = []
        self.update_pair_modes = False
        self.lldp_ready = False
        self.lldp_cfg = PoeLldpCfg()
        for port_id, port_params in poe_params.ports.items():
            poe_port = PoePort(self, port_id, port_params)
            self.swps.append(port_params.swp)
            self.ports[port_params.swp] = poe_port
            self.ports[port_id] = poe_port

    def update_status(self, sys_status):
        ''' system status '''
        self.consumed_power = float(sys_status.get('sys_consumed_power', 0))
        self.power_limit =  float(sys_status.get('sys_power_limit'))

        ''' port hw status '''
        for port_id, port_status in  sys_status.get('ports', {}).items():
            try:
                self.ports.get(port_status.get('swp')).update_hw_status(port_status)
            except:
                pass

        ''' port link status '''
        for swp in self.swps:
            if self.ports.get(swp).update_link_status():
                self.lldp_ready = True

        ''' port lldp status '''
        self._update_ports_lldp_status()

        ''' lldp configuration '''
        self.lldp_cfg.update()

    def get_running_state(self):
        running_state = {
            'sys_consumed_power':self.consumed_power,
            'sys_power_limit':self.power_limit,
            'ports':self.get_ports_running_state()
        }
        return running_state

    def set_l2_pm_data(self, poe_port, requested, allocated):
        self.poe.set_port_l2_power(poe_port.port_id, requested, allocated)

    def set_port_priority(self, poe_port, level):
        self.poe.set_port_priority(poe_port.port_id, level)

    def get_poe_port(self, port_id):
        return self.ports.get(port_id)

    def get_ports_running_state(self):
        running_state = {}
        for swp, port in self.ports.items():
            running_state[port.port_id] = port.get_running_state()
        return running_state

    def _update_ports_lldp_status(self):
        lldp_status = {}
        if self.lldp_ready:
            try:
                lldp_status = json.loads(subprocess.check_output(['lldpcli', 'show', 'neighbors', 'protocol', 'lldp', 'hidden', 'details', '-f', 'json']))
            except:
                pass
        interfaces = []
        lldp = lldp_status.get('lldp')
        if lldp:
            interfaces = lldp[0].get('interface', [])
        all_ports = set(self.swps)
        for interface in interfaces:
            swp = interface.get('name')
            poe_port = self.get_poe_port(swp)
            if poe_port and poe_port.hw_status == poe_port.PORT_HW_STATUS_CONNECTED:
                if poe_port.swp in all_ports:
                    all_ports.remove(poe_port.swp)
                poe_port.update_lldp_status(interface)
        for swp in all_ports:
            poe_port = self.get_poe_port(swp)
            if poe_port:
                poe_port.update_lldp_status()

def main():
    global g_poe
    global g_dbg
    if not os.geteuid() == 0:
        raise RuntimeError("must be root to run")

    parser = argparse.ArgumentParser(description="Power Over Ethernet (POE) system monitor")
    parser.add_argument('-c', '--console', action='store_true', help =argparse.SUPPRESS)
    parser.add_argument('debug_ports', nargs='*', help=argparse.SUPPRESS)
    args = parser.parse_args()
    if args.console:
        g_log.use_console = True

    syslog.syslog(syslog.LOG_INFO, "Starting POE Daemon")

    #
    # determine the platform
    #
    try:
        ph = subprocess.Popen((platform_detect), stdout=subprocess.PIPE,
                              shell=False, stderr=subprocess.STDOUT)
        cmdout = ph.communicate()[0]
        ph.wait()
    except OSError, e:
        raise OSError("cannot detect platform")

    [platform, model] = cmdout.rstrip('\n').split(',')
    platform_path = '/'.join([platform_root, platform, model])
    #
    # load the poe class file and instantiate the object
    #
    try:
        m = imp.load_source('poe','/'.join([platform_path, 'bin/poe.py']))
    except IOError:
        m = None
    if not m:
        try:
            name = '/'.join([base_root, 'poe_base.py'])
            m = imp.load_source('poe',name)
        except IOError:
            raise IOError("cannot load module: " + name)
    poe = getattr(m, 'poe')()

    pidfile = "/run/poed.pid"
    if already_running(pidfile):
        sys.exit()
    else:
        file(pidfile, 'w').write(str(os.getpid()))

    poe_params = poe.get_poe_params()
    if poe_params.supported:
        ''' we have seen some switches where the CPLD reports that POE is supported, but there
            is no POE hardware on the switch. If we cannot communicate with the POE hardware,
            rather than continuously restart and fail, we will log the error, make ourselves idle,
            and try again in a bit (but only log the first failure).
        '''
        logged = False
        while True:
            try:
                res = poe.get_poe_version_info()
                ver_str = res.get('ver_str')
                break
            except poe_base.POEDRuntimeError:
                if not logged:
                    g_log.error('No response from POE hardware.')
                    logged = True
                time.sleep(30)
        g_log.info('POE hardware version %s\n' % ver_str)
        if len(args.debug_ports):
            g_dbg = args.debug_ports
        g_poe = poe
        poe.handle_poed_start()
        if poe.is_first_boot:
            poe.handle_first_boot()
        poe.handle_warm_cold_boot()
        prev_system_status = {}
        prev_power = None
        g_state.update_state()
        poe_system = PoeSystem(poe_params, poe)
        poe.save_sensors_info(poe_params)
        while True:
            time.sleep(poe_params.interval)
            try:
                prev_power = poe.adjust_system_poe_power(prev_power)
                new_system_status = poe.get_poe_system_status(prev_system_status != {})
                poe_system.update_status(new_system_status)
                poe.save_running_state(poe_system.get_running_state())
                poe.save_sensors_data(new_system_status, poe_params)
            except poe_base.POEDSequenceError:
                pass
            except poe_base.POEDRuntimeError, errstr:
                g_log.error("%s\n" % str(errstr))
            except Exception as catch_all:
                g_log.error("%s\n" % str(catch_all))
            g_state.update_state()
    else:
        g_log.info('POE is not supported on this platform.\n')
        g_state.update_state(True)
        while True:
            time.sleep(poe_params.interval)

#--------------------
#
# check to see if an instance is already running
#
def already_running(pidfile):
    myname=os.path.basename(sys.argv[0])
    try:
        if not os.path.isfile(pidfile):
            return False
        oldpid = re.findall('\D*(\d+).*', (file(pidfile, 'r').readline()))[0]
        if not os.path.exists('/proc/%s' % oldpid):
            return False
        if myname not in file('/proc/%s/cmdline' % oldpid, 'r').readline():
            return False
        sys.stderr.write("%s already running as process %s\n" % (myname, oldpid))
        return True
    except Exception as inst:
        raise poe_base.POEDRuntimeError("unable to validate pidfile %s: %s" %
                               (pidfile, str(inst)))

#---------------
#
# normal exit
#
def exit_normally(signum=0, frame=None):
    g_log.info('exiting normally\n')
    sys.stderr.write("%s : exiting normally\n" % sys.argv[0])
    if g_poe:
        g_poe.poe_disable()
    g_state.update_state(True)
    exit(0)

#--------------------
#
# execution check
#
if __name__ == "__main__":
    try:
        signal.signal(signal.SIGTERM, exit_normally)
        syslog.openlog()
        syslog.setlogmask(syslog.LOG_UPTO(syslog.LOG_INFO))
        g_log = PoedLog()
        g_state = PoeState()
        exit(main())
    except KeyboardInterrupt:
        exit_normally()
    except Exception, e:
        (exc_type, exc_value, exc_traceback) = sys.exc_info()
        err = ''.join(traceback.format_exception(exc_type, exc_value, exc_traceback))
        g_log.error('Unhandled Exception : %s\n' % err)
        g_state.update_state(True)
