#!/usr/bin/python
# coding: utf8
import base64
import ctypes
from ctypes.util import find_library
try:
    import cPickle as pickle
    import anydbm as dbm
except ImportError:
    import pickle
    import dbm
import datetime
import os
import sys
import tempfile
import shutil
try:
    import eventlet.green.socket as socket
    import eventlet.wsgi
except ImportError:
    if 'gencert' not in sys.argv:
        # no eventlet, start the partially implemented golang variant
        os.execv("/opt/affluent/bin/affluent-ng", ["/opt/affluent/bin/affluent-ng"])

import glob
import grp
import hashlib
try:
    import httplib
except ImportError:
    import http.client as httplib
import json
import pam
import pwd
try:
    import eventlet.green.os as os
    import eventlet.green.subprocess as subprocess
    import eventlet.green.socket as socket
    import eventlet.green.ssl as ssl
except ImportError:
    import os
    import subprocess
    import socket
    import ssl
from os.path import exists
import struct
import time
import uuid
import yaml


libc = ctypes.CDLL(find_library('libc'))
_getgrouplist = libc.getgrouplist
_getgrouplist.restype = ctypes.c_int32

RTM_NEWLINK = 16
RTM_GETLINK = 18
NLM_F_REQUEST = 1
NLM_F_DUMP = 768
NL_MSGDONE = 3
IFF_LOWER_UP = 1 << 16
IFF_MASTER = 1 << 10
IFLA_IFNAME = 3
IFLA_LINK = 5
IFLA_LINKINFO = 18
IFLA_INFO_KIND = 1


def msg_align(len):
    return (len + 3) & ~3

if sys.version_info.major == 2:
    class RestrictedUnpickler(object):
        def __init__(self, handler):
            self.handler = handler

        def load(self):
            unp = pickle.Unpickler(self.handler)
            unp.find_global = None
            return unp.load()
else:
    class RestrictedUnpickler(pickle.Unpickler):
        find_global = None
        def find_class(self, module, name):
            raise pickle.UnpicklingError("Forbidden pickle")

nlsz = struct.calcsize('IHHII')
ilen = struct.calcsize('BHiII')
rthlen = struct.calcsize('HH')


glsize = nlsz + ilen
nlhdr = struct.pack('IHHII', glsize, RTM_GETLINK, NLM_F_REQUEST|NLM_F_DUMP, 0, 0)
ifinfomsg = struct.pack('BHiII', socket.AF_PACKET, 0, 0, 0, 0)

mcastv6addr = 'ff02::c'
ssdp6mcast = socket.inet_pton(socket.AF_INET6, mcastv6addr)


class SecureHTTPConnection(httplib.HTTPConnection):

    def __init__(self, host, port, verifycallback, dev, cadata=None, **kwargs):
        self.dev = dev
        self.cadata = cadata
        httplib.HTTPConnection.__init__(self, host, port, **kwargs)
        self._certverify = verifycallback
        self.host = host
        self.port = port
        self.stdheaders = {}
        if '[' not in host and '%' in host and 'Host' not in self.stdheaders:
            self.stdheaders['Host'] = '[' + host[:host.find('%')] + ']'

    def connect(self):
        addrinfo = socket.getaddrinfo(self.host, self.port)[0]
        plainsock = socket.socket(addrinfo[0])
        if self.dev:
            plainsock.setsockopt(socket.SOL_SOCKET, 25, self.dev)
        plainsock.settimeout(60)
        plainsock.connect(addrinfo[4])
        ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH, cadata=self.cadata)
        ctx.verify_mode = ssl.CERT_REQUIRED
        self.sock = ctx.wrap_socket(plainsock)
        #bincert = self.sock.getpeercert(binary_form=True)
        #if not self._certverify(bincert):
        #    raise Exception('Invalid Certificate')


class CertVerifier(object):
    def __init__(self, certificate):
        self.certificate = certificate

    def verifycert(self, dercert):
        return dercert == self.certificate


def get_speed(iface):
    with open('/sys/class/net/{0}/speed'.format(iface)) as speedf:
        return int(speedf.read())


