#
# Copyright (C) 2017,  Netronome Systems, Inc.  All rights reserved.
#

import os, sys, struct, pprint, threading, json
from urlparse import urlparse
from contextlib import contextmanager

import grpc

from RTERPCInterface import (DesignBase, CountersBase, TablesBase, 
    ParserValueSetsBase, RegistersBase, MetersBase, TrafficClassBase, 
    DigestsBase, MulticastBase, DebugCtlBase, SystemBase)

from sdk6_rte_grpc import netro_runtime_pb2, netro_runtime_pb2_grpc
from sdk6_rte_grpc import p4runtime_pb2, p4runtime_pb2_grpc
from p4.config import p4info_pb2

import RTERPCInterface

# XXX remaining issues:
# XXX * master arbitration via p4runtime.StreamChannel not implemented    
# XXX * globally unique ids, we do not have the compiler assigned ids in bmv2
# XXX   * will rte assign?
# XXX   * how does this map to published ids?
# XXX * rules (TableEntry) do not have names, required by user configs, inspectors
# XXX   * other missing rule fields: default_rule, timeout
# XXX * missing counter field: width
# XXX * meter config fields mismitch:
# XXX   * cir, cburst, pir, pburst vs rate_k, burst_k, array_offset, count
# XXX * digests not available in p4runtime.proto, use p4.config.v1model ?
# XXX * registers not available in p4runtime.proto, implemented via Externs?
# XXX * parser value sets, multicast, traffic class not available, implemented via Externs?
# XXX * rte thrift interface has nothing equivalent to p4info.actions

COUNTER_CLASS_MAP = {
    p4info_pb2.CounterSpec.UNSPECIFIED: RTERPCInterface.P4CounterClass.Unspecified,
    p4info_pb2.CounterSpec.PACKETS: RTERPCInterface.P4CounterClass.Packets,
    p4info_pb2.CounterSpec.BYTES: RTERPCInterface.P4CounterClass.Bytes,
    p4info_pb2.CounterSpec.BOTH: RTERPCInterface.P4CounterClass.Both,
}

METER_CLASS_MAP = {
    p4info_pb2.MeterSpec.UNSPECIFIED: RTERPCInterface.MeterClass.Unspecified,
    p4info_pb2.MeterSpec.PACKETS: RTERPCInterface.MeterClass.Packets,
    p4info_pb2.MeterSpec.BYTES: RTERPCInterface.MeterClass.Bytes,
} 

MATCH_TYPE_MAP = {
    p4info_pb2.MatchField.UNSPECIFIED: RTERPCInterface.MatchType.Unspecified,
    p4info_pb2.MatchField.VALID: RTERPCInterface.MatchType.Valid,
    p4info_pb2.MatchField.EXACT: RTERPCInterface.MatchType.Exact,
    p4info_pb2.MatchField.LPM: RTERPCInterface.MatchType.LPM,
    p4info_pb2.MatchField.TERNARY: RTERPCInterface.MatchType.Ternary,
    p4info_pb2.MatchField.RANGE: RTERPCInterface.MatchType.Range,
}

def HandleRteReturn(rte_ret):
    if rte_ret.value != netro_runtime_pb2.RteReturn.SUCCESS:
        #error = err_msg
        #if err_msg is None:
        #    error = 'Error in %s'%func.func_name
        error = 'RTE error'
        reason = ''
        if rte_ret.reason:
            reason = ": %s.\nPlease see RTE log for more info"%rte_ret.reason
        raise RTERPCInterface.RTEReturnError, '%s: %s%s'%(RTERPCInterface.RTE_RETURN_CODES[rte_ret.value], error, reason)
    

# decorator to transform non SUCCESS RteReturn values to exceptions
def RteReturnHandler(err_msg=None):
    def _RteReturnHandler(func):
        def __RteReturnHandler(*args, **kwargs):
            try:
                rte_ret = func(*args, **kwargs)
            except grpc.RpcError, err:
                raise RTERPCInterface.RTECommError, "Communication failure with RPC server: %s"%str(err)
            else:
                HandleRteReturn(rte_ret)
        return __RteReturnHandler
    return _RteReturnHandler

        
