#! /usr/bin/python3
import enum
import ipaddress
import os
import pdb
import random
import socket
import time
import uuid
import yaml

from collections import OrderedDict
from scapy.layers.l2 import Dot1Q

import device_pb2 as device_pb2
import types_pb2 as types_pb2
import tunnel_pb2 as tunnel_pb2
import upgrade_pb2 as upgrade_pb2
import infra.common.defs as defs
import infra.common.parser as parser
import apollo.config.agent.api as api
from apollo.config.agent.api import ObjectTypes as ObjectTypes
import apollo.config.topo as topo
from infra.common.logging import logger
from apollo.config.store import EzAccessStore
from apollo.config.store import client as EzAccessStoreClient
from infra.common.glopts import GlobalOptions

IP_VERSION_6 = 6
IP_VERSION_4 = 4

L3PROTO_MIN = 0
L3PROTO_MAX = 256

PROTO_WILDCARD_MATCH_NONE = 0
PROTO_WILDCARD_MATCH_ANY = 256

L4PORT_MIN = 0
L4PORT_MAX = 65535

ICMPTYPE_MIN = 0
ICMPTYPE_MAX = 255

ICMPCODE_MIN = 0
ICMPCODE_MAX = 255

ETHER_HDR_LEN = 14
DOT1Q_HDR_LEN = 4

IPV4_MINADDR = ipaddress.ip_address("0.0.0.0")
IPV4_MAXADDR = ipaddress.ip_address("255.255.255.255")

IPV6_MINADDR = ipaddress.ip_address("0::0")
IPV6_MAXADDR = ipaddress.ip_address("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")

IPV4_DEFAULT_ROUTE = ipaddress.ip_network("0.0.0.0/0")
IPV6_DEFAULT_ROUTE = ipaddress.ip_network("0::/0")

NAT_ADDR_TYPE_PUBLIC = 0
NAT_ADDR_TYPE_SERVICE = 1

ZERO_BYTES = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
IPPROTO_TO_NAME_TBL = {num:name[8:] for name,num in vars(socket).items() if name.startswith("IPPROTO")}

IF_TYPE_SHIFT = 28
LIF_IF_LIF_ID_MASK = 0xFFFFFF

INVALID_BATCH_COOKIE = 0

def LifId2LifIfIndex(lifid):
    return ((topo.InterfaceTypes.LIF << IF_TYPE_SHIFT) | (lifid))

def LifIfindex2LifId(lififindex):
    return (lififindex & LIF_IF_LIF_ID_MASK)

def LifId2HostIfIndex(lifid):
    return ((topo.InterfaceTypes.HOST << IF_TYPE_SHIFT) | (lifid))

def HostIfindex2LifId(hostifindex):
    return (hostifindex & LIF_IF_LIF_ID_MASK)

def LifIfIndex2HostIfIndex(lififindex):
    return LifId2HostIfIndex(LifIfindex2LifId(lififindex))

PENSANDO_NIC_MAC = 0x022222111111
PDS_UUID_MAGIC_BYTE_VAL = 0x4242
PDS_UUID_BYTE_ORDER = "little"
PDS_NODE_UUID_BYTE_ORDER = "big"

PDS_UUID_LEN = 16
PDS_UUID_ID_LEN = 4
PDS_UUID_OBJTYPE_LEN = 2
PDS_UUID_RESERVED_LEN = 2
PDS_UUID_MAGIC_BYTE_LEN = 2
PDS_UUID_SYSTEM_MAC_LEN = 6

PDS_UUID_ID_OFFSET_START = 0
PDS_UUID_ID_OFFSET_END = PDS_UUID_ID_OFFSET_START + PDS_UUID_ID_LEN

PDS_UUID_OBJTYPE_OFFSET_START = PDS_UUID_ID_OFFSET_END
PDS_UUID_OBJTYPE_OFFSET_END = PDS_UUID_OBJTYPE_OFFSET_START + PDS_UUID_OBJTYPE_LEN

PDS_UUID_RESERVED_OFFSET_START = PDS_UUID_OBJTYPE_OFFSET_END
PDS_UUID_RESERVED_OFFSET_END = PDS_UUID_RESERVED_OFFSET_START + PDS_UUID_RESERVED_LEN

PDS_UUID_MAGIC_BYTE_OFFSET_START = PDS_UUID_RESERVED_OFFSET_END
PDS_UUID_MAGIC_BYTE_OFFSET_END = PDS_UUID_MAGIC_BYTE_OFFSET_START + PDS_UUID_MAGIC_BYTE_LEN

PDS_UUID_SYSTEM_MAC_OFFSET_START = PDS_UUID_MAGIC_BYTE_OFFSET_END

PDS_UUID_MAGIC_BYTE = PDS_UUID_MAGIC_BYTE_VAL.to_bytes(PDS_UUID_MAGIC_BYTE_LEN, PDS_UUID_BYTE_ORDER)
# only for SIM
PDS_UUID_SYSTEM_MAC = PENSANDO_NIC_MAC.to_bytes(PDS_UUID_SYSTEM_MAC_LEN, PDS_NODE_UUID_BYTE_ORDER)