def get_interfaces():
    ifinfo = {}
    # first use RTM_GETLINK to enumurate devices
    s = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, socket.NETLINK_ROUTE)
    s.bind((0, 0))
    s.sendall(nlhdr + ifinfomsg)
    expected = True
    try:
        while expected:
            pdata = s.recv(65536)
            v = memoryview(pdata)
            if not len(v):
                break
            while len(v):
                length, typ = struct.unpack('IH', v[:struct.calcsize('IH')])
                if typ == NL_MSGDONE:
                    expected = False
                    break
                if typ == RTM_NEWLINK:
                    ipinfo = v[nlsz:length]
                    _, iftyp, ifidx, iflags, _ = struct.unpack('BHiII', ipinfo[:ilen])
                    if iftyp == 1 and not iflags & IFF_MASTER:  #Ether ports, ignore loopback
                        currinfo = {'if_index': ifidx}
                        rta = ipinfo[ilen:]
                        ifname = None
                        while len(rta):
                            rtalen, rtatype = struct.unpack('HH', rta[:struct.calcsize('HH')])
                            if rtalen < rthlen:
                                break
                            if rtatype == IFLA_IFNAME:
                                ifname = rta[rthlen:rtalen].tobytes()
                                ifname = ifname.strip(b'\x00')
                                if not isinstance(ifname, str):
                                    ifname = ifname.decode('utf8')
                            elif rtatype == IFLA_LINK:
                                ridx = struct.unpack('I', rta[rthlen:rtalen])
                                if ridx != ifidx:
                                    ifname = None
                                    break
                            elif rtatype == IFLA_LINKINFO:
                                linfo = rta[rthlen:rtalen]
                                kind = None
                                while len(linfo):
                                    lilen, litype = struct.unpack('HH', linfo[:4])
                                    if lilen < rthlen:
                                        break
                                    if litype == IFLA_INFO_KIND:
                                        kind = linfo[rthlen:lilen]
                                        break
                                    linfo = linfo[msg_align(lilen):]
                                if kind and kind.tobytes().strip('\x00') == b'bridge':
                                    ifname = None
                                    break
                            rta = rta[msg_align(rtalen):]
                        if ifname:
                            if iflags & IFF_LOWER_UP:
                                currinfo['link'] = 'up'
                                currinfo['linkspeed'] = get_speed(ifname)
                            else:
                                currinfo['link'] = 'down'
                                currinfo['linkspeed'] = 0
                            ifinfo[ifname] = currinfo
                if not length:
                    break
                v = v[msg_align(length):]
    finally:
        s.close()
    return ifinfo



def get_openssl_conf_location():
    if exists('/etc/pki/tls/openssl.cnf'):
        return '/etc/pki/tls/openssl.cnf'
    elif exists('/etc/ssl/openssl.cnf'):
        return '/etc/ssl/openssl.cnf'
    else:
        raise Exception("Cannot find openssl config file")

def get_ip_addresses():
    lines = subprocess.check_output('ip addr'.split(' '))
    if not isinstance(lines, str):
        lines = lines.decode('utf8')
    for line in lines.split('\n'):
        if line.startswith('    inet6 '):
            line = line.replace('    inet6 ', '').split('/')[0]
            if line.startswith('fe80::'):
                continue
            if line == '::1':
                continue
        elif line.startswith('    inet '):
            line = line.replace('    inet ', '').split('/')[0]
            if line == '127.0.0.1':
                continue
            if line.startswith('169.254.'):
                continue
        else:
            continue
        yield line

def create_certificate():
    shortname = socket.gethostname().split('.')[0]
    longname = socket.getfqdn()
    os.chdir('/etc/affluent')
    subprocess.check_call(
        'openssl ecparam -name secp384r1 -genkey -out privkey.pem'.split(' '))
    san = ['IP:{0}'.format(x) for x in get_ip_addresses()]
    san.append('DNS:{0}'.format(shortname))
    san.append('DNS:{0}'.format(longname))
    san = ','.join(san)
    sslcfg = get_openssl_conf_location()
    tmphdl, tmpconfig = tempfile.mkstemp()
    os.close(tmphdl)
    shutil.copy2(sslcfg, tmpconfig)
    with open(tmpconfig, 'a') as cfgfile:
        cfgfile.write('\n[SAN]\nsubjectAltName={0}'.format(san))
    subprocess.check_call(
        'openssl req -new -x509 -key privkey.pem -days 7300 -out cert.pem '
        '-subj /CN={0} -extensions SAN '
        '-config {1}'.format(longname, tmpconfig).split(' ')
    )

credcache = {}
showusers = {}
showgroups = {}
editusers = {}
editgroups = {}
bad_uuids = (
    '03000200-0400-0500-0006-000700080009',
    '00000000-0000-0000-0000-000000000000',
    'ffffffff-ffff-ffff-ffff-ffffffffffff',
    '00112233-4455-6677-8899-aabbccddeeff',
    '20202020-2020-2020-2020-202020202020')

class TooSmallException(Exception):
    def __init__(self, count):
        self.count = count
        super(TooSmallException, self).__init__()

def getgrouplist(name, gid, ng=32):
    _getgrouplist.argtypes = [ctypes.c_char_p, ctypes.c_uint, ctypes.POINTER(ctypes.c_uint * ng), ctypes.POINTER(ctypes.c_int)]
    glist = (ctypes.c_uint * ng)()
    nglist = ctypes.c_int(ng)
    if not isinstance(name, bytes):
        name = name.encode('utf-8')
    count = _getgrouplist(name, gid, ctypes.byref(glist), ctypes.byref(nglist))
    if count < 0:
        raise TooSmallException(nglist.value)
    for gidx in range(count):
        gent = glist[gidx]
        yield grp.getgrgid(gent).gr_name

def grouplist(name):
    pent = pwd.getpwnam(name)
    try:
        groups = getgrouplist(pent.pw_name, pent.pw_gid)
    except TooSmallException as e:
        groups = getgrouplist(pent.pw_name, pent.pw_gid, e.count)
    return list(groups)