# decorator to catch grpc communication failures
def RPC(func):
    def _RPC(self, *args, **kwargs):
        with self.rte.GRPC_API_LOCK:
            try:
                return func(self, *args, **kwargs)
            except grpc.RpcError, err:
                raise RTERPCInterface.RTECommError, "Communication failure with RPC server: %s"%str(err)
    return _RPC

def EncodeBytes(v, fmt):
    if fmt == p4runtime_pb2.RAW:
        return ''.join([chr(int(c)) for c in v])
    elif fmt == p4runtime_pb2.AUTO:
        return v

def DecodeBytes(v, fmt):
    if fmt == p4runtime_pb2.RAW:
        return ''.join([str(ord(c)) for c in v])
    elif fmt == p4runtime_pb2.AUTO:
        return v
    
class Design(DesignBase):
    def GetDesignCommandRequest(self, device_id=0, election_id=0):
        req = netro_runtime_pb2.DesignCommandRequest()
        req.device_id = device_id
        req.election_id.Clear()
        return req

    def RefreshP4Schema(self):
        req = p4runtime_pb2.GetForwardingPipelineConfigRequest(device_ids=[self.rte.device_id])
        self.rte.p4info = self.rte.p4_rt.GetForwardingPipelineConfig(req).configs[0].p4info
        
        self.rte._direct_counters = set()
        self.rte._counters_by_id = {}
        for cntr in self.rte.p4info.direct_counters:
            self.rte._direct_counters.add(cntr.preamble.id)
            self.rte._counters_by_id[cntr.preamble.id] = cntr
        for cntr in self.rte.p4info.counters:
            self.rte._counters_by_id[cntr.preamble.id] = cntr
    
        self.rte._direct_meters = set()
        self.rte._meters_by_id = {}
        for mtr in self.rte.p4info.direct_meters:
            self.rte._direct_meters.add(mtr.preamble.id)
            self.rte._meters_by_id[mtr.preamble.id] = mtr
        for mtr in self.rte.p4info.meters:
            self.rte._meters_by_id[mtr.preamble.id] = mtr
    
        self.rte._tables_by_id = {}
        for tbl in self.rte.p4info.tables:
            self.rte._tables_by_id[tbl.preamble.id] = tbl
    
        self.rte._actions_by_id = {}
        self.rte._actions_by_name = {}
        for act in self.rte.p4info.actions:
            self.rte._actions_by_id[act.preamble.id] = act
            self.rte._actions_by_name[act.preamble.name] = act
        

    @RPC
    def Load(self, elf_fw, pif_design, pif_config):
        with open(elf_fw, "rb") as f:
            elf_fw_data = f.read()

        pif_design_data = ""
        if pif_design:
            with open(pif_design, "rb") as f:
                pif_design_data = f.read()

        pif_config_data = ""
        if pif_config:
            with open(pif_config, "rb") as f:
                pif_config_data = f.read()

        self.Unload()

        req = self.GetDesignCommandRequest()
        req.command.design_load.nfpfw = elf_fw_data
        req.command.design_load.pif_design_json = pif_design_data
        req.command.design_load.pif_config_json = pif_config_data
        HandleRteReturn(self.rte.netro_rt.DesignCommand(req).rte_return)

        self.RefreshP4Schema()

    @RPC
    def Unload(self):
        req = self.GetDesignCommandRequest()
        req.command.design_unload.Clear()
        HandleRteReturn(self.rte.netro_rt.DesignCommand(req).rte_return)
        self.rte.p4info = None

    @RPC
    def ConfigReload(self, pif_config):
        req = self.GetDesignCommandRequest()
        with open(pif_config, "rb") as f:
            req.command.design_reload.pif_config_json = f.read()
        HandleRteReturn(self.rte.netro_rt.DesignCommand(req).rte_return)

    @RPC
    def LoadStatus(self):
        req = self.GetDesignCommandRequest()
        req.command.design_status.Clear()
        resp = self.rte.netro_rt.DesignCommand(req)
        HandleRteReturn(resp.rte_return)
        if resp.HasField('load_status'):
            return {
                'is_loaded': resp.load_status.is_loaded,
                'uuid': resp.load_status.uuid, 
                'frontend_build_date': resp.load_status.frontend_build_date,
                'frontend_source': resp.load_status.frontend_source,
                'frontend_version': resp.load_status.frontend_version,
                'uptime': resp.load_status.uptime,
            }
        else:
            return {
                'is_loaded': False,
                'uuid': '', 
                'frontend_build_date': '',
                'frontend_source': '',
                'frontend_version': '',
                'uptime': 0,
            }
            