class PdsUuid:

    def __init__(self, value, objtype=None, node_uuid=None):

        if node_uuid:
            node_uuid = node_uuid.to_bytes(PDS_UUID_SYSTEM_MAC_LEN, PDS_NODE_UUID_BYTE_ORDER)

        if isinstance(value, int):
            assert (value > 0), "ID value must be greater than zero"
            self.Id = value
            self.Type = objtype
            self.Uuid = PdsUuid.GetUUIDfromId(self.Id, self.Type, node_uuid)
        elif isinstance(value, bytes):
            self.Uuid = value
            self.Id = PdsUuid.GetIdfromUUID(self.Uuid)
        elif isinstance(value, list):
            self.Uuid = bytes(value)
            self.Id = PdsUuid.GetIdfromUUID(self.Uuid)
        else:
            assert 0, f"{type(value)} is NOT supported for PdsUuid class"
        self.UuidStr = PdsUuid.GetUuidString(self.Uuid)

    def __repr__(self):
        return self.UuidStr

    def __str__(self):
        return f"ID:{self.Id} UUID:{self.UuidStr}"

    def GetUuid(self):
        return self.Uuid

    def GetId(self):
        return self.Id

    def String(self):
        return self.UuidStr

    @staticmethod
    def SetSystemMAC(uuid_mac):
        global PDS_UUID_SYSTEM_MAC
        PDS_UUID_SYSTEM_MAC = uuid_mac.to_bytes(PDS_UUID_SYSTEM_MAC_LEN, PDS_NODE_UUID_BYTE_ORDER)

    @staticmethod
    def GetUuidString(uuid):
        # keep this inline with pds_obj_key_s.str()
        uuid_id = PdsUuid.GetIdfromUUID(uuid, "big")
        uuid_objtype = PdsUuid.GetObjTypefromUUID(uuid, "big")
        uuid_rsvd = PdsUuid.GetReservedValfromUUID(uuid, "big")
        uuid_magic = int.from_bytes(uuid[PDS_UUID_MAGIC_BYTE_OFFSET_START:PDS_UUID_MAGIC_BYTE_OFFSET_END], PDS_UUID_BYTE_ORDER)
        uuid_mac = int.from_bytes(uuid[PDS_UUID_SYSTEM_MAC_OFFSET_START:], PDS_NODE_UUID_BYTE_ORDER)
        uuidstr = f"{uuid_id:08x}-{uuid_objtype:04x}-{uuid_rsvd:04x}-{uuid_magic:04x}-{uuid_mac:012x}"
        return uuidstr

    @staticmethod
    def GetIdfromUUID(uuid, byte_order=PDS_UUID_BYTE_ORDER):
        return int.from_bytes(uuid[PDS_UUID_ID_OFFSET_START:PDS_UUID_ID_OFFSET_END], byte_order)

    @staticmethod
    def GetObjTypefromUUID(uuid, byte_order=PDS_UUID_BYTE_ORDER):
        return int.from_bytes(uuid[PDS_UUID_OBJTYPE_OFFSET_START:PDS_UUID_OBJTYPE_OFFSET_END], byte_order)

    @staticmethod
    def GetReservedValfromUUID(uuid, byte_order=PDS_UUID_BYTE_ORDER):
        return int.from_bytes(uuid[PDS_UUID_RESERVED_OFFSET_START:PDS_UUID_RESERVED_OFFSET_END], byte_order)

    @staticmethod
    def GetUUIDfromId(id, objtype=None, node_uuid=None):
        # uuid is of 16 bytes
        uuid = bytearray(PDS_UUID_LEN)
        if id == 0:
            return bytes(uuid)
        # first 4 bytes ==> id
        uuid[PDS_UUID_ID_OFFSET_START:PDS_UUID_ID_OFFSET_END] = id.to_bytes(PDS_UUID_ID_LEN, PDS_UUID_BYTE_ORDER)
        # next 2 bytes ==> object type (except HAL created objects like lifs & ports)
        if objtype:
            uuid[PDS_UUID_OBJTYPE_OFFSET_START:PDS_UUID_OBJTYPE_OFFSET_END] = objtype.to_bytes(PDS_UUID_OBJTYPE_LEN, PDS_UUID_BYTE_ORDER)
        # next 2 bytes ==> reserved (0x0000)
        # next 2 bytes ==> magic byte (0x4242)
        uuid[PDS_UUID_MAGIC_BYTE_OFFSET_START:PDS_UUID_MAGIC_BYTE_OFFSET_END] = PDS_UUID_MAGIC_BYTE
        # next 6 bytes ==> system mac (0x022222111111)
        if not node_uuid:
            node_uuid = PDS_UUID_SYSTEM_MAC
        uuid[PDS_UUID_SYSTEM_MAC_OFFSET_START:] = node_uuid
        return bytes(uuid)

def Int2IPAddrStr(ip_int):
    return str(ipaddress.ip_address(ip_int))

def Int2MacStr(mac_int):
    mac_hex = "{:012x}".format(mac_int)
    return ':'.join(format(s, '02x') for s in bytes.fromhex(mac_hex))

def List2UuidStr(uuid_list):
    return str(uuid.UUID(bytes=bytes(uuid_list)))

def GetRandomObject(objList):
    return random.choice(objList)

def GetRandomSamples(objList, num):
    return random.sample(objList, k=num)

def GetFilteredObjects(objs, maxlimits, randomize=False):
    if maxlimits == None or maxlimits == 0 or maxlimits >= len(objs):
        num = len(objs)
    else:
        num = maxlimits
    if randomize:
        return GetRandomSamples(objs, num)
    return objs[0:num]

def IsSkipHostDriverLoad():
    skip_host_driver_load = getattr(GlobalOptions, 'skip_host_driver_load', False)
    return skip_host_driver_load

def IsSkipSetup():
    skip_setup = getattr(GlobalOptions, 'skip_setup', False)
    return skip_setup

def IsNetAgentMode():
    return GlobalOptions.netagent

def IsDryRun():
    return GlobalOptions.dryrun

def Sleep(timeout=1):
    if IsDryRun():
        return
    time.sleep(timeout)
    return

def GetYamlSpecAttr(spec, attr='id', convert2uuid=True):
    val = spec[attr]
    return PdsUuid(val).GetUuid() if convert2uuid else val

def ValidateRpcIPAddr(srcaddr, dstaddr):
    if srcaddr.version == IP_VERSION_6:
        if dstaddr.Af != types_pb2.IP_AF_INET6:
            return False
        if dstaddr.V6Addr != srcaddr.packed:
            return False
    else:
        if dstaddr.Af != types_pb2.IP_AF_INET:
            return False
        if dstaddr.V4Addr != int(srcaddr):
            return False
    return True

def ValidateRpcIPV46Prefix(srcpfx, dstpfx):
    if srcpfx is None and dstpfx.Len == 0:
        return True
    if (srcpfx is not None and dstpfx.Len == 0) or (srcpfx is None and dstpfx.Len != 0):
        return False
    if dstpfx.Len != srcpfx.prefixlen:
        return False
    if srcpfx.version == IP_VERSION_6:
        if dstpfx.Addr != srcpfx.network_address.packed:
            return False
    else:
       if dstpfx.Addr != int(srcpfx.network_address):
           return False
    return True

def ValidateRpcIPPrefix(srcpfx, dstpfx):
    if dstpfx.Len != srcpfx.prefixlen:
        return False
    dstPfxAddr = dstpfx.Addr
    if srcpfx.version == IP_VERSION_6:
        if dstPfxAddr.Af != types_pb2.IP_AF_INET6:
            return False
        if dstPfxAddr.V6Addr != srcpfx.network_address.packed:
            return False
    else:
        if dstPfxAddr.Af != types_pb2.IP_AF_INET:
            return False
        if dstPfxAddr.V4Addr != int(srcpfx.network_address):
            return False
    return True

def ValidateRpcEncap(encapType, encapValue, encap):
    if encapType != encap.type:
        return False
    if encapType == types_pb2.ENCAP_TYPE_NONE:
        pass
    elif encapType == types_pb2.ENCAP_TYPE_DOT1Q:
        if encapValue != encap.value.VlanId:
            return False
    elif encapType == types_pb2.ENCAP_TYPE_QINQ:
        if encapValue[0] != encap.value.QinQ.CTag:
            return False
        if encapValue[1] != encap.value.QinQ.STag:
            return False
    elif encapType == types_pb2.ENCAP_TYPE_MPLSoUDP:
        if encapValue != encap.value.MPLSTag:
            return False
    elif encapType == types_pb2.ENCAP_TYPE_VXLAN:
        if encapValue != encap.value.Vnid:
            return False
    else:
        return False
    return True