def authorize(name, method):
    global showusers
    global showgroups
    global editusers
    global editgroups
    if method == 'GET':
        theusers = showusers
        thegroups = showgroups
    else:
        theusers = editusers
        thegroups = editgroups
    if name in theusers:
        return 200
    groups = grouplist(name)
    for group in groups:
        if group in thegroups:
            return 200
    if os.path.exists('/etc/nvue-auth.yaml'):
        with open('/etc/nvue-auth.yaml', 'r') as nain:
            adata = yaml.safe_load(nain)
        for rule in adata.get('rules', []):
            rulemethod = rule.get('match-request', {}).get('method', None)
            if rulemethod is None:
                continue
            action = rule.get('action', 'deny')
            rulenames = rule.get('match-user', {}).get('name', [])
            rulegroups = rule.get('match-user', {}).get('group', [])
            if not isinstance(rulenames, list):
                rulenames = [rulenames]
            if not isinstance(rulegroups, list):
                rulegroups = [rulegroups]
            if rulemethod == '*' or rulemethod == method:
                if name in rulenames:
                    return 200 if action == 'allow' else 403
                for group in groups:
                    if group in rulegroups:
                        return 200 if action == 'allow' else 403
    elif os.path.exists('/etc/netd.conf'):
        showusers = set([])
        showgroups = set([])
        editusers = set([])
        editgroups = set([])
        with open('/etc/netd.conf') as netconf:
            line = netconf.readline()
            while line:
                if line.startswith('users_with_'):
                    keyname, extra = line.split('=', 1)
                    parts = extra.split(',')
                    for part in parts:
                        if '#' in part:
                            break
                        part = part.strip()
                        showusers.add(part)
                        if '_edit' in keyname:
                            editusers.add(part)
                elif line.startswith('groups_with_'):
                    keyname, extra = line.split('=')
                    parts = extra.split(',')
                    for part in parts:
                        if '#' in part:
                            break
                        part = part.strip()
                        showgroups.add(part)
                        if '_edit' in keyname:
                            editgroups.add(part)
                line = netconf.readline()
    if method == 'GET':
        theusers = showusers
        thegroups = showgroups
    else:
        theusers = editusers
        thegroups = editgroups
    if name in theusers:
        return 200
    for group in groups:
        if group in thegroups:
            return 200
    return 403

validtokens = {}

def authenticate(auth, method, webui, url):
    if not auth:
        return 401, None
    if auth.startswith('Basic '):
        # WebUI is only allowed basic auth for getting a token
        if webui and url != '/webui/token':
            return 401, None
        auth = auth.split(' ')[1]
        auth = base64.b64decode(auth)
        if not isinstance(auth, str):
            auth = auth.decode('utf-8')
        user, password = auth.split(':', 1)
        if user in credcache:
            if isinstance(password, bytes):
                cryptic = hashlib.sha256(password).digest()
            else:
                cryptic = hashlib.sha256(password.encode('utf-8')).digest()
            if credcache[user] == cryptic:
                return authorize(user, method), user
            del credcache[user]
            # invalidate cache on bad attempt to slow
            # down attacks allow pam to completely control
            # backoff
        pid = os.fork()
        if not pid:
            auth = False
            try:
                os.setuid(0)
            except Exception:
                pass
            try:
                auth = pam.authenticate(user, password)
            finally:
                os._exit(0 if auth else 1)
        auth = os.waitpid(pid, 0)[1] == 0
        if auth:
            if not isinstance(password, bytes):
                password = password.encode('utf-8')
            cryptic = hashlib.sha256(password).digest()
            credcache[user] = cryptic
            return authorize(user, method), user
    elif auth.startswith('Bearer '):
        auth = auth.split(' ')[1]
        sessinfo = validtokens.get(auth, None)
        if sessinfo:
            sessinfo['expiry'] = time.time() + 120
            user = sessinfo['user']
            return authorize(user, method), user
    return 401, None

# Grant tributary access to lldp stuff:
#  usermod -G adm,_lldpd tributary
# lldpcli to add fingerprint to description:
# configure system description 'Cumulus Linux version 4.0.0 running on Lenovo NE0152T;S2='

def get_lldp_all():
    return {
        'chassis': get_lldp_chassis(),
        'neighbors': get_lldp_neighbors(),
    }

def get_lldp_chassis():
    chassisinfo = {}
    ci = subprocess.check_output(
        ['/sbin/lldpcli', '-f', 'json0', 'show', 'chassis'])
    ci = json.loads(ci)
    chassisinfo['id'] = ci['local-chassis'][0]['chassis'][0]['id'][0]['value']
    chassisinfo['descr'] = ci[
        'local-chassis'][0]['chassis'][0]['descr'][0]['value']
    chassisinfo['name'] = ci[
        'local-chassis'][0]['chassis'][0]['name'][0]['value']
    return chassisinfo

healthbystate = {
    'OK': 'ok',
    'BAD': 'critical',
    'ABSENT': 'ok',
}
sensorunits_by_type = {
    'fan': 'RPM',
    'temp': '°C',
}

def event_to_lxca(evt, invinfo):
    newevt = {
        'eventDate': evt['timestamp'],
        'msg': 'Component {0} is experiencing the state(s): {1}'.format(evt['name'], ';'.join(evt['states'])),
        'severityText': 'Major' if evt['health'] == 'critical' else 'Informational',
        'originatorUUID': myuuid,
        'eventUUID': evt['event_uuid'],
        'systemSerialNumberText': invinfo['Serial Number']
    }
    return newevt