class Counters(CountersBase):
    def ExtractRteValue(self, rv):
        return rv.intval if rv.type == RteValueType.Int64 else int(rv.stringval)

    def GetWriteRequest(self, device_id=0, election_id=0):
        req = p4runtime_pb2.WriteRequest()
        req.device_id = device_id
        req.election_id.Clear()
        return req

    def GetReadRequest(self, device_id=0):
        req = p4runtime_pb2.ReadRequest()
        req.device_id = device_id
        return req

    @RPC
    def ListP4Counters(self):
        cntrs = []
        for cntr in self.rte.p4info.counters:
            cntrs.append({
                'count': cntr.size,
                'name': cntr.preamble.name,
                'width': 0,
                'tableid': 0,
                'table': '',
                'type': RTERPCInterface.P4CounterType.Global,
                'id': cntr.preamble.id,
            })
        for cntr in self.rte.p4info.direct_counters:
            cntrs.append({
                'count': 1,
                'name': cntr.preamble.name,
                'width': 0,
                'tableid': cntr.direct_table_id,
                'table': self.rte._tables_by_id[cntr.direct_table_id].preamble.name,
                'type': RTERPCInterface.P4CounterType.Direct,
                'id': cntr.preamble.id,
            })
        return cntrs
    
    @RPC
    def GetP4Counter(self, counter):
        counter_id = self.ResolveToCounterId(counter)

        direct = counter_id in self.rte._direct_counters
        cntr_info = self.rte._direct_by_id[counter_id]

        req = self.GetReadRequest()
        cntr = req.entities.add()
        if direct:
            cntr.direct_counter_entry.counter_id = counter_id
        else:
            cntr.counter_entry.counter_id = counter_id
            cntr.counter_entry.index = 0 # return all
        
        result = []
        for resp in self.rte.p4_rt.Read(req):
            for ent in resp.entities:
                if direct:
                    if cntr_info.spec.unit in (p4info_pb2.CounterSpec.PACKETS, p4info_pb2.CounterSpec.BOTH):
                        result.append(ent.direct_counter_entry.data.packet_count)
                    if cntr_info.spec.unit in (p4info_pb2.CounterSpec.BYTES, p4info_pb2.CounterSpec.BOTH):
                        result.append(ent.direct_counter_entry.data.bytes_count)
                else:
                    if cntr_info.spec.unit in (p4info_pb2.CounterSpec.PACKETS, p4info_pb2.CounterSpec.BOTH):
                        result.append(ent.counter_entry.data.packet_count)
                    if cntr_info.spec.unit in (p4info_pb2.CounterSpec.BYTES, p4info_pb2.CounterSpec.BOTH):
                        result.append(ent.counter_entry.data.bytes_count)
        return result

    @RPC
    def ClearP4Counter(self, counter):
        counter_id = self.ResolveToCounterId(counter)

        req = self.GetWriteRequest()
        update = req.updates.add()
        update.type = p4runtime_pb2.Update.DELETE
        if counter_id in self.rte._direct_counters:
            update.entity.direct_counter_entry.counter_id = counter_id
        else:
            update.entity.counter_entry.counter_id = counter_id
        self.rte.p4_rt.Write(req)

    @RPC
    def ClearAllP4Counters(self):
        req = self.GetWriteRequest()
        update = req.updates.add()
        update.type = p4runtime_pb2.Update.DELETE
        update.entity.counter_entry.counter_id = 0
        update = req.updates.add()
        update.type = p4runtime_pb2.Update.DELETE
        update.entity.direct_counter_entry.counter_id = 0
        self.rte.p4_rt.Write(req)

    @RPC
    def GetSystemCounters(self):
        req = self.rte.System.GetSystemInfoReadRequest()
        info = req.info.add()
        info.system_cntrs.Clear()
        return [{
            'name': sc.name,
            'value': sc.int_count,
            'id': sc.id,
        } for sc in self.rte.netro_rt.ReadSystemInfo(req).info[0].system_cntrs.system_cntrs]

    @RPC
    def ClearAllSysCounters(self):
        req = self.rte.System.GetSystemCommandRequest()
        req.command.clear_system_cntrs.Clear()
        HandleRteReturn(self.rte.netro_rt.SendSystemCommand(req).rte_return)