def CompareSpec(objSpec, agentSpec, attrs=None):
    mismatchingAttrs = []
    if not attrs or len(attrs) == 0:
        # if nothing passed, validate all attributes present in objSpec
        attrs = objSpec.DESCRIPTOR.fields_by_name.keys()
        agentSpecAttrs = agentSpec.DESCRIPTOR.fields_by_name.keys()
        # make sure objSpec & agentSpec have same attributes
        mismatchingAttrs.extend(list(set(attrs) ^ set(agentSpecAttrs)))

    for attr in attrs:
        if getattr(objSpec, attr, None) != getattr(agentSpec, attr, None):
            mismatchingAttrs.append(attr)
    return mismatchingAttrs

def ValidateGrpcResponse(resp, expApiStatus=types_pb2.API_STATUS_OK):
    return expApiStatus == resp.ApiStatus

def ValidateGrpcValues(obj, resp):
    return obj.ValidateSpec(resp.Spec) and\
           obj.ValidateStats(resp.Stats) and\
           obj.ValidateStatus(resp.Status)

# TODO: convert yaml 2 proto and remove the following
def ValidateYamlValues(obj, resp):
    spec = resp['spec']
    status = resp['status']
    stats = resp['stats']
    return obj.ValidateYamlSpec(spec) and\
           obj.ValidateYamlStats(stats) and\
           obj.ValidateYamlStatus(status)

def ValidateObject(obj, resp, yaml=False):
    if obj is None:
        logger.info(f"Read failed - No object found for the response {resp}")
        return False
    if yaml:
        return ValidateYamlValues(obj, resp)
    return ValidateGrpcValues(obj, resp)

def GetAttrFromResponse(obj, resp, attr):
    result = getattr(resp, attr, None)
    if result is None:
        logger.error(f"Error fetching {attr} from {resp}")
        assert(0)
    if obj.IsSingleton():
        return [result]
    return result

def SetObjectHwHabitantStatus(obj, oper, status):
    if oper == 'Create' or oper == 'Update':
        if status == types_pb2.API_STATUS_OK:
            obj.SetHwHabitant(True)
    elif oper == 'Delete':
        if status == types_pb2.API_STATUS_OK:
            obj.SetHwHabitant(False)
    return

def ValidateBatch(batchList, cookie):
    item = batchList[0]
    oper = item[0]
    obj = item[1]
    operStatus = item[2]
    commitStatus = item[3]

    if IsDryRun():
        SetObjectHwHabitantStatus(obj, oper, types_pb2.API_STATUS_OK)
        return True

    is_obj_present = obj.IsHwHabitant()

    logger.info(f"ValidateBatch: operation {oper} for {obj} on {obj.Node} "
                f"obj_present {is_obj_present}, cookie {cookie}")

    expApiStatus = types_pb2.API_STATUS_OK
    if is_obj_present == True:
        if oper == 'Create':
            expApiStatus = types_pb2.API_STATUS_EXISTS_ALREADY
        elif oper == 'Update' or oper == 'Delete':
            expApiStatus = types_pb2.API_STATUS_OK
    else:
        if oper == 'Create':
            expApiStatus = types_pb2.API_STATUS_OK
        elif oper == 'Update' or oper == 'Delete':
            expApiStatus = types_pb2.API_STATUS_NOT_FOUND

    logger.info(f"ValidateBatch: recvd {operStatus}, {commitStatus} "
            f"Expected {expApiStatus}")

    commitResp = (expApiStatus == commitStatus)

    def __validateOperation(oper, operStatus, expApiStatus):
        operResp = False
        if oper == 'Create':
            operResp = ValidateCreate(obj, operStatus, expApiStatus)
        elif oper == 'Delete':
            operResp = ValidateDelete(obj, operStatus, expApiStatus)
        elif oper == 'Update':
            operResp = ValidateUpdate(obj, operStatus, expApiStatus)
        return operResp

    # if batch is created internally
    if cookie == INVALID_BATCH_COOKIE:
        operResp = __validateOperation(oper, operStatus, expApiStatus)
        if operResp:
            SetObjectHwHabitantStatus(obj, oper, expApiStatus)
        logger.info(f"ValidateBatch: internal batch result {operResp}")
        return operResp

    # objects which do lookup in pdsagent
    dBObjs = [ ObjectTypes.INTERFACE ]
    if obj.ObjType not in dBObjs:
        # validate operStatus against types_pb2.API_STATUS_OK
        expApiStatus = types_pb2.API_STATUS_OK
        operResp = __validateOperation(oper, operStatus, expApiStatus)
    else:
        operResp = __validateOperation(oper, operStatus, expApiStatus)
        # validate commitStatus against types_pb2.API_STATUS_OK
        commitResp = (commitStatus == types_pb2.API_STATUS_OK)

    if operResp and commitResp:
        SetObjectHwHabitantStatus(obj, oper, commitStatus)
    logger.info(f"ValidateBatch: batch result {operResp} and {commitResp}")
    return (operResp and commitResp)

def ValidateCreate(obj, resps, expApiStatus = types_pb2.API_STATUS_OK):
    if IsDryRun():
        # assume creation was fine in case of dry run
        obj.SetHwHabitant(True)
        return True
    for resp in resps:
        if not ValidateGrpcResponse(resp, expApiStatus):
            logger.error(f"[Re]Creation failed for {obj} on {obj.Node}, received resp {resp.ApiStatus} and expected resp {expApiStatus}")
            obj.Show()
            return False
    return True

def ValidateRead(obj, resps, expApiStatus = types_pb2.API_STATUS_OK):
    if IsDryRun(): return True
    for resp in resps:
        if ValidateGrpcResponse(resp, expApiStatus):
            # resp is API_STATUS_OK then ValidateObject
            # otherwise pass
            if ValidateGrpcResponse(resp):
                readresponse = GetAttrFromResponse(obj, resp, 'Response')
                if len(readresponse) == 0:
                    logger.info(f"ValidateRead failed for {obj} on {obj.Node}, received empty response")
                    return False
                for response in readresponse:
                    if ValidateObject(obj, response):
                        if hasattr(obj, 'Status'):
                            obj.Status.Update(response.Status)
                        if hasattr(obj, 'Stats'):
                            obj.Stats.Update(response.Stats)
                    else:
                        logger.info(f"ValidateRead failed for {obj} on {obj.Node}, received resp {resp} & expected status {expApiStatus}")
                        return False
        else:
            logger.info(f"ValidateRead failed for {obj} on {obj.Node} with unexpected status, received resp {resp} & expected status {expApiStatus}")
            return False
    return True