def log_event(evt):
    now = datetime.datetime.utcnow().replace(microsecond=0).isoformat()
    if now[-1] != 'Z':
        now += 'Z'
    evt['timestamp'] = now
    if 'event_uuid' not in evt:
        evt['event_uuid'] = str(uuid.uuid4())
    tstamp = int(time.time()) & 0xffffffff
    evt = json.dumps(evt) + '\n'
    invinfo = None
    try:
        mgrs = dbm.open('/etc/affluent/managers')
    except Exception:
        mgrs = None
    try:
        if b'mgmt' in subprocess.check_output(['ip', 'vrf', 'list']):
            devname = b'mgmt'
        else:
            devname = None
        if mgrs:
            dmgrs = mgrs
        else:
            dmgrs = {}
        for mgr in dmgrs:
            mgrinfo = json.loads(mgrs[mgr])
            if 'event_destination' in mgrinfo:
                dest = mgrinfo['event_destination']
                if not dest.startswith('https://'):
                    continue
                dest = dest.replace('https://', '')
                host, dest = dest.split('/', 1)
                dest = '/' + dest
                if mgrinfo.get('event_dialect', 'native') == 'lxca':
                    if not invinfo:
                        invinfo = get_inventory_data()['inventory'][0]['information']
                    sendevt = json.loads(evt)
                    sendevt = event_to_lxca(sendevt, invinfo)
                    sendevt = json.dumps(sendevt)
                else:
                    sendevt = evt
                certificate = mgrinfo['event_destination_certificate']
                certificate = ssl.PEM_cert_to_DER_cert(certificate)
                cli = SecureHTTPConnection(host, 443, CertVerifier(certificate).verifycert, devname, certificate)
                cli.request('POST', dest, sendevt)
                cli.getresponse().read()
    finally:
        if mgrs is not None:
            mgrs.close()
        with open('/var/log/affluent/events', 'a') as txtlogout:
            position = txtlogout.tell()
            cbl = struct.pack('>BBIIIH', 16, 1, position, len(evt), tstamp, 0)
            txtlogout.write(evt)
        with open('/var/log/affluent/events.cbl', 'ab') as binlogout:
            binlogout.write(cbl)


def read_eventlog(lxcamode=False):
    invinfo = None
    if lxcamode:
        invinfo = get_inventory_data()['inventory'][0]['information']
    yield '['
    with open('/var/log/affluent/events.cbl', 'rb') as binin:
        with open('/var/log/affluent/events', 'r') as txtin:
            cbl = binin.read(16)
            while len(cbl) == 16:
                rlen, _, seekto, sz, _, _ = struct.unpack('>BBIIIH', cbl)
                if rlen == 16:
                    txtin.seek(seekto)
                    record = txtin.read(sz)
                    if lxcamode:
                        evt = json.loads(record)
                        evt = event_to_lxca(evt, invinfo)
                        yield json.dumps(evt)
                    else:
                        if record[-1] == '\n':
                            record = record[:-1]
                        yield record
                cbl = binin.read(16)
                if len(cbl) == 16:
                    yield ','
    yield ']'


def get_health():
    health = {
        'health': 'ok',
        'sensors': [],
    }
    for sensor in get_sensor_data().get('sensors', []):
        if sensor['health'] != 'ok':
            health['health'] = sensor['health']
            health['sensors'].append(sensor)
    if glob.glob('/var/support/cl_support*'):
        if health['health'] != 'critical':
            health['health'] = 'warning'
        health['sensors'].append({
           'name': 'Support Data',
           'value': None,
           'units': '',
           'health': 'warning',
           'states': ['Generated support data present'],
        })
    return health

def ssdp_responder():
    sds = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
    vrfname = None
    if b'mgmt' in subprocess.check_output(['ip', 'vrf', 'list']):
        vrfname = b'mgmt'
    devname = b'eth0'
    ipo = subprocess.check_output(['ip', 'link', 'show', 'dev', devname])
    mgtidx = int(ipo.split(b':', 1)[0])
    sds.setsockopt(socket.SOL_SOCKET, 25, devname)
    v6grp = ssdp6mcast + struct.pack('=I', mgtidx)
    sds.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, v6grp)
    sds.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sds.bind(('', 1900))
    while True:
        try:
            (rsp, peer) = sds.recvfrom(9000)
            if not rsp.startswith(b'M-SEARCH'):
                continue
            if b'ssdp:discover' not in rsp:
                continue
            if b'service:affluent' not in rsp:
                continue
            peer = peer[:3] + (mgtidx,)
            invdata = get_inventory_data()['inventory'][0]['information']
            sn = invdata['Serial Number']
            mn = invdata['Model']
            uuid = invdata['UUID']
            pn = invdata['Product name']
            usn = 'model:{}::serial:{}::uuid:{}\r\nMODELNAME: {}'.format(mn, sn, uuid, pn)
            sds.sendto(b'HTTP/1.1 200 OK\r\nCACHE-CONTROL: max-age=1800\r\nST: urn::service:affluent:1\r\nUSN: {}\r\n'.format(usn), peer)
        except Exception:
            continue


