daemon: removed nem map from individual emane networks, all nems are stored and generated from the emane manager
This commit is contained in:
		
							parent
							
								
									fcda1f9f14
								
							
						
					
					
						commit
						5cc4d92760
					
				
					 9 changed files with 64 additions and 89 deletions
				
			
		| 
						 | 
				
			
			@ -491,10 +491,13 @@ def iface_to_proto(node_id: int, iface: CoreInterface) -> core_pb2.Interface:
 | 
			
		|||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_nem_id(node: CoreNode, iface_id: int, context: ServicerContext) -> int:
 | 
			
		||||
def get_nem_id(
 | 
			
		||||
    session: Session, node: CoreNode, iface_id: int, context: ServicerContext
 | 
			
		||||
) -> int:
 | 
			
		||||
    """
 | 
			
		||||
    Get nem id for a given node and interface id.
 | 
			
		||||
 | 
			
		||||
    :param session: session node belongs to
 | 
			
		||||
    :param node: node to get nem id for
 | 
			
		||||
    :param iface_id: id of interface on node to get nem id for
 | 
			
		||||
    :param context: request context
 | 
			
		||||
| 
						 | 
				
			
			@ -508,7 +511,7 @@ def get_nem_id(node: CoreNode, iface_id: int, context: ServicerContext) -> int:
 | 
			
		|||
    if not isinstance(net, EmaneNet):
 | 
			
		||||
        message = f"{node.name} interface {iface_id} is not an EMANE network"
 | 
			
		||||
        context.abort(grpc.StatusCode.INVALID_ARGUMENT, message)
 | 
			
		||||
    nem_id = net.getnemid(iface)
 | 
			
		||||
    nem_id = session.emane.get_nem_id(iface)
 | 
			
		||||
    if nem_id is None:
 | 
			
		||||
        message = f"{node.name} interface {iface_id} nem id does not exist"
 | 
			
		||||
        context.abort(grpc.StatusCode.INVALID_ARGUMENT, message)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1551,29 +1551,29 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
 | 
			
		|||
        logging.debug("emane link: %s", request)
 | 
			
		||||
        session = self.get_session(request.session_id, context)
 | 
			
		||||
        nem1 = request.nem1
 | 
			
		||||
        emane1, iface = session.emane.nemlookup(nem1)
 | 
			
		||||
        if not emane1 or not iface:
 | 
			
		||||
        iface1 = session.emane.get_iface(nem1)
 | 
			
		||||
        if not iface1:
 | 
			
		||||
            context.abort(grpc.StatusCode.NOT_FOUND, f"nem one {nem1} not found")
 | 
			
		||||
        node1 = iface.node
 | 
			
		||||
        node1 = iface1.node
 | 
			
		||||
 | 
			
		||||
        nem2 = request.nem2
 | 
			
		||||
        emane2, iface = session.emane.nemlookup(nem2)
 | 
			
		||||
        if not emane2 or not iface:
 | 
			
		||||
        iface2 = session.emane.get_iface(nem2)
 | 
			
		||||
        if not iface2:
 | 
			
		||||
            context.abort(grpc.StatusCode.NOT_FOUND, f"nem two {nem2} not found")
 | 
			
		||||
        node2 = iface.node
 | 
			
		||||
        node2 = iface2.node
 | 
			
		||||
 | 
			
		||||
        if emane1.id == emane2.id:
 | 
			
		||||
        if iface1.net == iface2.net:
 | 
			
		||||
            if request.linked:
 | 
			
		||||
                flag = MessageFlags.ADD
 | 
			
		||||
            else:
 | 
			
		||||
                flag = MessageFlags.DELETE
 | 
			
		||||
            color = session.get_link_color(emane1.id)
 | 
			
		||||
            color = session.get_link_color(iface1.net.id)
 | 
			
		||||
            link = LinkData(
 | 
			
		||||
                message_type=flag,
 | 
			
		||||
                type=LinkTypes.WIRELESS,
 | 
			
		||||
                node1_id=node1.id,
 | 
			
		||||
                node2_id=node2.id,
 | 
			
		||||
                network_id=emane1.id,
 | 
			
		||||
                network_id=iface1.net.id,
 | 
			
		||||
                color=color,
 | 
			
		||||
            )
 | 
			
		||||
            session.broadcast_link(link)
 | 
			
		||||
| 
						 | 
				
			
			@ -1796,8 +1796,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
 | 
			
		|||
        for request in request_iterator:
 | 
			
		||||
            session = self.get_session(request.session_id, context)
 | 
			
		||||
            node1 = self.get_node(session, request.node1_id, context, CoreNode)
 | 
			
		||||
            nem1 = grpcutils.get_nem_id(node1, request.iface1_id, context)
 | 
			
		||||
            nem1 = grpcutils.get_nem_id(session, node1, request.iface1_id, context)
 | 
			
		||||
            node2 = self.get_node(session, request.node2_id, context, CoreNode)
 | 
			
		||||
            nem2 = grpcutils.get_nem_id(node2, request.iface2_id, context)
 | 
			
		||||
            nem2 = grpcutils.get_nem_id(session, node2, request.iface2_id, context)
 | 
			
		||||
            session.emane.publish_pathloss(nem1, nem2, request.rx1, request.rx2)
 | 
			
		||||
        return EmanePathlossesResponse()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,6 @@ from lxml import etree
 | 
			
		|||
 | 
			
		||||
from core.config import ConfigGroup, Configuration
 | 
			
		||||
from core.emane import emanemanifest, emanemodel
 | 
			
		||||
from core.emane.nodes import EmaneNet
 | 
			
		||||
from core.emulator.data import LinkOptions
 | 
			
		||||
from core.nodes.interface import CoreInterface
 | 
			
		||||
from core.xml import emanexml
 | 
			
		||||
| 
						 | 
				
			
			@ -124,12 +123,11 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
 | 
			
		|||
        # TODO: batch these into multiple events per transmission
 | 
			
		||||
        # TODO: may want to split out seconds portion of delay and jitter
 | 
			
		||||
        event = CommEffectEvent()
 | 
			
		||||
        emane_node = self.session.get_node(self.id, EmaneNet)
 | 
			
		||||
        nemid = emane_node.getnemid(iface)
 | 
			
		||||
        nemid2 = emane_node.getnemid(iface2)
 | 
			
		||||
        nem1 = self.session.emane.get_nem_id(iface)
 | 
			
		||||
        nem2 = self.session.emane.get_nem_id(iface2)
 | 
			
		||||
        logging.info("sending comm effect event")
 | 
			
		||||
        event.append(
 | 
			
		||||
            nemid,
 | 
			
		||||
            nem1,
 | 
			
		||||
            latency=convert_none(options.delay),
 | 
			
		||||
            jitter=convert_none(options.jitter),
 | 
			
		||||
            loss=convert_none(options.loss),
 | 
			
		||||
| 
						 | 
				
			
			@ -137,4 +135,4 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
 | 
			
		|||
            unicast=int(convert_none(options.bandwidth)),
 | 
			
		||||
            broadcast=int(convert_none(options.bandwidth)),
 | 
			
		||||
        )
 | 
			
		||||
        service.publish(nemid2, event)
 | 
			
		||||
        service.publish(nem2, event)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -90,7 +90,8 @@ class EmaneManager(ModelManager):
 | 
			
		|||
        """
 | 
			
		||||
        super().__init__()
 | 
			
		||||
        self.session: "Session" = session
 | 
			
		||||
        self.nems: Dict[int, CoreInterface] = {}
 | 
			
		||||
        self.nems_to_ifaces: Dict[int, CoreInterface] = {}
 | 
			
		||||
        self.ifaces_to_nems: Dict[CoreInterface, int] = {}
 | 
			
		||||
        self._emane_nets: Dict[int, EmaneNet] = {}
 | 
			
		||||
        self._emane_node_lock: threading.Lock = threading.Lock()
 | 
			
		||||
        # port numbers are allocated from these counters
 | 
			
		||||