class Tables(TablesBase):
    def UpdateRule(self, update_type, tbl_id, rule_name, default_rule, match, actions, priority = None, timeout = None):
        tbl_id = self.ResolveToTableId(tbl_id)

        acts = json.loads(actions) if actions else {}
        matches = json.loads(match) if match else {}

        req = p4runtime_pb2.WriteRequest()
        req.device_id = self.rte.device_id
        req.election_id.Clear()

        update = req.updates.add()
        update.type = update_type

        
        update.entity.table_entry.table_id = tbl_id
        if priority is not None:
            update.entity.table_entry.priority = priority
        
        if 'type' in acts:
            act_info = self.rte._actions_by_name[acts['type']]
            update.entity.table_entry.action.action.action_id = act_info.preamble.id
            if 'data' in acts:
                for param_info in act_info.params:
                    if param_info.name in acts['data']:
                        p = update.entity.table_entry.action.action.params.add()
                        p.param_id = param_info.id
                        p.format = p4runtime_pb2.AUTO
                        p.value = EncodeBytes(str(acts['data'][param_info.name]['value']), p4runtime_pb2.AUTO)
        
        if matches:
            for (fld, match_vals), fld_info in zip(matches.items(), self.rte._tables_by_id[tbl_id].match_fields):
                match = update.entity.table_entry.match.add()
                match.field_id = fld_info.id
                match.format = p4runtime_pb2.AUTO
                mtype = MATCH_TYPE_MAP[fld_info.match_type]
                if mtype == RTERPCInterface.MatchType.Exact:
                    match.exact.value = EncodeBytes(str(match_vals['value']), p4runtime_pb2.AUTO)
                elif mtype == RTERPCInterface.MatchType.LPM:
                    match.lpm.value = EncodeBytes(str(match_vals['value']), p4runtime_pb2.AUTO)
                    if 'prefix' in match_vals:
                        match.lpm.prefix_len = str(match_vals['prefix'])
                elif mtype == RTERPCInterface.MatchType.Ternary:
                    match.ternary.value = EncodeBytes(str(match_vals['value']), p4runtime_pb2.AUTO)
                    if 'mask' in match_vals:
                        match.ternary.mask = EncodeBytes(str(match_vals['mask']), p4runtime_pb2.AUTO)
                elif mtype == RTERPCInterface.MatchType.Range:
                    match.range.low = EncodeBytes(str(match_vals['value']), p4runtime_pb2.AUTO)
                    if 'rangeto' in match_vals:
                        match.range.high = EncodeBytes(str(match_vals['rangeto']), p4runtime_pb2.AUTO)
                elif mtype == RTERPCInterface.MatchType.Valid:
                    match.valid.value = match_vals['value'] == 'valid'
            
        self.rte.p4_rt.Write(req)

    @RPC
    def AddRule(self, tbl_id, rule_name, default_rule, match, actions, priority = None, timeout = None):
        self.UpdateRule(p4runtime_pb2.Update.INSERT, tbl_id, rule_name, default_rule, match, actions, priority, timeout)

    @RPC
    def EditRule(self, tbl_id, rule_name, default_rule, match, actions, priority = None, timeout = None):
        self.UpdateRule(p4runtime_pb2.Update.MODIFY, tbl_id, rule_name, default_rule, match, actions, priority, timeout)

    @RPC
    def DeleteRule(self, tbl_id, rule_name, default_rule, match, actions):
        self.UpdateRule(p4runtime_pb2.Update.DELETE, tbl_id, rule_name, default_rule, match, actions)

    @RPC
    def List(self):
        return [{
            'tbl_name': td.preamble.name,
            'tbl_id': td.preamble.id,
            'support_timeout': td.with_entry_timeout,
            'tbl_entries_max': td.size,
        } for td in self.rte.p4info.tables]

    @RPC
    def ListRules(self, tbl_id):
        tbl_id = self.ResolveToTableId(tbl_id)
        req = p4runtime_pb2.ReadRequest()
        req.device_id = self.rte.device_id
        req.format = p4runtime_pb2.AUTO
        tble = req.entities.add()
        tble.table_entry.table_id = tbl_id
        rules = []
        for resp in self.rte.p4_rt.Read(req):
            for ent in resp.entities:
                assert ent.HasField('table_entry')

                act = self.rte._actions_by_id[ent.table_entry.action.action.action_id]
                data = {}
                for param_info, param_value in zip(act.params, ent.table_entry.action.action.params):
                    data[param_info.name] = {'value': DecodeBytes(param_value.value, param_value.format)}
                
                actions = {'type': act.preamble.name, 'data': data}
                matches = {}
                for match, match_info in zip(ent.table_entry.match, self.rte._tables_by_id[ent.table_entry.table_id].match_fields):
                    matches[match_info.name] = vals = {}
                    if match_info.match_type == p4info_pb2.MatchField.EXACT and match.HasField('exact'):
                        vals['value'] = DecodeBytes(match.exact.value, match.format)
                    elif match_info.match_type == p4info_pb2.MatchField.TERNARY and match.HasField('ternary'):
                        vals['value'] = DecodeBytes(match.ternary.value, match.format)
                        vals['mask'] = DecodeBytes(match.ternary.mask, match.format)
                    elif match_info.match_type == p4info_pb2.MatchField.LPM and match.HasField('lpm'):
                        vals['value'] = DecodeBytes(match.lpm.value, match.format)
                        vals['prefix'] = match.lpm.prefix_len
                    elif match_info.match_type == p4info_pb2.MatchField.RANGE and match.HasField('range'):
                        vals['value'] = DecodeBytes(match.range.low, match.format)
                        vals['rangeto'] = DecodeBytes(match.range.high, match.format)
                    elif match_info.match_type == p4info_pb2.MatchField.VALID and match.HasField('valid'):
                        vals['value'] = 'valid' if match.valid.value else 'invalid'
                
                rules.append({
                    'timeout_seconds': -1,
                    'actions': json.dumps(actions),
                    'priority': ent.table_entry.priority,
                    'rule_name': '???', # XXX p4runtime TableEntry does not have a name
                    'default_rule': '???', # XXX use p4info.tables[X].const_default_action_id
                    'match': json.dumps(matches),
                })
        return rules

    @RPC
    def GetVersion(self):
        raise RTERPCInterface.RTERPCNotImplemented