def monitor_hardware():
    currhealth = get_health()
    prevhealthbyname = {}
    log_event({'name': 'Affluent', 'states': ['running'], 'health': 'ok'})
    for sensor in currhealth['sensors']:
        log_event(sensor)
        prevhealthbyname[sensor['name']] = {'health': sensor['health'], 'states': sensor['states']}
    while True:
        eventlet.sleep(10)  # the unit_state is refreshed every 10 secnds
        for token in list(validtokens):  # Expire tokens that have expired
            if validtokens[token]['expiry'] < time.time():
                del validtokens[token]
        try:
            currhealth = get_health()
            currnames = []
            for sensor in currhealth['sensors']:
                currname = sensor['name']
                currnames.append(currname)
                statesummary = {'health': sensor['health'], 'states': sensor['states']}
                if currname in prevhealthbyname:
                    if statesummary == prevhealthbyname[currname]:
                        continue
                log_event(sensor)
                prevhealthbyname[sensor['name']] = {'health': sensor['health'], 'states': sensor['states']}
            for sname in prevhealthbyname:
                if sname not in currnames:
                    del prevhealthbyname[sname]
                    for sensor in get_sensor_data(sname).get('sensors', []):
                        log_event(sensor)
        except Exception as e:
            sys.stderr.write(e)


def _apiify(name):
    return name.lower().replace(' ', '_')

def list_sensors():
    names = [{'href': 'all'}]
    try:
        for sensename in os.listdir('/run/cache/cumulus/unit_state'):
            fname = '/run/cache/cumulus/unit_state/{0}/dump'.format(
                sensename)
            if os.path.islink(fname) or not os.path.isfile(fname):
                continue
            loaded = False
            tries = 20
            while not loaded and tries:
                tries -= 1
                try:
                    with open(fname, 'rb') as infile:
                        # The format of the cumulus unit_state is pickle, so we must 
                        # use pickle to load it.  These files are only writable by root
                        sensedata = RestrictedUnpickler(infile).load()
                    loaded = True
                except Exception:
                    eventlet.sleep(0.2)
            dispname = sensedata.get('description', sensename)
            names.append({'href': _apiify(dispname)})
    except OSError as e:
        if e.errno != 2:
            raise
    return {'item': names}


def get_sensor_data(sensor=None):
    if sensor == 'all':
        sensor = None
    allsensors = []
    try:
        for sensename in os.listdir('/run/cache/cumulus/unit_state'):
            fname = '/run/cache/cumulus/unit_state/{0}/dump'.format(
                    sensename)
            if os.path.islink(fname) or not os.path.isfile(fname):
                continue
            loaded = False
            tries = 20
            while not loaded and tries:
                tries -= 1
                try:
                    with open(fname, 'rb') as infile:
                        # Cumulus uses pickle and we don't control that, the files
                        # are only writable by root user.
                        sensedata = RestrictedUnpickler(infile).load()
                    loaded = True
                except Exception:
                    eventlet.sleep(0.2)
            dispname = sensedata.get('description', sensename)
            if sensor and sensor not in (dispname, _apiify(dispname)):
                continue
            for incandidate in sensedata:
                if incandidate.endswith('_input'):
                    break
            else:
                incandidate = ''
            health = sensedata.get('state', None)
            senseinfo = {
                'name': dispname,
                'value': sensedata.get(incandidate, None),
                'units': sensorunits_by_type.get(sensedata.get(
                    'type', None), ''),
                'health': healthbystate.get(health, 'unknown'),
                'states': [],
            }
            states = sensedata.get('msg', None)
            if states:
                states = states.replace(sensename + ':', '')
                states = states.strip()
                senseinfo['states'] = [states]
            if health not in healthbystate:
                senseinfo['states'].append(health)
            allsensors.append(senseinfo)
            sensedata = {}
    except OSError as e:
        if e.errno != 2:
            raise
    return {'sensors': allsensors}


invdata = None


def get_inventory_data():
    global invdata
    if invdata is None:
        invdata = subprocess.check_output(
            ['/usr/cumulus/bin/decode-syseeprom', '--json'])
        invdata = json.loads(invdata)
    tlvinfo = invdata.get('tlv', {})
    mtmsn = tlvinfo.get('Service Tag', {}).get('value', 'Unknown Unknown')
    model, sn = mtmsn.split(' ', 1)
    if model == 'Unknown':
        model = tlvinfo.get('Part Number', {}).get('value', 'Unknown')
    if sn == 'Unknown':
        sn = tlvinfo.get('Serial Number', {}).get('value', 'Unknown')
    invinfo = {
        'Product name': tlvinfo.get('Product Name', {}).get('value', None),
        'Manufacturer': tlvinfo.get('Vendor Name', {}).get('value', None),
        'Model': model,
        'UUID': myuuid,
        'Serial Number': sn,
        'Board manufacture date': tlvinfo.get('Manufacture Date', {}).get('value', None),
        'Revision': tlvinfo.get('Device Version', {}).get('value', None),
    }
    invinfo = {
        'information': invinfo,
        'name': 'System',
        'present': True,
    }
    return {'inventory': [invinfo]}