def ValidateDelete(obj, resps, expApiStatus = types_pb2.API_STATUS_OK):
    if IsDryRun():
        # assume deletion was fine in case of dry run
        obj.SetHwHabitant(False)
        return True
    for resp in resps:
        respStatus = GetAttrFromResponse(obj, resp, 'ApiStatus')
        for status in respStatus:
            if status != expApiStatus:
                logger.error(f"Deletion failed for {obj} on {obj.Node}, received {status} but expected {expApiStatus}")
                obj.Show()
                return False
    return True

def ValidateUpdate(obj, resps, expApiStatus = types_pb2.API_STATUS_OK):
    if IsDryRun(): return True
    for resp in resps:
        if ValidateGrpcResponse(resp, expApiStatus):
            if expApiStatus == types_pb2.API_STATUS_OK:
                InformDependents(obj, 'UpdateNotify')
        else:
            logger.error(f"Update failed for {obj} on {obj.Node}, received {resp.ApiStatus}, expected {expApiStatus}")
            obj.PrepareRollbackUpdate()
            obj.SetDirty(False)
            return False
    return True

def ValidateTagsList(obj_tag_list, spec_tag_list):
    if len(obj_tag_list) != len(spec_tag_list):
        return False
    for tag in obj_tag_list:
        if tag not in spec_tag_list:
            return False
    return True

def LoadYaml(cmdoutput):
    return yaml.load(cmdoutput, Loader=yaml.FullLoader)

def GetBatchCookie(node):
    batchClient = EzAccessStore.GetBatchClient()
    obj = batchClient.GetObjectByKey(node)
    return obj.GetBatchCookie()

def GetBatchCommitStatus(node):
    batchClient = EzAccessStore.GetBatchClient()
    obj = batchClient.GetObjectByKey(node)
    return obj.GetBatchCommitStatus()

def InformDependents(dependee, cbFn):
    # inform dependent objects
    if not IsInformRelatedObjs(dependee.Node): return
    for objType, ObjList in dependee.Deps.items():
        for depender in ObjList:
            getattr(depender, cbFn)(dependee)
    return

def TriggerCreate(obj):
    node = obj.Node
    if IsNetAgentMode():
        return api.client[node].Create(obj.ObjType, [obj])
    else:
        batchClient = EzAccessStore.GetBatchClient()
        batchClient.Start(node)
        cookie = GetBatchCookie(node)
        msg = obj.GetGrpcCreateMessage(cookie)
        operStatus = api.client[node].Create(obj.ObjType, [msg])
        batchClient.Commit(node)
        commitStatus = GetBatchCommitStatus(node)
        validate = ('Create', obj, operStatus, commitStatus)
        return ValidateBatch([validate], cookie)

def CreateObject(obj):
    node = obj.Node
    if IsDryRun() and obj.IsHwHabitant():
        logger.info(f"Already restored {obj} on {node}")
        return True

    if not obj.IsOriginFixed():
        logger.info(f"Skipping creation of non-fixed object {obj} on {node}")
        return True

    def RestoreObj(robj):
        node = robj.Node
        if robj.IsOriginDiscovered():
            logger.info(f"Skip Recreating discovered object {robj} on {node}")
            return True
        if robj.Duplicate != None:
            #TODO: Ideally a new dependee object should be created first
            # and all dependents should be updated to point to the new object
            # before we delete this. Otherwise, for a moment, the dependent objects
            # are pointing to an object which has been deleted which leads to
            # inconsistency. Doing it this way in DOL, since IOTA scale test cases
            # uses the same code, and at full scale we can't create an object
            # without freeing another.
            logger.info(f"Deleting object {robj.Duplicate} on {node}")
            res = TriggerDelete(robj.Duplicate)
            if not res:
                logger.error(f"Failed to delete {robj} on {node}")
                return res
            confClient = EzAccessStoreClient[node].GetConfigClient(robj.ObjType)
            if confClient == None:
                return False
            confClient.DeleteObjFromDict(robj.Duplicate)
            robj.Duplicate = None
            logger.info(f"[Re]Creating object {robj} on {node}")
        else:
            logger.info(f"Creating object {robj} on {node}")
        res = TriggerCreate(robj)
        if not res:
            logger.error(f"Failed to create {robj} on {node}")
            return res
        if IsUpdateSupported():
            InformDependents(robj, 'RestoreNotify')
        return True

    # create from top to bottom
    res = RestoreObj(obj)
    if not res:
        logger.error(f"Failed to restore {obj} on {node}, skip creating children")
        return res
    res = list(map(lambda childObj: not(CreateObject(childObj)), obj.Children))
    if any(res):
        logger.error(f"Failed to create children of {obj} on {node}, "
                     f"res {res} children {obj.Children}")
        return False
    return True

def ReadObject(obj, expApiStatus = types_pb2.API_STATUS_OK):
    if IsNetAgentMode():
        found = False
        ret = False
        resps = api.client[obj.Node].GetHttp(obj.ObjType)
        if not resps:
            return types_pb2.API_STATUS_NOT_FOUND
        for j in resps:
            if hasattr(j['meta'], 'uuid'):
                if j['meta']['uuid'] == obj.UUID.UuidStr:
                    found = True
                    ret = obj.ValidateJSONSpec(j)
                else:
                    logger.error('uuid not found in - ', j)
        # positive case. obj found and valid
        if expApiStatus == types_pb2.API_STATUS_OK:
            if found and ret:
                return expApiStatus
            else:
                return types_pb2.API_STATUS_NOT_FOUND
        # negative case. either object not found, or validation fail
        else:
            if not found or not ret:
                return expApiStatus
            else:
                return types_pb2.API_STATUS_OK
    else:
        msg = obj.GetGrpcReadMessage()
        resps = api.client[obj.Node].Get(obj.ObjType, [msg])
        return ValidateRead(obj, resps, expApiStatus)

def UpdateObject(obj):
    logger.info(f"Updating object {obj} on {obj.Node}")
    node = obj.Node
    if IsNetAgentMode():
        return api.client[node].Update(obj.ObjType, [obj])
    else:
        batchClient = EzAccessStore.GetBatchClient()
        batchClient.Start(node)
        cookie = GetBatchCookie(node)
        msg = obj.GetGrpcUpdateMessage(cookie)
        operStatus = api.client[node].Update(obj.ObjType, [msg])
        batchClient.Commit(node)
        commitStatus = GetBatchCommitStatus(node)
        validate = ('Update', obj, operStatus, commitStatus)
        return ValidateBatch([validate], cookie)

def TriggerDelete(obj):
    node = obj.Node
    if IsNetAgentMode():
        return api.client[node].Delete(obj.ObjType, [obj])
    else:
        batchClient = EzAccessStore.GetBatchClient()
        batchClient.Start(node)
        cookie = GetBatchCookie(node)
        msg = obj.GetGrpcDeleteMessage(cookie)
        operStatus = api.client[node].Delete(obj.ObjType, [msg])
        batchClient.Commit(node)
        commitStatus = GetBatchCommitStatus(node)
        validate = ('Delete', obj, operStatus, commitStatus)
        return ValidateBatch([validate], cookie)

def DeleteObject(obj):
    node = obj.Node
    if IsDryRun() and not obj.IsHwHabitant():
        logger.info(f"Already deleted {obj} on {node}")
        return True

    if not obj.IsOriginFixed():
        logger.info(f"Skipping deletion of non-fixed object {obj} on {node}")
        return True

    def DelObj(dobj):
        node = dobj.Node
        if dobj.IsOriginDiscovered():
            logger.info(f"Skip Deleting discovered object {dobj} on {node}")
            return True
        logger.info(f"Deleting object {dobj} on {node}")
        #TODO: Ideally a new dependee object should be created first
        # and all dependents should be updated to point to the new object
        # before we delete this. Otherwise, for a moment, the dependent objects
        # are pointing to an object which has been deleted which leads to
        # inconsistency. Doing it this way in DOL, since IOTA scale test cases
        # uses the same code, and at full scale we can't create an object
        # without freeing another.
        obj_present = dobj.IsHwHabitant()
        ret = TriggerDelete(dobj)
        if not ret:
            logger.error(f"Failed to delete {dobj} on {node}")
            return False

        # create duplicate object only if object is present
        if not obj_present:
            return True

        if IsUpdateSupported():
            if dobj.ObjType == ObjectTypes.TUNNEL or\
               dobj.ObjType == ObjectTypes.NEXTHOP or\
               dobj.ObjType == ObjectTypes.NEXTHOPGROUP or\
               dobj.ObjType == ObjectTypes.INTERFACE:
                dupObj = dobj.Dup()
                confClient = EzAccessStore.GetConfigClient(dobj.ObjType)
                if confClient == None:
                    return False
                confClient.AddObjToDict(dupObj)
                TriggerCreate(dupObj)
            InformDependents(dobj, 'DeleteNotify')
        return True

    # Delete from bottom to top
    res = list(map(lambda childObj: not(DeleteObject(childObj)), obj.Children))
    if any(res):
        logger.error(f"Failed to cleanup children of {obj} on {node}, "
                     f"res {res} children {obj.Children}")
        return False
    # Delete the final
    return DelObj(obj)

def GetIPProtoByName(protoname):
    """
        returns IP Protocol number for the given protocol name
    """
    return socket.getprotobyname(protoname)

def GetIPProtoName(proto, get_rand_l3_proto=False):
    """
        returns IP Protocol name for the given protocol number
    """
    if proto == L3PROTO_MAX:
        # get any random l3 proto
        if get_rand_l3_proto:
            l3_proto = ['ICMP', 'TCP', 'UDP']
            return random.choice(l3_proto)
        else:
            return 'any'
    return IPPROTO_TO_NAME_TBL[proto]

def IsICMPProtocol(proto):
    """
        returns True if given proto is icmp/icmpv6
    """
    return 'ICMP' in GetIPProtoName(proto)

def __get_subnet(ip, prev=False):
    """
        returns next subnet of 'ip' if 'prev' is False else previous subnet
    """
    totalhosts = -1 if prev else 1
    totalhosts *= ip.num_addresses
    newpfx = str(ip.network_address + totalhosts) + '/' + str(ip.prefixlen)
    return ipaddress.ip_network(newpfx)

def GetNextSubnet(ip):
    return __get_subnet(ip)

def GetPreviousSubnet(ip):
    return __get_subnet(ip, prev=True)

def GetRandomPrefix(pfx):
    """
        return random prefix within the subnet of pfx
    """
    pfxLen = pfx.prefixlen
    maxPfxLen = pfx.max_prefixlen
    newPfxLen = random.randint(pfxLen, maxPfxLen)
    newPfxAddr = pfx.network_address + random.getrandbits(maxPfxLen - pfxLen)
    newPfxStr = f"{str(newPfxAddr)}/{newPfxLen}"
    return ipaddress.ip_network(newPfxStr, False)

def GetTunnelType(e):
    if e == 'internet-gateway':
        return tunnel_pb2.TUNNEL_TYPE_IGW
    elif e == 'workload':
        return tunnel_pb2.TUNNEL_TYPE_WORKLOAD
    elif e == 'service' or e == 'remoteservice':
        return tunnel_pb2.TUNNEL_TYPE_SERVICE
    elif e == 'ipsec':
        return tunnel_pb2.TUNNEL_TYPE_IPSEC
    else:
        return tunnel_pb2.TUNNEL_TYPE_NONE

def GetTunnelTypeString(e):
    if e == tunnel_pb2.TUNNEL_TYPE_IGW:
        return "internet-gateway"
    elif e == tunnel_pb2.TUNNEL_TYPE_WORKLOAD:
        return "workload"
    elif e == tunnel_pb2.TUNNEL_TYPE_SERVICE:
        return "service"
    elif e == tunnel_pb2.TUNNEL_TYPE_IPSEC:
        return "ipsec"
    elif e == tunnel_pb2.TUNNEL_TYPE_NONE:
        return "None"
    assert (0), f"ERROR: Invalid/Unknown Tunnel Type: {e}"
    return None

def GetErspanProtocolType(t):
    if t == 0:
        return topo.ErspanProtocolTypes.ERSPAN_TYPE_NONE
    elif t == 1:
        return topo.ErspanProtocolTypes.ERSPAN_TYPE_1
    elif t == 2:
        return topo.ErspanProtocolTypes.ERSPAN_TYPE_2
    elif t == 3:
        return topo.ErspanProtocolTypes.ERSPAN_TYPE_3
    assert (0), f"ERROR: Invalid/Unknown Erspan Type:{t}"
    return None

def isDefaultRoute(ippfx):
    if ippfx == IPV4_DEFAULT_ROUTE or ippfx == IPV6_DEFAULT_ROUTE:
        return True
    return False

def isDefaultAddrRange(addrLow, addrHigh):
    if addrLow == IPV4_MINADDR and addrHigh == IPV4_MAXADDR:
        return True
    if addrLow == IPV6_MINADDR and addrHigh == IPV6_MAXADDR:
        return True
    return False

def isTagWithDefaultRoute(tag):
    tagpfxlist = tag.Prefixes if tag else None
    if tagpfxlist is None:
        return False
    for tagpfx in tagpfxlist:
        if isDefaultRoute(tagpfx):
            return True
    return False

def GetIPVersion(ipaf):
    if ipaf == 'IPV6':
        return IP_VERSION_6
    return IP_VERSION_4

def GetRpcIPAddrFamily(ipaddrfamily):
    if ipaddrfamily == 'IPV6':
        return types_pb2.IP_AF_INET6
    elif ipaddrfamily == 'IPV4':
        return types_pb2.IP_AF_INET
    else:
        return types_pb2.IP_AF_NONE

def GetRpcIPPrefix(srcpfx, dstpfx):
    dstpfx.Len = srcpfx.prefixlen
    if srcpfx.version == IP_VERSION_6:
        dstpfx.Addr.Af = types_pb2.IP_AF_INET6
        dstpfx.Addr.V6Addr = srcpfx.network_address.packed
    else:
        dstpfx.Addr.Af = types_pb2.IP_AF_INET
        dstpfx.Addr.V4Addr = int(srcpfx.network_address)

def GetRpcIPv4Prefix(srcpfx, dstpfx):
    dstpfx.Len = srcpfx.prefixlen
    dstpfx.Addr = int(srcpfx.network_address)

def GetRpcIPv6Prefix(srcpfx, dstpfx):
    dstpfx.Len = srcpfx.prefixlen
    dstpfx.Addr = srcpfx.network_address.packed

def GetRpcIfIPPrefix(srcpfx, dstpfx):
    dstpfx.Len = ipaddress.ip_network(srcpfx.network).prefixlen
    dstpfx.Addr.Af = types_pb2.IP_AF_INET
    dstpfx.Addr.V4Addr = int(ipaddress.ip_address(srcpfx.ip))

def GetRpcIPAddr(srcaddr, dstaddr):
    if srcaddr.version == IP_VERSION_6:
        dstaddr.Af = types_pb2.IP_AF_INET6
        dstaddr.V6Addr = srcaddr.packed
    else:
        dstaddr.Af = types_pb2.IP_AF_INET
        dstaddr.V4Addr = int(srcaddr)

def GetRpcIPRange(addrLow, addrHigh, addrRange):
    if addrLow.version != addrHigh.version:
        assert (0), f"ERROR: addrRange version mismatch: Low {addrLow} High {addrHigh}"
    if addrLow.version == IP_VERSION_6:
        GetRpcIPAddr(addrLow, addrRange.IPv6Range.Low)
        GetRpcIPAddr(addrHigh, addrRange.IPv6Range.High)
    else:
        GetRpcIPAddr(addrLow, addrRange.IPv4Range.Low)
        GetRpcIPAddr(addrHigh, addrRange.IPv4Range.High)
    return

def PopulateRpcEncap(encapType, encapValue, specEncap):
    specEncap.type = encapType
    if encapType == types_pb2.ENCAP_TYPE_NONE:
        pass
    elif encapType == types_pb2.ENCAP_TYPE_DOT1Q:
        specEncap.value.VlanId = encapValue
    elif encapType == types_pb2.ENCAP_TYPE_QINQ:
        specEncap.value.QinQ.CTag  = encapValue[0]
        specEncap.value.QinQ.STag  = encapValue[1]
    elif encapType == types_pb2.ENCAP_TYPE_MPLSoUDP:
        specEncap.value.MPLSTag  = encapValue
    elif encapType == types_pb2.ENCAP_TYPE_VXLAN:
        specEncap.value.Vnid  = encapValue
    else:
        assert (0), f"Unsupported encap type {encapType}"

def GetRpcDeviceMode(mode):
    if mode == "host":
        return device_pb2.DEVICE_OPER_MODE_HOST
    elif mode == "bitw_smart_switch":
        return device_pb2.DEVICE_OPER_MODE_BITW_SMART_SWITCH
    elif mode == "bitw_smart_service":
        return device_pb2.DEVICE_OPER_MODE_BITW_SMART_SERVICE
    elif mode == "bitw_classic_switch":
        return device_pb2.DEVICE_OPER_MODE_BITW_CLASSIC_SWITCH
    return device_pb2.DEVICE_OPER_MODE_NONE


def GetRpcDirection(direction):
    if direction == "ingress":
         return types_pb2.RULE_DIR_INGRESS
    elif direction == "egress":
         return types_pb2.RULE_DIR_EGRESS
    return types_pb2.RULE_DIR_NONE

def GetRpcSecurityRuleAction(action):
    if action == "allow":
        return types_pb2.SECURITY_RULE_ACTION_ALLOW
    elif action == "deny":
        return types_pb2.SECURITY_RULE_ACTION_DENY
    else:
        return types_pb2.SECURITY_RULE_ACTION_NONE

def GetRpcUpgradeMode(mode):
    if mode == "hitless":
        return upgrade_pb2.UPGRADE_MODE_HITLESS
    elif mode == "graceful":
        return upgrade_pb2.UPGRADE_MODE_GRACEFUL
    else:
        return upgrade_pb2.UPGRADE_MODE_NONE

def GetPortIDfromInterface(interfaceid):
    return topo.INTF2PORT_TBL.get(interfaceid, topo.PortTypes.NONE)

def IsPipelineArtemis():
    if GlobalOptions.pipeline == 'artemis':
        return True
    return False

def IsPipelineApulu():
    if GlobalOptions.pipeline == 'apulu':
        return True
    return False

def IsPipelineApollo():
    if GlobalOptions.pipeline == 'apollo':
        return True
    return False

def GetPipelineName():
    return GlobalOptions.pipeline

def IsHostLifSupported():
    if IsPipelineArtemis():
        return False
    return True

def IsInterfaceSupported():
    if IsPipelineArtemis():
        return False
    return True

def IsL3InterfaceSupported():
    if IsPipelineApulu():
        return True
    return False

def IsIGWTunnelSupported():
    if IsPipelineApulu():
        return False
    return True

def IsWorkloadTunnelSupported():
    if IsPipelineApulu():
        return False
    return True

def IsServiceTunnelSupported():
    if IsPipelineArtemis():
        return True
    return False

def IsUnderlayTunnelSupported():
    if IsPipelineApulu():
        return True
    return False

def IsIpsecTunnelSupported():
    if IsPipelineApulu():
        return True
    return False

def IsRouteTableSupported():
    return True

def IsVnicPolicySupported():
    if IsPipelineApulu():
        return True
    return False

def IsTagSupported():
    if IsPipelineArtemis():
        return True
    if IsPipelineApulu():
        return True
    return False

def IsMeteringSupported():
    if IsPipelineArtemis():
        return True
    return False

def IsServiceMappingSupported():
    if IsPipelineArtemis():
        return True
    return False

def IsPfxRangeSupported():
    if IsPipelineApollo():
        return False
    return True

def IsFlowInstallationNeeded():
    if IsPipelineArtemis():
        return True
    return False

def IsNatSupported():
    if IsPipelineArtemis() or IsPipelineApollo():
        return True
    return False

def IsDualEcmp(spec):
    return getattr(spec, 'dual-ecmp', False)

def GetDefaultDeviceMode():
    if IsPipelineApulu():
        return 'host'
    return 'bitw'

def IsV4Stack(stack):
    return True if ((stack == "dual") or (stack == 'ipv4')) else False

def IsV6Stack(stack):
    return True if ((stack == "dual") or (stack == 'ipv6')) else False

def GetVlanHeaderSize(packet):
    pkt = packet.GetScapyPacket()
    if Dot1Q in pkt:
        return DOT1Q_HDR_LEN
    return 0

def IsUpdateSupported():
    if IsPipelineApulu():
        return True
    return False

def MergeFilteredObjects(objs, selected_objs):
    if topo.ChosenFlowObjs.select_objs is True:
        topo.ChosenFlowObjs.Add(objs)
    elif topo.ChosenFlowObjs.use_selected_objs is True:
        objs.extend(selected_objs)

def MergeDicts(objs1, objs2):
    objs = OrderedDict()
    for obj in objs1.values():
        objs.update({obj.Id: obj})
    for obj in objs2.values():
        objs.update({obj.Id: obj})
    return objs

# For debug purposes
def dump(obj):
   for attr in dir(obj):
       if hasattr( obj, attr ):
           logger.info( "obj.%s = %s" % (attr, getattr(obj, attr)))

def DumpObject(obj):
    parentObj = getattr(obj, 'Parent', None)
    if parentObj:
        DumpObject(parentObj)
    logger.info(" === Selected %s === " % (obj.ObjType.name))
    obj.Show()
    return

def ReadObjectStats(cfgObj, obj):
    parentObj = getattr(cfgObj, 'Parent', None)
    if parentObj:
        ReadObjectStats(parentObj, obj)
    cfgObj.Read()
    if hasattr(cfgObj, 'Stats'):
        obj.statscache[cfgObj.GID()] = cfgObj.Stats
    return

def ReadTestcaseStats(obj):
    tcAttrs = [ 'route', 'tunnel', 'policy', 'localmapping', 'remotemapping']
    obj.statscache = {}
    for attr in tcAttrs:
        cfgObj = getattr(obj, attr, None)
        if cfgObj: ReadObjectStats(cfgObj, obj)
    return

def DumpTestcaseConfig(obj):
    tcAttrs = [ 'route', 'tunnel', 'policy', 'localmapping', 'remotemapping', 'upgrade']
    logger.info("========== Testcase config start ==========")
    for attr in tcAttrs:
        cfgObj = getattr(obj, attr, None)
        if cfgObj: DumpObject(cfgObj)
    ruleObj = getattr(obj, 'tc_rule', None)
    objs = list(filter(None, [ruleObj]))
    for cfgObj in objs:
        logger.info(" === Selected rule/route ===")
        cfgObj.Show()
    logger.info("========== Testcase config end ==========")
    return

if os.environ.get('PDSPKG_TOPDIR') is not None:
    PDSPKG_TOPDIR=os.environ.get('PDSPKG_TOPDIR')
else:
    PDSPKG_TOPDIR = os.environ['WS_TOP'] + "/nic/"

def GetDeviceJsonPath():
    rel_path = "conf/%s/device.json" % (GetPipelineName())
    abs_path = os.path.join(PDSPKG_TOPDIR, rel_path)
    return abs_path

def GetNicmgrLogPath():
    rel_path = "nicmgr.log"
    abs_path = os.path.join(PDSPKG_TOPDIR, rel_path)
    return abs_path

class rrobiniter:
    def __init__(self, objs):
        assert len(objs) != 0
        self.objs = objs
        self.iterator = iter(objs)
        self.size = len(objs)
    def rrnext(self):
        while True:
            try:
                return next(self.iterator)
            except:
                self.iterator = iter(self.objs)
                continue
    def size(self):
        return self.size

def IsBatchingDisabled():
    no_batching = os.environ.get("BATCHING_DISABLED", 0)
    if int(no_batching) == 1:
        return True
    return False

def IsDol():
    return defs.TEST_TYPE == "DOL"

def IsBitwSmartSvcMode(node=None):
    dutNode = EzAccessStore.GetDUTNode() if not node else node
    deviceObj = EzAccessStoreClient[dutNode].GetDevice()
    return deviceObj.IsBitwSmartSvcMode()

def RunPdsctlShowCmd(node, cmd, args=None, yaml=True):
    if IsDol():
        import apollo.test.utils.pdsctl as pdsctl
        ret, yaml_op = pdsctl.ExecutePdsctlShowCommand(cmd, args, yaml)
    else:
        import iota.test.apulu.utils.pdsctl as pdsctl
        ret, yaml_op = pdsctl.ExecutePdsctlShowCommand(node, cmd, args, yaml, print_op=False)
    return ret, yaml_op

def GetNodeLoopbackPrefix(node):
    loopback_ip = EzAccessStoreClient[node].GetLoopbackIp()
    if loopback_ip:
        return ipaddress.ip_network(loopback_ip + "/32")
    else:
        return None

def GetNodeLoopbackIp(node):
    loopback_ip = EzAccessStoreClient[node].GetLoopbackIp()
    if loopback_ip:
        return ipaddress.IPv4Address(loopback_ip)
    else:
        return None

def GetNodeLoopbackRemoteTEP(node):
    rtep = EzAccessStoreClient[node].GetLoopbackRemoteTEP()
    if rtep:
        return ipaddress.IPv4Address(rtep)
    else:
        return None

def GetNodeUnderlayIp(node, intf_name):
    ip = EzAccessStoreClient[node].GetUnderlayIp(intf_name)
    if ip:
        return(ipaddress.IPv4Address(ip))
    return None

def GetNodeUnderlayMaskLen(node, intf_name):
    return EzAccessStoreClient[node].GetUnderlayMaskLen(intf_name)

def GetNodeUnderlayBGPRemoteASN(node, intf_name):
    asn = EzAccessStoreClient[node].GetUnderlayBGPRemoteASN(intf_name)
    if asn:
         return(int(asn))
    return None

def GetNodeUnderlayNexthop(node, intf_name):
    nexthop = EzAccessStoreClient[node].GetUnderlayNexthop(intf_name)
    if nexthop:
        return(ipaddress.IPv4Address(nexthop))
    return None

def IsBridgingEnabled(node):
    return EzAccessStoreClient[node].IsBridgingEnabled()

def CopySpec(spec, ifspec):
    for attr in ifspec.__dict__:
        setattr(spec, attr, getattr(ifspec, attr))
    return spec

def ValidateListAttr(cfgAttr, specAttr):
    mismatchCount = len(set(cfgAttr) ^ set(specAttr))
    return True if mismatchCount == 0 else False

def ValidatePolicyAttr(cfgAttr, specAttr):
    if cfgAttr == None and specAttr == None:
        return True
    if (cfgAttr == None and specAttr) or (cfgAttr and specAttr == None):
        return False
    if len(cfgAttr) != len(specAttr):
        return False
    specAttr.sort()
    cfgAttrUUID = list(map(lambda policyId: \
                           PdsUuid.GetUUIDfromId(policyId, ObjectTypes.POLICY),\
                           cfgAttr))
    cfgAttrUUID.sort()
    if specAttr != cfgAttrUUID:
        return False
    return True