class Registers(RegistersBase):
    @RPC
    def List(self):
        raise RTERPCInterface.RTERPCNotImplemented

    def ResolveToRegisterArrayArg(self, register, index, count):
        raise RTERPCInterface.RTERPCNotImplemented

    @RPC
    def Get(self, register, index=0, count=1):
        raise RTERPCInterface.RTERPCNotImplemented

    @RPC
    def Clear(self, register, index=0, count=1):
        raise RTERPCInterface.RTERPCNotImplemented

    @RPC
    def Set(self, register, values, index=0, count=1):
        raise RTERPCInterface.RTERPCNotImplemented

    @RPC
    def SetField(self, register, field_id, value, index=0, count=1):
        raise RTERPCInterface.RTERPCNotImplemented

class TrafficClass(TrafficClassBase):
    @RPC
    def Get(self, port_id):
        req = netro_runtime_pb2.SystemInfoReadRequest()
        req.device_id = self.rte.device_id
        info = req.info.add()
        info.traffic_class_list.port_id = port_id

        res = self.rte.netro_rt.ReadSystemInfo(req).info[0].traffic_class_list.traffic_class

        return [{
            'class_id': tcc.class_id,
            'weight': tcc.weight,
            'queue_no': tcc.queue_no,
            'committed': tcc.committed, 
        } for tcc in res]

    @RPC
    def Set(self, port_id, cfgs):
        req = netro_runtime_pb2.SystemCommandRequest()
        req.device_id = self.rte.device_id
        req.election_id.Clear()
        req.command.set_traffic_class.port_id = port_id
        for cfg in cfgs:
            tccfg = req.command.set_traffic_class.traffic_class_cfgs.add()
            tccfg.class_id = cfg['class_id']
            tccfg.weight = cfg['weight']
            tccfg.queue_no = cfg['queue_no']
        HandleRteReturn(self.rte.netro_rt.SendSystemCommand(req).rte_return)

    @RPC
    def Commit(self, port_id):
        req = netro_runtime_pb2.SystemCommandRequest()
        req.device_id = self.rte.device_id
        req.election_id.Clear()
        req.command.commit_traffic_class.port_id = port_id
        HandleRteReturn(self.rte.netro_rt.SendSystemCommand(req).rte_return)