def get_os_version():
    with open('/etc/os-release') as osr:
        osrdata = osr.read().split('\n')
        vername = None
        verid = None
        for line in osrdata:
            key, datum = line.split('=', 1)
            if key == 'NAME':
                vername = datum.replace('"', '')
            if key == 'VERSION_ID':
                verid = datum
            if vername and verid:
                break
        else:
            return {}
        return [{
            vername: {'version': verid},
        }]


def get_lldp_neighbors():
    result = subprocess.check_output(['/sbin/lldpcli', '-f', 'json0', 'show', 'neighbors'])
    result = json.loads(result)
    retval = []
    for res in result['lldp']:
        for interface in res:
            for neigh in res[interface]:
                peerchassis = neigh['chassis'][0]['id'][0]['value']
                peerportid = neigh['port'][0]['id'][0]['value']
                peerportdesc = neigh['port'][0].get('descr', [{}])[0].get(
                    'value', peerportid)
                myport = neigh['name']
                peername = neigh.get('chassis', [{}])[0].get('name', [{}])[0].get('value', None)
                peerdescr = neigh.get('chassis', [{}])[0].get('descr', [{}])[0].get('value', None)
                peerip = neigh.get('chassis', [{}])[0].get('mgmt-ip', [{}])[0].get('value', None)
                retval.append({
                    'localport': myport,
                    'peerportid': peerportid,
                    'peerportdescription': peerportdesc,
                    'peerchassisid': peerchassis,
                    'peerdescription': peerdescr,
                    'peerip': peerip,
                    'peername': peername,
                })
    return retval


def get_macs(filtervlan=None):
    macsbyvlan = {}
    vlanbymac = {}
    macsbyport = {}
    portbymac = {}
    fdb = subprocess.check_output(['/usr/sbin/bridge', '-j', 'fdb'])
    fdb = json.loads(fdb)
    for ent in fdb:
        if ent['state'] == 'permanent':
            continue
        if not ent.get('ifname', None):
            continue
        vlan = ent.get('vlan', None)
        if filtervlan and vlan != filtervlan:
            continue
        ifname = ent['ifname']
        mac = ent['mac']
        portbymac[mac] = {'port': ifname, 'vlan': vlan}
        if vlan:
            if vlan in macsbyvlan:
                macsbyvlan[vlan].add(mac)
            else:
                macsbyvlan[vlan] = set([mac])
            if mac in vlanbymac:
                vlanbymac[mac].add(vlan)
            else:
                vlanbymac[mac] = set([vlan])
        if ifname not in macsbyport:
            macsbyport[ifname] = [mac]
        elif mac not in macsbyport[ifname]:
            macsbyport[ifname].append(mac)
    for mac in vlanbymac:
        vlanbymac[mac] = list(vlanbymac[mac])
    for vlan in macsbyvlan:
        macsbyvlan[vlan] = list(macsbyvlan[vlan])
    return macsbyport, portbymac, vlanbymac, macsbyvlan

def get_all():
    return {
        'lldp': get_lldp_all(),
        'macsbyport': get_macs()[0],
    }

defheaders = (('Content-Type', 'application/json'),
              ('Cache-Control', 'no-store'),
              ('Pragma', 'no-cache'),
              ('X-Content-Type-Options', 'nosniff'),
              ('Content-Security-Policy', "default-src 'none'; frame-ancestors 'none'"),
              ('Strict-Transport-Security', 'max-age=86400'),
              ('X-Permitted-Cross-Domain-Policies', 'none'))


def send_sensor_reply(payload, sendit, code=200, headers=defheaders):
    if not payload['sensors']:
        sendit('404 Not found', headers)
        yield '{"error": "Not Found"}'
    sendit('{0}'.format(code), headers)
    yield json.dumps(payload, ensure_ascii=True) + '\n'

def send_reply(payload, sendit, code=200, headers=defheaders):
    sendit('{0}'.format(code), headers)
    yield json.dumps(payload, ensure_ascii=True) + '\n'


def handle_managers(url, env, start_response):
    method = env.get('REQUEST_METHOD', 'GET')
    url = url.replace('/managers', '')
    if url and url[0] == '/':
        url = url[1:]
    if method == 'GET':
        try:
            mymanagers = dbm.open('/etc/affluent/managers')
        except dbm.error:
            mymanagers = {}
        if not url:
            return send_reply([key.decode('utf8') for key in mymanagers], start_response)
        if url not in mymanagers:
            return send_reply({'error': 'Not found'}, start_response, 404)
        mydata = json.loads(mymanagers[url])
        return send_reply(mydata, start_response)
    elif method == 'DELETE':
        try:
            mymanagers = dbm.open('/etc/affluent/managers', 'w')
        except dbm.error:
            mymanagers = {}
        if not url:
            return send_reply({'error': 'Invalid Request'}, start_response)
        if url not in mymanagers:
            return send_reply({'error': 'Not found'}, start_response, 404)
        del mymanagers[url]
        mymanagers.close()
        return send_reply({'deleted': url}, start_response)
    elif method in ('PUT', 'POST'):
        clength = int(env['CONTENT_LENGTH'])
        if clength > 4096:
            return send_reply({'error': 'Request too large'}, start_response, 400)
        mymanagers = dbm.open('/etc/affluent/managers', 'c')
        if not url:
            for idx in range(32):
                if '{0}'.format(idx) not in mymanagers:
                    url = '{0}'.format(idx)
                    break
            else:
                return send_reply({'error': 'No automatic index available'}, start_response, 400)
        if method == 'POST' or url not in mymanagers:
            pendinfo = {}
        else:
            pendinfo = mymanagers[url]
        try:
            newinfo = json.load(env['wsgi.input'])
        except Exception:
            return send_reply({'error': 'Invalid request format'}, start_response, 400)
        for key in newinfo:
            pendinfo[key] = newinfo[key]
        mymanagers[url] = json.dumps(pendinfo)
        mymanagers.close()
        return send_reply({'created': url}, start_response)
    return send_reply({'error': 'Unsupported method'}, start_response, 400)


def get_ntp_list():
    servers = []
    with open('/etc/ntp.conf') as confin:
        line = confin.readline()
        while line:
            if line.startswith('server '):
                servers.append(line.split()[1])
            line = confin.readline()
    return servers

def list_ntp_servers(env, start_response):
    return send_reply(get_ntp_list(), start_response, 200)

def ntp_editable():
    with open('/etc/os-release', 'r') as osr:
        for line in osr.readlines():
            if line.startswith('VERSION_ID'):
                major = line = line.split('=', 1)[1].split('.', 1)[0]
                if int(major) >= 5:
                    return False
                break
    return True
    with open('/etc/ntp.conf', 'rb') as ntpconf:
        confdata = ntpconf.read()
    conflines = confdata.split(b'\n')
    if b'Auto-generated by NVUE' not in conflines[0]:
        return False
    if b'# md5sum: ' not in conflines[2]:
        return False
    md5s = conflines[2].split()[-1]
    actual = hashlib.md5(b'\n'.join(conflines[3:])).hexdigest()
    if not isinstance(actual, bytes):
        actual = actual.encode('utf8')
    return actual == md5s

def change_ntp_servers(env, start_response, user):
    if not ntp_editable():
        return send_reply({'error': 'NTP configuration configuration is not editable'}, start_response, 400)
    ntpservers = get_ntp_list()
    try:
        newinfo = json.load(env['wsgi.input'])
    except Exception:
        return send_reply({'error': 'Invalid request format'}, start_response, 400)
    adds = []
    dels = []
    for srv in newinfo:
        if srv not in ntpservers:
            adds.append(srv)
    for srv in ntpservers:
        if srv not in newinfo:
            dels.append(srv)
    targuid = pwd.getpwnam(user).pw_uid
    pid = os.fork()
    if pid:
        rc = os.waitpid(pid, 0)
    else:
        try:
            pass
        finally:
            os.setuid(targuid)
            for srv in adds:
                run_nclu_cmd(['/usr/bin/net', 'add', 'time', 'ntp', 'server', srv])
            for srv in dels:
                run_nclu_cmd(['/usr/bin/net', 'del', 'time', 'ntp', 'server', srv])
            run_nclu_cmd(['/usr/bin/net', 'commit'])
            os._exit(0)

def run_nclu_cmd(cmdv):
    mysock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    mysock.connect('/run/nclu/uds')
    mysock.sendall(json.dumps(cmdv))
    retval = mysock.recv(8192)
    mysock.close()
    return retval

def handle_ntp(url, env, start_response, user):
    if url[-1] == '/':
        url = url[:-1]
    method = env.get('REQUEST_METHOD', 'GET')
    if url == '/configuration/ntp/servers':
        if method in ('POST', 'PUT'):
            change_ntp_servers(env, start_response, user)
        return list_ntp_servers(env, start_response)

def serve(env, start_response):
    url = env.get('PATH_INFO', '')
    try:
        webui = bool(env.get('HTTP_REFERER', None))
        auth, user = authenticate(env.get('HTTP_AUTHORIZATION', None), env.get('REQUEST_METHOD', 'GET'), webui, url)
    except Exception:
        auth = None
        user = None
    if auth != 200:
        return send_reply({}, start_response, auth)
    if url == '/health':
        return send_reply(get_health(), start_response)
    elif url.startswith('/configuration/ntp'):
        return handle_ntp(url, env, start_response, user)
    elif url in ('/inventory/firmware', '/inventory/hardware'):
        return send_reply(['all'], start_response)
    elif url == '/inventory/firmware/all':
        return send_reply(get_os_version(), start_response)
    elif url == '/inventory/hardware/all':
        return send_reply(get_inventory_data(), start_response)
    elif url == '/sensors/hardware/all/':
        return send_reply(list_sensors(), start_response)
    elif url.startswith('/sensors/hardware/all/'):
        return send_sensor_reply(get_sensor_data(url.split('/')[-1]), start_response)
    elif url == '/macs/by-port':
        return send_reply(get_macs()[0], start_response)
    elif url == '/macs/by-mac':
        return send_reply(get_macs()[1], start_response)
    elif url == '/macs/by-vlan':
        return send_reply(get_macs()[3], start_response)
    elif url.startswith('/macs/by-mac/'):
        mac = url.replace('/macs/by-mac/', '').replace('-', ':').lower()
        port = get_macs()[1].get(mac, None)
        if not port:
            return send_reply({'error': 'Mac Not Found', 'mac': mac}, start_response, 404)
        port['mac'] = mac
        return send_reply(port, start_response)
    elif url == '/lldp/neighbors':
        return send_reply(get_lldp_neighbors(), start_response)
    elif url == '/lldp/chassis':
        return send_reply(get_lldp_chassis(), start_response)
    elif url == '/lldp/all':
        return send_reply(get_lldp_all(), start_response)
    elif url == '/allnetinfo':
        return send_reply(get_all(), start_response)
    elif url.startswith('/managers'):
        return handle_managers(url, env, start_response)
    elif url == '/':
        return send_reply(['health' , 'lldp', 'inventory', 'sensors', 'macs', 'allnetinfo', 'managers'], start_response)
    elif url == '/inventory':
        return send_reply(['firmware', 'hardware'], start_response)
    elif url == '/interfaces/all':
        return send_reply(get_interfaces(), start_response)
    elif url == '/macs':
        return send_reply(['by-mac', 'by-port', 'by-vlan'], start_response)
    elif url == '/lldp':
        return send_reply(['all', 'chassis', 'neighbors'], start_response)
    elif url == '/events/log/xclarity/all':
        start_response('200 OK', defheaders)
        return yieldit(read_eventlog(lxcamode=True))
    elif url == '/events/log/all':
        start_response('200 OK', defheaders)
        return yieldit(read_eventlog())
    elif url == '/webui/token':
        start_response('200 OK', defheaders)
        return yieldit(gentoken(user))
    return send_reply({'error': 'Unrecognized request'}, start_response, 404)

def gentoken(user):
    token = base64.b64encode(os.urandom(18))
    validtokens[token] = {
        'user': user,
        'expiry': time.time() + 120,
    }
    yield token

def yieldit(gen):
    for datum in gen:
        yield datum

def generate_uuid():
    invdata = subprocess.check_output(['/usr/cumulus/bin/decode-syseeprom', '--json'])
    invdata = json.loads(invdata)
    mac = invdata.get('tlv', {}).get('Base MAC Address', {}).get('value', None)
    if mac:
        if not isinstance(mac, bytes):
            mac = mac.encode('utf8')
        return str(uuid.uuid5(uuid.UUID('a311972f-bd38-4c8a-8792-5975405170ad'), mac))
    return str(uuid.uuid4())

def main(args):
    global myuuid
    if 'gencert' in args:
        create_certificate()
        sys.exit(0)
    myuuid = None
    try:
        with open('/etc/affluent/uuid') as uuidin:
            myuuid = uuidin.read()
    except Exception:
        pass
    if not myuuid:
        tmphdl, uuidfile = tempfile.mkstemp()
        os.close(tmphdl)
        pid = os.fork()
        if pid:
            os.waitpid(pid, 0)
            with open(uuidfile, 'r') as uuidin:
                myuuid = uuidin.read()
            os.remove(uuidfile)
            with open('/etc/affluent/uuid', 'w') as uuidout:
                uuidout.write(myuuid)
        else:
            try:
                myuid = os.getuid()
                mygid = os.getgid()
                os.setuid(0)
                sysuuid = '00000000-0000-0000-0000-000000000000'
                try:
                    with open('/sys/devices/virtual/dmi/id/product_uuid') as uuidin:
                        sysuuid = uuidin.read().strip()
                except Exception:
                    sysuuid = '00000000-0000-0000-0000-000000000000'
                if sysuuid in bad_uuids:
                    sysuuid = generate_uuid()
                os.chown(uuidfile, 0, 0)
                with open(uuidfile, 'w') as uuidout:
                    uuidout.write(sysuuid)
                os.chown(uuidfile, myuid, mygid)
            finally:
                os._exit(0)
    if not exists('/etc/affluent/cert.pem'):
        pid = os.fork()
        if pid:
            os.waitpid(pid, 0)
        else:
            try:
                os.setuid(0)
                create_certificate()
                #This should be handled by packaging instead
                #subprocess.check_call(['systemctl', 'restart', 'nginx'])
            finally:
                os._exit(0)
    try:
        os.remove('/run/affluent/httpapi')
    except OSError as oe:
        if oe.errno != 2:
            raise
    eventlet.spawn(monitor_hardware)
    eventlet.spawn(ssdp_responder)
    sock = eventlet.listen('/run/affluent/httpapi', socket.AF_UNIX)
    # This is world writable to make it easy for reverse proxy
    # This is instead of a TCP socket, which would be freely accessible
    # anyway.  Client connections to our socket are treated as not particularly
    # trusted
    os.chmod('/run/affluent/httpapi', 0o666)  # nosec
    eventlet.wsgi.server(sock, serve, log=False, log_output=False,
                         debug=False, socket_timeout=60)

if __name__ == '__main__':
    main(sys.argv)