def ValidateMirrorAttr(cfgAttr, specAttr):
    if cfgAttr == None and specAttr == None:
        return True
    if (cfgAttr == None and specAttr) or (cfgAttr and specAttr == None):
        return False
    if len(cfgAttr) != len(specAttr):
        return False
    specAttr.sort()
    cfgAttrUUID = list(map(lambda mirrorId: \
                           PdsUuid.GetUUIDfromId(mirrorId, ObjectTypes.MIRROR),\
                           cfgAttr))
    cfgAttrUUID.sort()
    if specAttr != cfgAttrUUID:
        return False
    return True

def ValidatePolicerAttr(cfgAttr, specAttr):
    if cfgAttr == None and specAttr == ZERO_BYTES:
        return True
    if cfgAttr == None and specAttr != ZERO_BYTES:
        return False
    if specAttr != cfgAttr.UUID.GetUuid():
        return False
    return True

def GetPolicies(obj, spec, node, af, direction, generate=True):

    is_subnet = True if obj.ObjType == api.ObjectTypes.SUBNET else False
    def __get_vpc_id():
        if is_subnet:
            return obj.VPC.VPCId
        else:
            return obj.SUBNET.VPC.VPCId
        return None

    def __get_policies_frm_count(node, vpcid, count, cb):
        policyids = []
        for _ in range(count):
            id = cb(node, vpcid)
            if id:
                policyids.append(id)
        return policyids

    PolicyClient = EzAccessStore.GetConfigClient(ObjectTypes.POLICY)
    policy_attr_map = {
        "V4ingress" : ("v4ingrpolicycount", "v4ingrpolicies", PolicyClient.GetIngV4SecurityPolicyId),
        "V6ingress" : ("v6ingrpolicycount", "v6ingrpolicies", PolicyClient.GetIngV6SecurityPolicyId),
        "V4egress"  : ("v4egrpolicycount", "v4egrpolicies", PolicyClient.GetEgV4SecurityPolicyId),
        "V6egress"  : ("v6egrpolicycount", "v6egrpolicies", PolicyClient.GetEgV6SecurityPolicyId)
    }
    count_attr, specific_attr, cb = policy_attr_map[af+direction]
    policyids = getattr(spec, specific_attr, None)
    if policyids: return policyids
    count = getattr(spec, count_attr, 0)
    if not count: return []
    if generate:
        if is_subnet:
            policy_id = cb(node, __get_vpc_id())
            return PolicyClient.GenerateSubnetPolicies(obj, policy_id, count, direction, True if af == 'V6' else False)
        else:
            return PolicyClient.GenerateVnicPolicies(count, obj, direction, True if af == 'V6' else False)
    else:
        return __get_policies_frm_count(node, __get_vpc_id(), count, cb)

def GetTopoSpec(filename):
    path = '%s/config/topology/%s' % \
            (GlobalOptions.pipeline, GlobalOptions.topology)
    topospec = parser.ParseFile(path, filename)
    nodespec = getattr(topospec, 'node', None)
    if not nodespec:
        assert(0)
    return nodespec[0]

def MergeYAMLs(spec1, spec2):
    if isinstance(spec1, int):
        return
    if isinstance(spec1, list) and isinstance(spec2, list):
        if hasattr(spec1[0], 'id') and hasattr(spec2[0], 'id'):
            for d1 in spec1:
                for d2 in spec2:
                    if getattr(d1, 'id') == getattr(d2, 'id'):
                        MergeYAMLs(d1, d2)
                        return
        else:
            spec1.extend(spec2)
            return
    for attr1 in vars(spec1):
        data1 = getattr(spec1, attr1)
        if not data1:
            continue
        for attr2 in vars(spec2):
            if attr1 == attr2:
                # merge
                data2 = getattr(spec2, attr2)
                if not data2:
                    continue
                MergeYAMLs(data1, data2)
    for attr2 in vars(spec2):
        if not hasattr(spec1, attr2):
            setattr(spec1, attr2, getattr(spec2, attr2))
    return

def GetReconfigState(node):
    return EzAccessStoreClient[node].NodeObj.ReconfigState

def IsReconfigInProgress(node):
    ReconfigState = GetReconfigState(node)
    if ReconfigState.InProgress:
        return True
    return False

def SetReconfigInProgress(node, state=True):
    ReconfigState = GetReconfigState(node)
    ReconfigState.InProgress = True
    return

def ModifyPolicyDependency(obj, delete):
    policyids = obj.IngV4SecurityPolicyIds + obj.EgV4SecurityPolicyIds
    PolicyClient = EzAccessStore.GetConfigClient(ObjectTypes.POLICY)
    policyobjs = PolicyClient.GetObjectsByKeys(obj.Node, policyids)
    for policy in policyobjs:
        if policy != None:
            if delete:
                policy.DeleteDependent(obj)
            else:
                policy.AddDependent(obj)
    return

def ReconfigPolicies(obj, spec):
    if not hasattr(spec, 'v4ingrpolicycount') and not hasattr(spec, 'v4egrpolicycount'):
        return
    ModifyPolicyDependency(obj, True)
    obj.IngV4SecurityPolicyIds = GetPolicies(obj, spec, obj.Node, \
                                    "V4", "ingress", False)
    obj.EgV4SecurityPolicyIds = GetPolicies(obj, spec, obj.Node, \
                                    "V4", "egress", False)
    obj.IngV6SecurityPolicyIds = GetPolicies(obj, spec, obj.Node, \
                                    "V6", "ingress", False)
    obj.EgV6SecurityPolicyIds = GetPolicies(obj, spec, obj.Node, \
                                    "V6", "egress", False)
    ModifyPolicyDependency(obj, False)
    return

def IsInformRelatedObjs(node):
    if IsReconfigInProgress(node):
        return False
    return True

def IsAttribUpdated(obj, attr):
    if not obj.Precedent:
        return False
    val1 = getattr(obj, attr, None)
    val2 = getattr(obj.Precedent, attr, None)
    if val1 != val2:
        return True
    return False

def GetTagsFromMapping(Node):
    LmappingClient = EzAccessStore.GetConfigClient(ObjectTypes.LMAPPING)
    RmappingClient = EzAccessStore.GetConfigClient(ObjectTypes.RMAPPING)
    v4ltags = LmappingClient.GetLmappingV4Tags(Node)
    v6ltags = LmappingClient.GetLmappingV6Tags(Node)
    v4rtags = RmappingClient.GetRmappingV4Tags(Node)
    v6rtags = RmappingClient.GetRmappingV6Tags(Node)
    return v4ltags,v6ltags,v4rtags,v6rtags

def GetClientObject(objtype):
    return EzAccessStore.GetConfigClient(objtype)

def GetConfigObjectById(objtype, id):
    dutNode = EzAccessStore.GetDUTNode()
    objClient = GetClientObject(objtype)
    return objClient.GetObjectByKey(dutNode, id)