| 
						 | 
				
			
			@ -117,7 +118,7 @@ class EmaneManager(ModelManager):
 | 
			
		|||
 | 
			
		||||
    def next_nem_id(self) -> int:
 | 
			
		||||
        nem_id = int(self.get_config("nem_id_start"))
 | 
			
		||||
        while nem_id in self.nems:
 | 
			
		||||
        while nem_id in self.nems_to_ifaces:
 | 
			
		||||
            nem_id += 1
 | 
			
		||||
        return nem_id
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -363,7 +364,7 @@ class EmaneManager(ModelManager):
 | 
			
		|||
            0, remove=False, conf_required=False
 | 
			
		||||
        )
 | 
			
		||||
        nem_id = self.next_nem_id()
 | 
			
		||||
        self.nems[nem_id] = iface
 | 
			
		||||
        self.set_nem(nem_id, iface)
 | 
			
		||||
        self.write_nem(iface, nem_id)
 | 
			
		||||
        emanexml.build_platform_xml(self, control_net, emane_net, iface, nem_id)
 | 
			
		||||
        config = self.get_iface_config(emane_net, iface)
 | 
			
		||||
| 
						 | 
				
			
			@ -371,6 +372,18 @@ class EmaneManager(ModelManager):
 | 
			
		|||
        self.start_daemon(iface)
 | 
			
		||||
        self.install_iface(emane_net, iface)
 | 
			
		||||
 | 
			
		||||
    def set_nem(self, nem_id: int, iface: CoreInterface) -> None:
 | 
			
		||||
        if nem_id in self.nems_to_ifaces:
 | 
			
		||||
            raise CoreError(f"adding duplicate nem: {nem_id}")
 | 
			
		||||
        self.nems_to_ifaces[nem_id] = iface
 | 
			
		||||
        self.ifaces_to_nems[iface] = nem_id
 | 
			
		||||
 | 
			
		||||
    def get_iface(self, nem_id: int) -> Optional[CoreInterface]:
 | 
			
		||||
        return self.nems_to_ifaces.get(nem_id)
 | 
			
		||||
 | 
			
		||||
    def get_nem_id(self, iface: CoreInterface) -> Optional[int]:
 | 
			
		||||
        return self.ifaces_to_nems.get(iface)
 | 
			
		||||
 | 
			
		||||
    def write_nem(self, iface: CoreInterface, nem_id: int) -> None:
 | 
			
		||||
        path = os.path.join(self.session.session_dir, "emane_nems")
 | 
			
		||||
        try:
 | 
			
		||||