class Meters(MetersBase):
    def GetReadRequest(self, device_id=0):
        req = p4runtime_pb2.ReadRequest()
        req.device_id = device_id
        return req

    def GetWriteRequest(self, device_id=0, election_id=0):
        req = p4runtime_pb2.WriteRequest()
        req.device_id = device_id
        req.election_id.Clear()
        return req

    @RPC
    def List(self):
        mtrs = []
        for mtr in self.rte.p4info.meters:
            mtrs.append({
                'count': mtr.size,
                'name': mtr.preamble.name,
                'width': 0,
                'tableid': 0,
                'table': '',
                'mclass': RTEGRPCInterface.METER_CLASS_MAP[mtr.spec.unit],
                'type': RTERPCInterface.MeterType.Global,
                'id': mtr.preamble.id,
            })
        for mtr in self.rte.p4info.direct_meters:
            mtrs.append({
                'count': 1,
                'name': mtr.preamble.name,
                'width': 0,
                'tableid': mtr.direct_table_id,
                'table': self.rte._tables_by_id[mtr.direct_table_id].preamble.name,
                'mclass': RTEGRPCInterface.METER_CLASS_MAP[mtr.spec.unit],
                'type': RTERPCInterface.MeterType.Direct,
                'id': mtr.preamble.id,
            })
        return mtrs

    @RPC
    def GetConfig(self, meter_id):
        req = self.GetReadRequest()
        direct = meter_id in self.rte._direct_meters
        mtr = req.entities.add()
        if direct:
            mtr.direct_meter_entry.meter_id = meter_id
        else:
            mtr.meter_entry.meter_id = meter_id
            mtr.meter_entry.index = 0 # return all
        
        mtrcfgs = []
        for resp in self.rte.p4_rt.Read(req):
            for ent in resp.entities:
                if direct:
                    mtrcfgs.append({
                        'rate_k': ent.direct_meter_entry.config.cir,
                        'burst_k': ent.direct_meter_entry.config.cburst,
                        'array_offset': 0, # XXX ? .pir
                        'count': 0, # XXX ? .pburst
                    })
                else:
                    mtrcfgs.append({
                        'rate_k': ent.meter_entry.config.cir,
                        'burst_k': ent.meter_entry.config.cburst,
                        'array_offset': 0, # XXX ? .pir
                        'count': 0, # XXX ? .pburst
                    })
        return mtrcfgs

    @RPC
    def SetConfig(self, meter_id, configs):
        for idx, mtrcfg in enumerate(configs):
            req = self.GetWriteRequest()
            update = req.updates.add()
            update.type = p4runtime_pb2.Update.MODIFY
            update.meter_entry.config.cir = mtrcfg['rate']
            update.meter_entry.config.cburst = mtrcfg['burst']
            # XXX mtrcfg['off'], mtrcfg['cnt'] not handled
            self.rte.p4_rt.Write(req)

class Digests(DigestsBase):
    @RPC
    def List(self):
        raise RTERPCInterface.RTERPCNotImplemented

    @RPC
    def Register(self, digest_id):
        raise RTERPCInterface.RTERPCNotImplemented

    @RPC
    def Deregister(self, digest_regid):
        raise RTERPCInterface.RTERPCNotImplemented

    @RPC
    def Get(self, digest_handle):
        raise RTERPCInterface.RTERPCNotImplemented

class Multicast(MulticastBase):
    @RPC
    def List(self):
        req = netro_runtime_pb2.SystemInfoReadRequest()
        req.device_id = self.rte.device_id
        info = req.info.add()
        info.mcast_cfg_list.Clear()

        res = self.rte.netro_rt.ReadSystemInfo(req).info[0].mcast_cfg_list.mcast_list

        return [{
            'group_id': mcce.group_id,
            'max_entries': mcce.max_entries,
            'ports': [p for p in mcce.ports],
        } for mcce in res]

    @RPC
    def SetConfig(self, group_id, ports):
        req = netro_runtime_pb2.SystemCommandRequest()
        req.device_id = self.rte.device_id
        req.election_id.Clear()
        req.command.mcast_cfg_set.mcast_entry.group_id = group_id
        for port in ports:
            req.command.mcast_cfg_set.mcast_entry.ports.append(port)
        HandleRteReturn(self.rte.netro_rt.SendSystemCommand(req).rte_return)

class System(SystemBase):
    def GetSystemCommandRequest(self):
        req = netro_runtime_pb2.SystemCommandRequest()
        req.device_id = self.rte.device_id
        req.election_id.Clear()
        return req

    def GetSystemInfoReadRequest(self):
        req = netro_runtime_pb2.SystemInfoReadRequest()
        req.device_id = self.rte.device_id
        return req

    @RPC
    def Shutdown(self):
        req = self.GetSystemCommandRequest()
        cmd = req.command.shutdown_cmd.Clear()
        HandleRteReturn(self.rte.netro_rt.SendSystemCommand(req).rte_return)

    @RPC
    def Ping(self):
        req = self.GetSystemCommandRequest()
        cmd = req.command.ping.Clear()
        HandleRteReturn(self.rte.netro_rt.SendSystemCommand(req).rte_return)

    @RPC
    def Echo(self, echo_msg):
        req = self.GetSystemCommandRequest()
        cmd = req.command.echo.message = echo_msg
        HandleRteReturn(self.rte.netro_rt.SendSystemCommand(req).rte_return)

    @RPC
    def GetVersion(self):
        req = self.GetSystemInfoReadRequest()
        info = req.info.add()
        info.version_info.version = ''
        return self.rte.netro_rt.ReadSystemInfo(req).info[0].version_info.version

    @RPC
    def GetLogLevel(self):
        req = self.GetSystemInfoReadRequest()
        info = req.info.add()
        info.log_level.log_level = netro_runtime_pb2.GetLogLevel.UNKNOWN

        return self.rte.netro_rt.ReadSystemInfo(req).info[0].log_level.log_level

    @RPC
    def SetLogLevel(self, level):
        req = self.GetSystemCommandRequest()
        cmd = req.command.set_log_level.log_level = level
        HandleRteReturn(self.rte.netro_rt.SendSystemCommand(req).rte_return)

    @RPC
    def GetPortInfo(self):
        req = self.GetSystemInfoReadRequest()
        info = req.info.add()
        info.ports_info.Clear()
        ports_info = self.rte.netro_rt.ReadSystemInfo(req).info[0].ports_info
        return [{
            'id': pi.id,
            'info': pi.info,
            'token': pi.token,
        } for pi in ports_info.ports]

class DebugCtl(DebugCtlBase):
    def Execute(self, debug_id, debug_data):
        req = netro_runtime_pb2.DebugControlRequest()
        req.device_id = self.rte.device_id
        req.election_id.Clear()
        req.debug_id = debug_id
        req.debug_data = debug_data
        resp = self.rte.netro_rt.DebugControl(req)
        if resp.return_value != netro_runtime_pb2.DebugControlResponse.SUCCESS:
            raise RTERPCInterface.RTEReturnError, 'error during DebugControlRequest'
        return resp.return_data
    
class ParserValueSets(ParserValueSetsBase):
    @RPC
    def List(self):
        raise RTERPCInterface.RTERPCNotImplemented

    @RPC
    def Clear(self, pvs_id):
        raise RTERPCInterface.RTERPCNotImplemented

    @RPC
    def Add(self, pvs_id, pvs_entries):
        raise RTERPCInterface.RTERPCNotImplemented

    @RPC
    def Retrieve(self, pvs_id):
        raise RTERPCInterface.RTERPCNotImplemented


def DoConnect(conn, host, port, device_id=0, use_zlib=True, serialise_api=False):
    conn.channel = grpc.insecure_channel('%s:%d'%(host, port))
    conn.netro_rt = netro_runtime_pb2_grpc.NetroRuntimeStub(conn.channel)
    conn.p4_rt = p4runtime_pb2_grpc.P4RuntimeStub(conn.channel)
    
    conn.device_id = device_id

    conn.GRPC_API_LOCK = threading.Lock() if serialise_api else RTERPCInterface.NullCtx()
    
def DoDisconnect(conn):
    pass