| 
						 | 
				
			
			@ -405,7 +418,8 @@ class EmaneManager(ModelManager):
 | 
			
		|||
        """
 | 
			
		||||
        with self._emane_node_lock:
 | 
			
		||||
            self._emane_nets.clear()
 | 
			
		||||
            self.nems.clear()
 | 
			
		||||
            self.nems_to_ifaces.clear()
 | 
			
		||||
            self.ifaces_to_nems.clear()
 | 
			
		||||
 | 
			
		||||
    def shutdown(self) -> None:
 | 
			
		||||
        """
 | 
			
		||||
| 
						 | 
				
			
			@ -448,42 +462,29 @@ class EmaneManager(ModelManager):
 | 
			
		|||
            model_class = self.models[model_name]
 | 
			
		||||
            emane_net.setmodel(model_class, config)
 | 
			
		||||
 | 
			
		||||
    def nemlookup(self, nemid) -> Tuple[Optional[EmaneNet], Optional[CoreInterface]]:
 | 
			
		||||
        """
 | 
			
		||||
        Look for the given numerical NEM ID and return the first matching
 | 
			
		||||
        EMANE network and NEM interface.
 | 
			
		||||
        """
 | 
			
		||||
        emane_node = None
 | 
			
		||||
        iface = None
 | 
			
		||||
        for node_id in self._emane_nets:
 | 
			
		||||
            emane_node = self._emane_nets[node_id]
 | 
			
		||||
            iface = emane_node.get_nem_iface(nemid)
 | 
			
		||||
            if iface is not None:
 | 
			
		||||
                break
 | 
			
		||||
            else:
 | 
			
		||||
                emane_node = None
 | 
			
		||||
        return emane_node, iface
 | 
			
		||||
 | 
			
		||||
    def get_nem_link(
 | 
			
		||||
        self, nem1: int, nem2: int, flags: MessageFlags = MessageFlags.NONE
 | 
			
		||||
    ) -> Optional[LinkData]:
 | 
			
		||||
        emane1, iface = self.nemlookup(nem1)
 | 
			
		||||
        if not emane1 or not iface:
 | 
			
		||||
        iface1 = self.get_iface(nem1)
 | 
			
		||||
        if not iface1:
 | 
			
		||||
            logging.error("invalid nem: %s", nem1)
 | 
			
		||||
            return None
 | 
			
		||||
        node1 = iface.node
 | 
			
		||||
        emane2, iface = self.nemlookup(nem2)
 | 
			
		||||
        if not emane2 or not iface:
 | 
			
		||||
        node1 = iface1.node
 | 
			
		||||
        iface2 = self.get_iface(nem2)
 | 
			
		||||
        if not iface2:
 | 
			
		||||
            logging.error("invalid nem: %s", nem2)
 | 
			
		||||
            return None
 | 
			
		||||
        node2 = iface.node
 | 
			
		||||
        color = self.session.get_link_color(emane1.id)
 | 
			
		||||
        node2 = iface2.node
 | 
			
		||||
        if iface1.net != iface2.net:
 | 
			
		||||
            return None
 | 
			
		||||
        emane_net = iface1.net
 | 
			
		||||
        color = self.session.get_link_color(emane_net.id)
 | 
			
		||||
        return LinkData(
 | 
			
		||||
            message_type=flags,
 | 
			
		||||
            type=LinkTypes.WIRELESS,
 | 
			
		||||
            node1_id=node1.id,
 | 
			
		||||
            node2_id=node2.id,
 | 
			
		||||
            network_id=emane1.id,
 | 
			
		||||
            network_id=emane_net.id,
 | 
			
		||||
            color=color,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -728,7 +729,7 @@ class EmaneManager(ModelManager):
 | 
			
		|||
        Returns True if successfully parsed and a Node Message was sent.
 | 
			
		||||
        """
 | 
			
		||||
        # convert nemid to node number
 | 
			
		||||
        _emanenode, iface = self.nemlookup(nemid)
 | 
			
		||||
        iface = self.get_iface(nemid)
 | 
			
		||||
        if iface is None:
 | 
			
		||||
            logging.info("location event for unknown NEM %s", nemid)
 | 
			
		||||
            return False
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -52,7 +52,6 @@ class EmaneNet(CoreNetworkBase):
 | 
			
		|||
    ) -> None:
 | 
			
		||||
        super().__init__(session, _id, name, server)
 | 
			
		||||
        self.conf: str = ""
 | 
			
		||||
        self.nemidmap: Dict[CoreInterface, int] = {}
 | 
			
		||||
        self.model: "OptionalEmaneModel" = None
 | 
			
		||||
        self.mobility: Optional[WayPointMobility] = None
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -105,32 +104,6 @@ class EmaneNet(CoreNetworkBase):
 | 
			
		|||
            self.mobility = model(session=self.session, _id=self.id)
 | 
			
		||||
            self.mobility.update_config(config)
 | 
			
		||||
 | 
			
		||||
    def setnemid(self, iface: CoreInterface, nemid: int) -> None:
 | 
			
		||||
        """
 | 
			
		||||
        Record an interface to numerical ID mapping. The Emane controller
 | 
			
		||||
        object manages and assigns these IDs for all NEMs.
 | 
			
		||||
        """
 | 
			
		||||
        self.nemidmap[iface] = nemid
 | 
			
		||||
 | 
			
		||||
    def getnemid(self, iface: CoreInterface) -> Optional[int]:
 | 
			
		||||
        """
 | 
			
		||||
        Given an interface, return its numerical ID.
 | 
			
		||||
        """
 | 
			
		||||
        if iface not in self.nemidmap:
 | 
			
		||||
            return None
 | 
			
		||||
        else:
 | 
			
		||||
            return self.nemidmap[iface]
 | 
			
		||||
 | 
			
		||||
    def get_nem_iface(self, nemid: int) -> Optional[CoreInterface]:
 | 
			
		||||
        """
 | 
			
		||||
        Given a numerical NEM ID, return its interface. This returns the
 | 
			
		||||
        first interface that matches the given NEM ID.
 | 
			
		||||
        """
 | 
			
		||||
        for iface in self.nemidmap:
 | 
			
		||||
            if self.nemidmap[iface] == nemid:
 | 
			
		||||
                return iface
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    def _nem_position(
 | 
			
		||||
        self, iface: CoreInterface
 | 
			
		||||
    ) -> Optional[Tuple[int, float, float, float]]:
 | 
			
		||||
| 
						 | 
				
			
			@ -140,9 +113,9 @@ class EmaneNet(CoreNetworkBase):
 | 
			
		|||
        :param iface: interface to get nem emane position for
 | 
			
		||||
        :return: nem position tuple, None otherwise
 | 
			
		||||
        """
 | 
			
		||||
        nemid = self.getnemid(iface)
 | 
			
		||||
        nem_id = self.session.emane.get_nem_id(iface)
 | 
			
		||||
        ifname = iface.localname
 | 
			
		||||
        if nemid is None:
 | 
			
		||||
        if nem_id is None:
 | 
			
		||||
            logging.info("nemid for %s is unknown", ifname)
 | 
			
		||||
            return
 | 
			
		||||
        node = iface.node
 | 
			
		||||
| 
						 | 
				
			
			@ -153,7 +126,7 @@ class EmaneNet(CoreNetworkBase):
 | 
			
		|||
        node.position.set_geo(lon, lat, alt)
 | 
			
		||||
        # altitude must be an integer or warning is printed
 | 
			
		||||
        alt = int(round(alt))
 | 
			
		||||
        return nemid, lon, lat, alt
 | 
			
		||||
        return nem_id, lon, lat, alt
 | 
			
		||||
 | 
			
		||||
    def setnemposition(self, iface: CoreInterface) -> None:
 | 
			
		||||
        """
 | 
			
		||||
| 
						 | 
				
			
			@ -164,7 +137,6 @@ class EmaneNet(CoreNetworkBase):
 | 
			
		|||
        if self.session.emane.service is None:
 | 
			
		||||
            logging.info("position service not available")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        position = self._nem_position(iface)
 | 
			
		||||
        if position:
 | 
			
		||||
            nemid, lon, lat, alt = position
 | 
			
		||||
| 
						 | 
				
			
			@ -195,9 +167,12 @@ class EmaneNet(CoreNetworkBase):
 | 
			
		|||
 | 
			
		||||
    def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
 | 
			
		||||
        links = super().links(flags)
 | 
			
		||||
        # gather current emane links
 | 
			
		||||
        nem_ids = set(self.nemidmap.values())
 | 
			
		||||
        emane_manager = self.session.emane
 | 
			
		||||
        # gather current emane links
 | 
			
		||||
        nem_ids = set()
 | 
			
		||||
        for iface in self.get_ifaces():
 | 
			
		||||
            nem_id = emane_manager.get_nem_id(iface)
 | 
			
		||||
            nem_ids.add(nem_id)
 | 
			
		||||
        emane_links = emane_manager.link_monitor.links
 | 
			
		||||
        considered = set()
 | 
			
		||||
        for link_key in emane_links:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,7 +28,7 @@ class EmaneTransportService(CoreService):
 | 
			
		|||
            emane_net = iface.net
 | 
			
		||||
            config = emane_manager.get_iface_config(emane_net, iface)
 | 
			
		||||
            if emanexml.is_external(config):
 | 
			
		||||
                nem_id = emane_net.getnemid(iface)
 | 
			
		||||
                nem_id = emane_manager.get_nem_id(iface)
 | 
			
		||||
                cfg += f"emanegentransportxml {iface.name}-platform.xml\n"
 | 
			
		||||
                cfg += f"emanetransportd -r -l 0 -d transportdaemon{nem_id}.xml\n"
 | 
			
		||||
        return cfg
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -501,8 +501,8 @@ class CoreXmlWriter:
 | 
			
		|||
            iface = node.get_iface(iface_data.id)
 | 
			
		||||
            # check if emane interface
 | 
			
		||||
            if isinstance(iface.net, EmaneNet):
 | 
			
		||||
                nem = iface.net.getnemid(iface)
 | 
			
		||||
                add_attribute(iface_element, "nem", nem)
 | 
			
		||||
                nem_id = self.session.emane.get_nem_id(iface)
 | 
			
		||||
                add_attribute(iface_element, "nem", nem_id)
 | 
			
		||||
        add_attribute(iface_element, "id", iface_data.id)
 | 
			
		||||
        add_attribute(iface_element, "name", iface_data.name)
 | 
			
		||||
        add_attribute(iface_element, "mac", iface_data.mac)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,6 @@ from core import utils
 | 
			
		|||
from core.emane.nodes import EmaneNet
 | 
			
		||||
from core.executables import IP
 | 
			
		||||
from core.nodes.base import CoreNodeBase, NodeBase
 | 
			
		||||
from core.nodes.interface import CoreInterface
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from core.emulator.session import Session
 | 
			
		||||
| 
						 | 
				
			
			@ -38,11 +37,10 @@ def add_mapping(parent_element: etree.Element, maptype: str, mapref: str) -> Non
 | 
			
		|||
 | 
			
		||||
def add_emane_iface(
 | 
			
		||||
    host_element: etree.Element,
 | 
			
		||||
    iface: CoreInterface,
 | 
			
		||||
    nem_id: int,
 | 
			
		||||
    platform_name: str = "p1",
 | 
			
		||||
    transport_name: str = "t1",
 | 
			
		||||
) -> etree.Element:
 | 
			
		||||
    nem_id = iface.net.nemidmap[iface]
 | 
			
		||||
    host_id = host_element.get("id")
 | 
			
		||||
 | 
			
		||||
    # platform data
 | 
			
		||||
| 
						 | 
				
			
			@ -158,7 +156,8 @@ class CoreXmlDeployment:
 | 
			
		|||
        for iface in node.get_ifaces():
 | 
			
		||||
            emane_element = None
 | 
			
		||||
            if isinstance(iface.net, EmaneNet):
 | 
			
		||||
                emane_element = add_emane_iface(host_element, iface)
 | 
			
		||||
                nem_id = self.session.emane.get_nem_id(iface)
 | 
			
		||||
                emane_element = add_emane_iface(host_element, nem_id)
 | 
			
		||||
 | 
			
		||||
            parent_element = host_element
 | 
			
		||||
            if emane_element is not None:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -193,7 +193,6 @@ def build_platform_xml(
 | 
			
		|||
            value = emane_manager.get_config(name)
 | 
			
		||||
        add_param(platform_element, name, value)
 | 
			
		||||
    platform_element.append(nem_element)
 | 
			
		||||
    emane_net.setnemid(iface, nem_id)
 | 
			
		||||
    mac = _MAC_PREFIX + ":00:00:"
 | 
			
		||||
    mac += f"{(nem_id >> 8) & 0xFF:02X}:{nem_id & 0xFF:02X}"
 | 
			
		||||
    iface.set_mac(mac)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue