diff --git a/CHANGELOG.md b/CHANGELOG.md index ce95c5fd..7a85ee34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## 2022-03-21 CORE 8.2.0 + +* core-gui + * improved failed starts to trigger runtime to allow node investigation +* core-daemon + * improved default service loading to use a full import path + * updated session instantiation to always set to a runtime state +* core-cli + * \#672 - fixed xml loading + * \#578 - restored json flag and added geo output to session overview +* Documentation + * updated emane example and documentation + * improved table markdown + ## 2022-02-18 CORE 8.1.0 * Installation diff --git a/configure.ac b/configure.ac index 113782b5..ae652051 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ # Process this file with autoconf to produce a configure script. # this defines the CORE version number, must be static for AC_INIT -AC_INIT(core, 8.1.0) +AC_INIT(core, 8.2.0) # autoconf and automake initialization AC_CONFIG_SRCDIR([netns/version.h.in]) diff --git a/daemon/core/api/grpc/client.py b/daemon/core/api/grpc/client.py index e2e1e729..8e7b37c3 100644 --- a/daemon/core/api/grpc/client.py +++ b/daemon/core/api/grpc/client.py @@ -16,7 +16,7 @@ from core.api.grpc.configservices_pb2 import ( GetConfigServiceDefaultsRequest, GetNodeConfigServiceRequest, ) -from core.api.grpc.core_pb2 import ExecuteScriptRequest, GetConfigRequest +from core.api.grpc.core_pb2 import ExecuteScriptRequest, GetConfigRequest, LinkedRequest from core.api.grpc.emane_pb2 import ( EmaneLinkRequest, GetEmaneEventChannelRequest, @@ -1049,6 +1049,36 @@ class CoreGrpcClient: """ self.stub.EmanePathlosses(streamer.iter()) + def linked( + self, + session_id: int, + node1_id: int, + node2_id: int, + iface1_id: int, + iface2_id: int, + linked: bool, + ) -> None: + """ + Link or unlink an existing core wired link. + + :param session_id: session containing the link + :param node1_id: first node in link + :param node2_id: second node in link + :param iface1_id: node1 interface + :param iface2_id: node2 interface + :param linked: True to connect link, False to disconnect + :return: nothing + """ + request = LinkedRequest( + session_id=session_id, + node1_id=node1_id, + node2_id=node2_id, + iface1_id=iface1_id, + iface2_id=iface2_id, + linked=linked, + ) + self.stub.Linked(request) + def connect(self) -> None: """ Open connection to server, must be closed manually. diff --git a/daemon/core/api/grpc/events.py b/daemon/core/api/grpc/events.py index b319a978..b15862ac 100644 --- a/daemon/core/api/grpc/events.py +++ b/daemon/core/api/grpc/events.py @@ -3,7 +3,7 @@ from queue import Empty, Queue from typing import Iterable, Optional from core.api.grpc import core_pb2 -from core.api.grpc.grpcutils import convert_link +from core.api.grpc.grpcutils import convert_link_data from core.emulator.data import ( ConfigData, EventData, @@ -51,7 +51,7 @@ def handle_link_event(link_data: LinkData) -> core_pb2.Event: :param link_data: link data :return: link event that has message type and link information """ - link = convert_link(link_data) + link = convert_link_data(link_data) message_type = link_data.message_type.value link_event = core_pb2.LinkEvent(message_type=message_type, link=link) return core_pb2.Event(link_event=link_event, source=link_data.source) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index c585a135..e2cba7e9 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -1,7 +1,7 @@ import logging import time from pathlib import Path -from typing import Any, Dict, List, Tuple, Type, Union +from typing import Any, Dict, List, Optional, Tuple, Type, Union import grpc from grpc import ServicerContext @@ -20,6 +20,7 @@ from core.config import ConfigurableOptions from core.emane.nodes import EmaneNet from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions from core.emulator.enumerations import LinkTypes, NodeTypes +from core.emulator.links import CoreLink from core.emulator.session import Session from core.errors import CoreError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility @@ -27,7 +28,7 @@ from core.nodes.base import CoreNode, CoreNodeBase, NodeBase from core.nodes.docker import DockerNode from core.nodes.interface import CoreInterface from core.nodes.lxd import LxcNode -from core.nodes.network import CtrlNet, PtpNet, WlanNode +from core.nodes.network import CoreNetwork, CtrlNet, PtpNet, WlanNode from core.services.coreservices import CoreService logger = logging.getLogger(__name__) @@ -110,7 +111,7 @@ def link_iface(iface_proto: core_pb2.Interface) -> InterfaceData: def add_link_data( link_proto: core_pb2.Link -) -> Tuple[InterfaceData, InterfaceData, LinkOptions, LinkTypes]: +) -> Tuple[InterfaceData, InterfaceData, LinkOptions]: """ Convert link proto to link interfaces and options data. @@ -119,7 +120,6 @@ def add_link_data( """ iface1_data = link_iface(link_proto.iface1) iface2_data = link_iface(link_proto.iface2) - link_type = LinkTypes(link_proto.type) options = LinkOptions() options_proto = link_proto.options if options_proto: @@ -134,7 +134,7 @@ def add_link_data( options.buffer = options_proto.buffer options.unidirectional = options_proto.unidirectional options.key = options_proto.key - return iface1_data, iface2_data, options, link_type + return iface1_data, iface2_data, options def create_nodes( @@ -174,8 +174,8 @@ def create_links( for link_proto in link_protos: node1_id = link_proto.node1_id node2_id = link_proto.node2_id - iface1, iface2, options, link_type = add_link_data(link_proto) - args = (node1_id, node2_id, iface1, iface2, options, link_type) + iface1, iface2, options = add_link_data(link_proto) + args = (node1_id, node2_id, iface1, iface2, options) funcs.append((session.add_link, args, {})) start = time.monotonic() results, exceptions = utils.threadpool(funcs) @@ -198,8 +198,8 @@ def edit_links( for link_proto in link_protos: node1_id = link_proto.node1_id node2_id = link_proto.node2_id - iface1, iface2, options, link_type = add_link_data(link_proto) - args = (node1_id, node2_id, iface1.id, iface2.id, options, link_type) + iface1, iface2, options = add_link_data(link_proto) + args = (node1_id, node2_id, iface1.id, iface2.id, options) funcs.append((session.update_link, args, {})) start = time.monotonic() results, exceptions = utils.threadpool(funcs) @@ -344,61 +344,84 @@ def get_node_proto( ) -def get_links(node: NodeBase): +def get_links(session: Session, node: NodeBase) -> List[core_pb2.Link]: """ Retrieve a list of links for grpc to use. + :param session: session to get links for node :param node: node to get links from :return: protobuf links """ + link_protos = [] + for core_link in session.link_manager.node_links(node): + link_protos.extend(convert_core_link(core_link)) + if isinstance(node, (WlanNode, EmaneNet)): + for link_data in node.links(): + link_protos.append(convert_link_data(link_data)) + return link_protos + + +def convert_iface(iface: CoreInterface) -> core_pb2.Interface: + """ + Convert interface to protobuf. + + :param iface: interface to convert + :return: protobuf interface + """ + if isinstance(iface.node, CoreNetwork): + return core_pb2.Interface(id=iface.id) + else: + ip4 = iface.get_ip4() + ip4_mask = ip4.prefixlen if ip4 else None + ip4 = str(ip4.ip) if ip4 else None + ip6 = iface.get_ip6() + ip6_mask = ip6.prefixlen if ip6 else None + ip6 = str(ip6.ip) if ip6 else None + mac = str(iface.mac) if iface.mac else None + return core_pb2.Interface( + id=iface.id, + name=iface.name, + mac=mac, + ip4=ip4, + ip4_mask=ip4_mask, + ip6=ip6, + ip6_mask=ip6_mask, + ) + + +def convert_core_link(core_link: CoreLink) -> List[core_pb2.Link]: + """ + Convert core link to protobuf data. + + :param core_link: core link to convert + :return: protobuf link data + """ links = [] - for link in node.links(): - link_proto = convert_link(link) - links.append(link_proto) + node1, iface1 = core_link.node1, core_link.iface1 + node2, iface2 = core_link.node2, core_link.iface2 + unidirectional = core_link.is_unidirectional() + link = convert_link(node1, iface1, node2, iface2, iface1.options, unidirectional) + links.append(link) + if unidirectional: + link = convert_link( + node2, iface2, node1, iface1, iface2.options, unidirectional + ) + links.append(link) return links -def convert_iface(iface_data: InterfaceData) -> core_pb2.Interface: - return core_pb2.Interface( - id=iface_data.id, - name=iface_data.name, - mac=iface_data.mac, - ip4=iface_data.ip4, - ip4_mask=iface_data.ip4_mask, - ip6=iface_data.ip6, - ip6_mask=iface_data.ip6_mask, - ) - - -def convert_link_options(options_data: LinkOptions) -> core_pb2.LinkOptions: - return core_pb2.LinkOptions( - jitter=options_data.jitter, - key=options_data.key, - mburst=options_data.mburst, - mer=options_data.mer, - loss=options_data.loss, - bandwidth=options_data.bandwidth, - burst=options_data.burst, - delay=options_data.delay, - dup=options_data.dup, - buffer=options_data.buffer, - unidirectional=options_data.unidirectional, - ) - - -def convert_link(link_data: LinkData) -> core_pb2.Link: +def convert_link_data(link_data: LinkData) -> core_pb2.Link: """ Convert link_data into core protobuf link. - :param link_data: link to convert :return: core protobuf Link """ iface1 = None if link_data.iface1 is not None: - iface1 = convert_iface(link_data.iface1) + iface1 = convert_iface_data(link_data.iface1) iface2 = None if link_data.iface2 is not None: - iface2 = convert_iface(link_data.iface2) + iface2 = convert_iface_data(link_data.iface2) options = convert_link_options(link_data.options) return core_pb2.Link( type=link_data.type.value, @@ -413,6 +436,89 @@ def convert_link(link_data: LinkData) -> core_pb2.Link: ) +def convert_iface_data(iface_data: InterfaceData) -> core_pb2.Interface: + """ + Convert interface data to protobuf. + + :param iface_data: interface data to convert + :return: interface protobuf + """ + return core_pb2.Interface( + id=iface_data.id, + name=iface_data.name, + mac=iface_data.mac, + ip4=iface_data.ip4, + ip4_mask=iface_data.ip4_mask, + ip6=iface_data.ip6, + ip6_mask=iface_data.ip6_mask, + ) + + +def convert_link_options(options: LinkOptions) -> core_pb2.LinkOptions: + """ + Convert link options to protobuf. + + :param options: link options to convert + :return: link options protobuf + """ + return core_pb2.LinkOptions( + jitter=options.jitter, + key=options.key, + mburst=options.mburst, + mer=options.mer, + loss=options.loss, + bandwidth=options.bandwidth, + burst=options.burst, + delay=options.delay, + dup=options.dup, + buffer=options.buffer, + unidirectional=options.unidirectional, + ) + + +def convert_link( + node1: NodeBase, + iface1: Optional[CoreInterface], + node2: NodeBase, + iface2: Optional[CoreInterface], + options: LinkOptions, + unidirectional: bool, +) -> core_pb2.Link: + """ + Convert link objects to link protobuf. + + :param node1: first node in link + :param iface1: node1 interface + :param node2: second node in link + :param iface2: node2 interface + :param options: link options + :param unidirectional: if this link is considered unidirectional + :return: protobuf link + """ + if iface1 is not None: + iface1 = convert_iface(iface1) + if iface2 is not None: + iface2 = convert_iface(iface2) + is_node1_wireless = isinstance(node1, (WlanNode, EmaneNet)) + is_node2_wireless = isinstance(node2, (WlanNode, EmaneNet)) + if not (is_node1_wireless or is_node2_wireless): + options = convert_link_options(options) + options.unidirectional = unidirectional + else: + options = None + return core_pb2.Link( + type=LinkTypes.WIRED.value, + node1_id=node1.id, + node2_id=node2.id, + iface1=iface1, + iface2=iface2, + options=options, + network_id=None, + label=None, + color=None, + ) + + def get_net_stats() -> Dict[str, Dict]: """ Retrieve status about the current interfaces in the system @@ -490,39 +596,13 @@ def get_service_configuration(service: CoreService) -> NodeServiceData: ) -def iface_to_data(iface: CoreInterface) -> InterfaceData: - ip4 = iface.get_ip4() - ip4_addr = str(ip4.ip) if ip4 else None - ip4_mask = ip4.prefixlen if ip4 else None - ip6 = iface.get_ip6() - ip6_addr = str(ip6.ip) if ip6 else None - ip6_mask = ip6.prefixlen if ip6 else None - return InterfaceData( - id=iface.node_id, - name=iface.name, - mac=str(iface.mac), - ip4=ip4_addr, - ip4_mask=ip4_mask, - ip6=ip6_addr, - ip6_mask=ip6_mask, - ) - - -def iface_to_proto(node_id: int, iface: CoreInterface) -> core_pb2.Interface: +def iface_to_proto(iface: CoreInterface) -> core_pb2.Interface: """ Convenience for converting a core interface to the protobuf representation. - :param node_id: id of node to convert interface for :param iface: interface to convert :return: interface proto """ - if iface.node and iface.node.id == node_id: - _id = iface.node_id - else: - _id = iface.net_id - net_id = iface.net.id if iface.net else None - node_id = iface.node.id if iface.node else None - net2_id = iface.othernet.id if iface.othernet else None ip4_net = iface.get_ip4() ip4 = str(ip4_net.ip) if ip4_net else None ip4_mask = ip4_net.prefixlen if ip4_net else None @@ -531,10 +611,7 @@ def iface_to_proto(node_id: int, iface: CoreInterface) -> core_pb2.Interface: ip6_mask = ip6_net.prefixlen if ip6_net else None mac = str(iface.mac) if iface.mac else None return core_pb2.Interface( - id=_id, - net_id=net_id, - net2_id=net2_id, - node_id=node_id, + id=iface.id, name=iface.name, mac=mac, mtu=iface.mtu, @@ -574,6 +651,12 @@ def get_nem_id( def get_emane_model_configs_dict(session: Session) -> Dict[int, List[NodeEmaneConfig]]: + """ + Get emane model configuration protobuf data. + + :param session: session to get emane model configuration for + :return: dict of emane model protobuf configurations + """ configs = {} for _id, model_configs in session.emane.node_configs.items(): for model_name in model_configs: @@ -591,6 +674,12 @@ def get_emane_model_configs_dict(session: Session) -> Dict[int, List[NodeEmaneCo def get_hooks(session: Session) -> List[core_pb2.Hook]: + """ + Retrieve hook protobuf data for a session. + + :param session: session to get hooks for + :return: list of hook protobufs + """ hooks = [] for state in session.hooks: state_hooks = session.hooks[state] @@ -601,6 +690,12 @@ def get_hooks(session: Session) -> List[core_pb2.Hook]: def get_default_services(session: Session) -> List[ServiceDefaults]: + """ + Retrieve the default service sets for a given session. + + :param session: session to get default service sets for + :return: list of default service sets + """ default_services = [] for name, services in session.services.default_services.items(): default_service = ServiceDefaults(node_type=name, services=services) @@ -611,6 +706,14 @@ def get_default_services(session: Session) -> List[ServiceDefaults]: def get_mobility_node( session: Session, node_id: int, context: ServicerContext ) -> Union[WlanNode, EmaneNet]: + """ + Get mobility node. + + :param session: session to get node from + :param node_id: id of node to get + :param context: grpc context + :return: wlan or emane node + """ try: return session.get_node(node_id, WlanNode) except CoreError: @@ -621,17 +724,26 @@ def get_mobility_node( def convert_session(session: Session) -> wrappers.Session: - links = [] - nodes = [] + """ + Convert session to its wrapped version. + + :param session: session to convert + :return: wrapped session data + """ emane_configs = get_emane_model_configs_dict(session) + nodes = [] + links = [] for _id in session.nodes: node = session.nodes[_id] if not isinstance(node, (PtpNet, CtrlNet)): node_emane_configs = emane_configs.get(node.id, []) node_proto = get_node_proto(session, node, node_emane_configs) nodes.append(node_proto) - node_links = get_links(node) - links.extend(node_links) + if isinstance(node, (WlanNode, EmaneNet)): + for link_data in node.links(): + links.append(convert_link_data(link_data)) + for core_link in session.link_manager.links(): + links.extend(convert_core_link(core_link)) default_services = get_default_services(session) x, y, z = session.location.refxyz lat, lon, alt = session.location.refgeo @@ -665,6 +777,15 @@ def convert_session(session: Session) -> wrappers.Session: def configure_node( session: Session, node: core_pb2.Node, core_node: NodeBase, context: ServicerContext ) -> None: + """ + Configure a node using all provided protobuf data. + + :param session: session for node + :param node: node protobuf data + :param core_node: session node + :param context: grpc context + :return: nothing + """ for emane_config in node.emane_configs: _id = utils.iface_config_id(node.id, emane_config.iface_id) config = {k: v.value for k, v in emane_config.config.items()} diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index de23dfa4..9605d1d4 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -26,7 +26,7 @@ from core.api.grpc.configservices_pb2 import ( GetNodeConfigServiceRequest, GetNodeConfigServiceResponse, ) -from core.api.grpc.core_pb2 import ExecuteScriptResponse +from core.api.grpc.core_pb2 import ExecuteScriptResponse, LinkedRequest, LinkedResponse from core.api.grpc.emane_pb2 import ( EmaneLinkRequest, EmaneLinkResponse, @@ -76,17 +76,12 @@ from core.configservice.base import ConfigServiceBootError from core.emane.modelmanager import EmaneModelManager from core.emulator.coreemu import CoreEmu from core.emulator.data import InterfaceData, LinkData, LinkOptions -from core.emulator.enumerations import ( - EventTypes, - ExceptionLevels, - LinkTypes, - MessageFlags, -) +from core.emulator.enumerations import EventTypes, ExceptionLevels, MessageFlags from core.emulator.session import NT, Session from core.errors import CoreCommandError, CoreError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility from core.nodes.base import CoreNode, NodeBase -from core.nodes.network import WlanNode +from core.nodes.network import CoreNetwork, WlanNode from core.services.coreservices import ServiceManager logger = logging.getLogger(__name__) @@ -565,12 +560,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): ifaces = [] for iface_id in node.ifaces: iface = node.ifaces[iface_id] - iface_proto = grpcutils.iface_to_proto(request.node_id, iface) + iface_proto = grpcutils.iface_to_proto(iface) ifaces.append(iface_proto) emane_configs = grpcutils.get_emane_model_configs_dict(session) node_emane_configs = emane_configs.get(node.id, []) node_proto = grpcutils.get_node_proto(session, node, node_emane_configs) - links = get_links(node) + links = get_links(session, node) return core_pb2.GetNodeResponse(node=node_proto, ifaces=ifaces, links=links) def MoveNode( @@ -706,18 +701,22 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): node2_id = request.link.node2_id self.get_node(session, node1_id, context, NodeBase) self.get_node(session, node2_id, context, NodeBase) - iface1_data, iface2_data, options, link_type = grpcutils.add_link_data( - request.link - ) + iface1_data, iface2_data, options = grpcutils.add_link_data(request.link) node1_iface, node2_iface = session.add_link( - node1_id, node2_id, iface1_data, iface2_data, options, link_type + node1_id, node2_id, iface1_data, iface2_data, options ) iface1_data = None if node1_iface: - iface1_data = grpcutils.iface_to_data(node1_iface) + if isinstance(node1_iface.node, CoreNetwork): + iface1_data = InterfaceData(id=node1_iface.id) + else: + iface1_data = node1_iface.get_data() iface2_data = None if node2_iface: - iface2_data = grpcutils.iface_to_data(node2_iface) + if isinstance(node2_iface.node, CoreNetwork): + iface2_data = InterfaceData(id=node2_iface.id) + else: + iface2_data = node2_iface.get_data() source = request.source if request.source else None link_data = LinkData( message_type=MessageFlags.ADD, @@ -732,9 +731,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): iface1_proto = None iface2_proto = None if node1_iface: - iface1_proto = grpcutils.iface_to_proto(node1_id, node1_iface) + iface1_proto = grpcutils.iface_to_proto(node1_iface) if node2_iface: - iface2_proto = grpcutils.iface_to_proto(node2_id, node2_iface) + iface2_proto = grpcutils.iface_to_proto(node2_iface) return core_pb2.AddLinkResponse( result=True, iface1=iface1_proto, iface2=iface2_proto ) @@ -1164,7 +1163,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): self, request: core_pb2.GetInterfacesRequest, context: ServicerContext ) -> core_pb2.GetInterfacesResponse: """ - Retrieve all the interfaces of the system including bridges, virtual ethernet, and loopback + Retrieve all the interfaces of the system including bridges, virtual ethernet, + and loopback. :param request: get-interfaces request :param context: context object @@ -1189,32 +1189,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): """ logger.debug("emane link: %s", request) session = self.get_session(request.session_id, context) - nem1 = request.nem1 - iface1 = session.emane.get_iface(nem1) - if not iface1: - context.abort(grpc.StatusCode.NOT_FOUND, f"nem one {nem1} not found") - node1 = iface1.node - - nem2 = request.nem2 - iface2 = session.emane.get_iface(nem2) - if not iface2: - context.abort(grpc.StatusCode.NOT_FOUND, f"nem two {nem2} not found") - node2 = iface2.node - - if iface1.net == iface2.net: - if request.linked: - flag = MessageFlags.ADD - else: - flag = MessageFlags.DELETE - 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=iface1.net.id, - color=color, - ) + flag = MessageFlags.ADD if request.linked else MessageFlags.DELETE + link = session.emane.get_nem_link(request.nem1, request.nem2, flag) + if link: session.broadcast_link(link) return EmaneLinkResponse(result=True) else: @@ -1303,15 +1280,18 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): if not isinstance(wlan.model, BasicRangeModel): context.abort( grpc.StatusCode.NOT_FOUND, - f"wlan node {request.wlan} does not using BasicRangeModel", + f"wlan node {request.wlan} is not using BasicRangeModel", ) node1 = self.get_node(session, request.node1_id, context, CoreNode) node2 = self.get_node(session, request.node2_id, context, CoreNode) node1_iface, node2_iface = None, None - for net, iface1, iface2 in node1.commonnets(node2): - if net == wlan: - node1_iface = iface1 - node2_iface = iface2 + for iface in node1.get_ifaces(control=False): + if iface.net == wlan: + node1_iface = iface + break + for iface in node2.get_ifaces(control=False): + if iface.net == wlan: + node2_iface = iface break result = False if node1_iface and node2_iface: @@ -1336,3 +1316,16 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): nem2 = grpcutils.get_nem_id(session, node2, request.iface2_id, context) session.emane.publish_pathloss(nem1, nem2, request.rx1, request.rx2) return EmanePathlossesResponse() + + def Linked( + self, request: LinkedRequest, context: ServicerContext + ) -> LinkedResponse: + session = self.get_session(request.session_id, context) + session.linked( + request.node1_id, + request.node2_id, + request.iface1_id, + request.iface2_id, + request.linked, + ) + return LinkedResponse() diff --git a/daemon/core/api/grpc/wrappers.py b/daemon/core/api/grpc/wrappers.py index c1449ff6..c4e081fa 100644 --- a/daemon/core/api/grpc/wrappers.py +++ b/daemon/core/api/grpc/wrappers.py @@ -637,6 +637,15 @@ class SessionSummary: dir=proto.dir, ) + def to_proto(self) -> core_pb2.SessionSummary: + return core_pb2.SessionSummary( + id=self.id, + state=self.state.value, + nodes=self.nodes, + file=self.file, + dir=self.dir, + ) + @dataclass class Hook: diff --git a/daemon/core/emane/emanemanager.py b/daemon/core/emane/emanemanager.py index 4ffed725..95ebf90e 100644 --- a/daemon/core/emane/emanemanager.py +++ b/daemon/core/emane/emanemanager.py @@ -12,12 +12,12 @@ from core import utils from core.emane.emanemodel import EmaneModel from core.emane.linkmonitor import EmaneLinkMonitor from core.emane.modelmanager import EmaneModelManager -from core.emane.nodes import EmaneNet +from core.emane.nodes import EmaneNet, TunTap from core.emulator.data import LinkData from core.emulator.enumerations import LinkTypes, MessageFlags, RegisterTlvs from core.errors import CoreCommandError, CoreError -from core.nodes.base import CoreNetworkBase, CoreNode, NodeBase -from core.nodes.interface import CoreInterface, TunTap +from core.nodes.base import CoreNode, NodeBase +from core.nodes.interface import CoreInterface from core.xml import emanexml logger = logging.getLogger(__name__) @@ -224,11 +224,9 @@ class EmaneManager: :return: net, node, or interface model configuration """ model_name = emane_net.model.name - config = None # try to retrieve interface specific configuration - if iface.node_id is not None: - key = utils.iface_config_id(iface.node.id, iface.node_id) - config = self.get_config(key, model_name, default=False) + key = utils.iface_config_id(iface.node.id, iface.id) + config = self.get_config(key, model_name, default=False) # attempt to retrieve node specific config, when iface config is not present if not config: config = self.get_config(iface.node.id, model_name, default=False) @@ -272,7 +270,8 @@ class EmaneManager: nodes = set() for emane_net in self._emane_nets.values(): for iface in emane_net.get_ifaces(): - nodes.add(iface.node) + if isinstance(iface.node, CoreNode): + nodes.add(iface.node) return nodes def setup(self) -> EmaneState: @@ -323,7 +322,7 @@ class EmaneManager: for emane_net, iface in self.get_ifaces(): self.start_iface(emane_net, iface) - def start_iface(self, emane_net: EmaneNet, iface: CoreInterface) -> None: + def start_iface(self, emane_net: EmaneNet, iface: TunTap) -> None: nem_id = self.next_nem_id(iface) nem_port = self.get_nem_port(iface) logger.info( @@ -338,7 +337,7 @@ class EmaneManager: self.start_daemon(iface) self.install_iface(iface, config) - def get_ifaces(self) -> List[Tuple[EmaneNet, CoreInterface]]: + def get_ifaces(self) -> List[Tuple[EmaneNet, TunTap]]: ifaces = [] for emane_net in self._emane_nets.values(): if not emane_net.model: @@ -352,8 +351,9 @@ class EmaneManager: iface.name, ) continue - ifaces.append((emane_net, iface)) - return sorted(ifaces, key=lambda x: (x[1].node.id, x[1].node_id)) + if isinstance(iface, TunTap): + ifaces.append((emane_net, iface)) + return sorted(ifaces, key=lambda x: (x[1].node.id, x[1].id)) def setup_control_channels( self, nem_id: int, iface: CoreInterface, config: Dict[str, str] @@ -622,9 +622,9 @@ class EmaneManager: args = f"{emanecmd} -f {log_file} {platform_xml}" node.host_cmd(args, cwd=self.session.directory) - def install_iface(self, iface: CoreInterface, config: Dict[str, str]) -> None: + def install_iface(self, iface: TunTap, config: Dict[str, str]) -> None: external = config.get("external", "0") - if isinstance(iface, TunTap) and external == "0": + if external == "0": iface.set_ips() # at this point we register location handlers for generating # EMANE location events @@ -732,9 +732,6 @@ class EmaneManager: self.session.broadcast_node(node) return True - def is_emane_net(self, net: Optional[CoreNetworkBase]) -> bool: - return isinstance(net, EmaneNet) - def emanerunning(self, node: CoreNode) -> bool: """ Return True if an EMANE process associated with the given node is running, diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index 76a93767..a32814e1 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -4,7 +4,8 @@ share the same MAC+PHY model. """ import logging -from typing import TYPE_CHECKING, Dict, List, Optional, Type +import time +from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Type from core.emulator.data import InterfaceData, LinkData, LinkOptions from core.emulator.distributed import DistributedServer @@ -15,7 +16,7 @@ from core.emulator.enumerations import ( NodeTypes, RegisterTlvs, ) -from core.errors import CoreError +from core.errors import CoreCommandError, CoreError from core.nodes.base import CoreNetworkBase, CoreNode from core.nodes.interface import CoreInterface @@ -39,6 +40,114 @@ except ImportError: logger.debug("compatible emane python bindings not installed") +class TunTap(CoreInterface): + """ + TUN/TAP virtual device in TAP mode + """ + + def __init__( + self, + _id: int, + name: str, + localname: str, + use_ovs: bool, + node: CoreNode = None, + server: "DistributedServer" = None, + ) -> None: + super().__init__(_id, name, localname, use_ovs, node=node, server=server) + self.node: CoreNode = node + + def startup(self) -> None: + """ + Startup logic for a tunnel tap. + + :return: nothing + """ + self.up = True + + def shutdown(self) -> None: + """ + Shutdown functionality for a tunnel tap. + + :return: nothing + """ + if not self.up: + return + self.up = False + + def waitfor( + self, func: Callable[[], int], attempts: int = 10, maxretrydelay: float = 0.25 + ) -> bool: + """ + Wait for func() to return zero with exponential backoff. + + :param func: function to wait for a result of zero + :param attempts: number of attempts to wait for a zero result + :param maxretrydelay: maximum retry delay + :return: True if wait succeeded, False otherwise + """ + delay = 0.01 + result = False + for i in range(1, attempts + 1): + r = func() + if r == 0: + result = True + break + msg = f"attempt {i} failed with nonzero exit status {r}" + if i < attempts + 1: + msg += ", retrying..." + logger.info(msg) + time.sleep(delay) + delay += delay + if delay > maxretrydelay: + delay = maxretrydelay + else: + msg += ", giving up" + logger.info(msg) + return result + + def nodedevexists(self) -> int: + """ + Checks if device exists. + + :return: 0 if device exists, 1 otherwise + """ + try: + self.node.node_net_client.device_show(self.name) + return 0 + except CoreCommandError: + return 1 + + def waitfordevicenode(self) -> None: + """ + Check for presence of a node device - tap device may not appear right away waits. + + :return: nothing + """ + logger.debug("waiting for device node: %s", self.name) + count = 0 + while True: + result = self.waitfor(self.nodedevexists) + if result: + break + should_retry = count < 5 + is_emane_running = self.node.session.emane.emanerunning(self.node) + if all([should_retry, is_emane_running]): + count += 1 + else: + raise RuntimeError("node device failed to exist") + + def set_ips(self) -> None: + """ + Set interface ip addresses. + + :return: nothing + """ + self.waitfordevicenode() + for ip in self.ips(): + self.node.node_net_client.create_address(self.name, str(ip)) + + class EmaneNet(CoreNetworkBase): """ EMANE node contains NEM configuration and causes connected nodes @@ -49,7 +158,6 @@ class EmaneNet(CoreNetworkBase): apitype: NodeTypes = NodeTypes.EMANE linktype: LinkTypes = LinkTypes.WIRED type: str = "wlan" - has_custom_iface: bool = True def __init__( self, @@ -73,14 +181,11 @@ class EmaneNet(CoreNetworkBase): return self.model.linkconfig(iface, options, iface2) - def config(self, conf: str) -> None: - self.conf = conf - def startup(self) -> None: - pass + self.up = True def shutdown(self) -> None: - pass + self.up = False def link(self, iface1: CoreInterface, iface2: CoreInterface) -> None: pass @@ -88,10 +193,13 @@ class EmaneNet(CoreNetworkBase): def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None: pass - def linknet(self, net: "CoreNetworkBase") -> CoreInterface: - raise CoreError("emane networks cannot be linked to other networks") - def updatemodel(self, config: Dict[str, str]) -> None: + """ + Update configuration for the current model. + + :param config: configuration to update model with + :return: nothing + """ if not self.model: raise CoreError(f"no model set to update for node({self.name})") logger.info("node(%s) updating model(%s): %s", self.id, self.model.name, config) @@ -111,7 +219,7 @@ class EmaneNet(CoreNetworkBase): self.mobility.update_config(config) def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: - links = super().links(flags) + links = [] emane_manager = self.session.emane # gather current emane links nem_ids = set() @@ -132,22 +240,44 @@ class EmaneNet(CoreNetworkBase): # ignore incomplete links if (nem2, nem1) not in emane_links: continue - link = emane_manager.get_nem_link(nem1, nem2) + link = emane_manager.get_nem_link(nem1, nem2, flags) if link: links.append(link) return links - def custom_iface(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface: - # TUN/TAP is not ready for addressing yet; the device may - # take some time to appear, and installing it into a - # namespace after it has been bound removes addressing; - # save addresses with the interface now - iface_id = node.newtuntap(iface_data.id, iface_data.name) - node.attachnet(iface_id, self) - iface = node.get_iface(iface_id) - iface.set_mac(iface_data.mac) - for ip in iface_data.get_ips(): - iface.add_ip(ip) + def create_tuntap(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface: + """ + Create a tuntap interface for the provided node. + + :param node: node to create tuntap interface for + :param iface_data: interface data to create interface with + :return: created tuntap interface + """ + with node.lock: + if iface_data.id is not None and iface_data.id in node.ifaces: + raise CoreError( + f"node({self.id}) interface({iface_data.id}) already exists" + ) + iface_id = ( + iface_data.id if iface_data.id is not None else node.next_iface_id() + ) + name = iface_data.name if iface_data.name is not None else f"eth{iface_id}" + session_id = self.session.short_session_id() + localname = f"tap{node.id}.{iface_id}.{session_id}" + iface = TunTap(iface_id, name, localname, self.session.use_ovs(), node=node) + if iface_data.mac: + iface.set_mac(iface_data.mac) + for ip in iface_data.get_ips(): + iface.add_ip(ip) + node.ifaces[iface_id] = iface + self.attach(iface) + if self.up: + iface.startup() if self.session.state == EventTypes.RUNTIME_STATE: self.session.emane.start_iface(self, iface) return iface + + def adopt_iface(self, iface: CoreInterface, name: str) -> None: + raise CoreError( + f"emane network({self.name}) do not support adopting interfaces" + ) diff --git a/daemon/core/emulator/distributed.py b/daemon/core/emulator/distributed.py index 2b4830ad..6faa852a 100644 --- a/daemon/core/emulator/distributed.py +++ b/daemon/core/emulator/distributed.py @@ -15,6 +15,7 @@ from fabric import Connection from invoke import UnexpectedExit from core import utils +from core.emulator.links import CoreLink from core.errors import CoreCommandError, CoreError from core.executables import get_requirements from core.nodes.interface import GreTap @@ -183,21 +184,36 @@ class DistributedController: def start(self) -> None: """ - Start distributed network tunnels. + Start distributed network tunnels for control networks. :return: nothing """ mtu = self.session.options.get_config_int("mtu") for node_id in self.session.nodes: node = self.session.nodes[node_id] - if not isinstance(node, CoreNetwork): - continue - if isinstance(node, CtrlNet) and node.serverintf is not None: + if not isinstance(node, CtrlNet) or node.serverintf is not None: continue for name in self.servers: server = self.servers[name] self.create_gre_tunnel(node, server, mtu, True) + def create_gre_tunnels(self, core_link: CoreLink) -> None: + """ + Creates gre tunnels for a core link with a ptp network connection. + + :param core_link: core link to create gre tunnel for + :return: nothing + """ + if not self.servers: + return + if not core_link.ptp: + raise CoreError( + "attempted to create gre tunnel for core link without a ptp network" + ) + mtu = self.session.options.get_config_int("mtu") + for server in self.servers.values(): + self.create_gre_tunnel(core_link.ptp, server, mtu, True) + def create_gre_tunnel( self, node: CoreNetwork, server: DistributedServer, mtu: int, start: bool ) -> Tuple[GreTap, GreTap]: diff --git a/daemon/core/emulator/links.py b/daemon/core/emulator/links.py new file mode 100644 index 00000000..22f75b98 --- /dev/null +++ b/daemon/core/emulator/links.py @@ -0,0 +1,256 @@ +""" +Provides functionality for maintaining information about known links +for a session. +""" + +import logging +from dataclasses import dataclass +from typing import Dict, Optional, Tuple, ValuesView + +from core.emulator.data import LinkData, LinkOptions +from core.emulator.enumerations import LinkTypes, MessageFlags +from core.errors import CoreError +from core.nodes.base import NodeBase +from core.nodes.interface import CoreInterface +from core.nodes.network import PtpNet + +logger = logging.getLogger(__name__) +LinkKeyType = Tuple[int, Optional[int], int, Optional[int]] + + +def create_key( + node1: NodeBase, + iface1: Optional[CoreInterface], + node2: NodeBase, + iface2: Optional[CoreInterface], +) -> LinkKeyType: + """ + Creates a unique key for tracking links. + + :param node1: first node in link + :param iface1: node1 interface + :param node2: second node in link + :param iface2: node2 interface + :return: link key + """ + iface1_id = iface1.id if iface1 else None + iface2_id = iface2.id if iface2 else None + if node1.id < node2.id: + return node1.id, iface1_id, node2.id, iface2_id + else: + return node2.id, iface2_id, node1.id, iface1_id + + +@dataclass +class CoreLink: + """ + Provides a core link data structure. + """ + + node1: NodeBase + iface1: Optional[CoreInterface] + node2: NodeBase + iface2: Optional[CoreInterface] + ptp: PtpNet = None + label: str = None + color: str = None + + def key(self) -> LinkKeyType: + """ + Retrieve the key for this link. + + :return: link key + """ + return create_key(self.node1, self.iface1, self.node2, self.iface2) + + def is_unidirectional(self) -> bool: + """ + Checks if this link is considered unidirectional, due to current + iface configurations. + + :return: True if unidirectional, False otherwise + """ + unidirectional = False + if self.iface1 and self.iface2: + unidirectional = self.iface1.options != self.iface2.options + return unidirectional + + def options(self) -> LinkOptions: + """ + Retrieve the options for this link. + + :return: options for this link + """ + if self.is_unidirectional(): + options = self.iface1.options + else: + if self.iface1: + options = self.iface1.options + else: + options = self.iface2.options + return options + + def get_data(self, message_type: MessageFlags, source: str = None) -> LinkData: + """ + Create link data for this link. + + :param message_type: link data message type + :param source: source for this data + :return: link data + """ + iface1_data = self.iface1.get_data() if self.iface1 else None + iface2_data = self.iface2.get_data() if self.iface2 else None + return LinkData( + message_type=message_type, + type=LinkTypes.WIRED, + node1_id=self.node1.id, + node2_id=self.node2.id, + iface1=iface1_data, + iface2=iface2_data, + options=self.options(), + label=self.label, + color=self.color, + source=source, + ) + + def get_data_unidirectional(self, source: str = None) -> LinkData: + """ + Create other unidirectional link data. + + :param source: source for this data + :return: unidirectional link data + """ + iface1_data = self.iface1.get_data() if self.iface1 else None + iface2_data = self.iface2.get_data() if self.iface2 else None + return LinkData( + message_type=MessageFlags.NONE, + type=LinkTypes.WIRED, + node1_id=self.node2.id, + node2_id=self.node1.id, + iface1=iface2_data, + iface2=iface1_data, + options=self.iface2.options, + label=self.label, + color=self.color, + source=source, + ) + + +class LinkManager: + """ + Provides core link management. + """ + + def __init__(self) -> None: + """ + Create a LinkManager instance. + """ + self._links: Dict[LinkKeyType, CoreLink] = {} + self._node_links: Dict[int, Dict[LinkKeyType, CoreLink]] = {} + + def add(self, core_link: CoreLink) -> None: + """ + Add a core link to be tracked. + + :param core_link: link to track + :return: nothing + """ + node1, iface1 = core_link.node1, core_link.iface1 + node2, iface2 = core_link.node2, core_link.iface2 + if core_link.key() in self._links: + raise CoreError( + f"node1({node1.name}) iface1({iface1.id}) " + f"node2({node2.name}) iface2({iface2.id}) link already exists" + ) + logger.info( + "adding link from node(%s:%s) to node(%s:%s)", + node1.name, + iface1.name if iface1 else None, + node2.name, + iface2.name if iface2 else None, + ) + self._links[core_link.key()] = core_link + node1_links = self._node_links.setdefault(node1.id, {}) + node1_links[core_link.key()] = core_link + node2_links = self._node_links.setdefault(node2.id, {}) + node2_links[core_link.key()] = core_link + + def delete( + self, + node1: NodeBase, + iface1: Optional[CoreInterface], + node2: NodeBase, + iface2: Optional[CoreInterface], + ) -> CoreLink: + """ + Remove a link from being tracked. + + :param node1: first node in link + :param iface1: node1 interface + :param node2: second node in link + :param iface2: node2 interface + :return: removed core link + """ + key = create_key(node1, iface1, node2, iface2) + if key not in self._links: + raise CoreError( + f"node1({node1.name}) iface1({iface1.id}) " + f"node2({node2.name}) iface2({iface2.id}) is not linked" + ) + logger.info( + "deleting link from node(%s:%s) to node(%s:%s)", + node1.name, + iface1.name if iface1 else None, + node2.name, + iface2.name if iface2 else None, + ) + node1_links = self._node_links[node1.id] + node1_links.pop(key) + node2_links = self._node_links[node2.id] + node2_links.pop(key) + return self._links.pop(key) + + def reset(self) -> None: + """ + Resets and clears all tracking information. + + :return: nothing + """ + self._links.clear() + self._node_links.clear() + + def get_link( + self, + node1: NodeBase, + iface1: Optional[CoreInterface], + node2: NodeBase, + iface2: Optional[CoreInterface], + ) -> Optional[CoreLink]: + """ + Retrieve a link for provided values. + + :param node1: first node in link + :param iface1: interface for node1 + :param node2: second node in link + :param iface2: interface for node2 + :return: core link if present, None otherwise + """ + key = create_key(node1, iface1, node2, iface2) + return self._links.get(key) + + def links(self) -> ValuesView[CoreLink]: + """ + Retrieve all known links + + :return: iterator for all known links + """ + return self._links.values() + + def node_links(self, node: NodeBase) -> ValuesView[CoreLink]: + """ + Retrieve all links for a given node. + + :param node: node to get links for + :return: node links + """ + return self._node_links.get(node.id, {}).values() diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 93ec0ae8..c6bd0fed 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -35,10 +35,10 @@ from core.emulator.distributed import DistributedController from core.emulator.enumerations import ( EventTypes, ExceptionLevels, - LinkTypes, MessageFlags, NodeTypes, ) +from core.emulator.links import CoreLink, LinkManager from core.emulator.sessionconfig import SessionConfig from core.errors import CoreError from core.location.event import EventLoop @@ -86,6 +86,7 @@ CONTAINER_NODES: Set[Type[NodeBase]] = {DockerNode, LxcNode} CTRL_NET_ID: int = 9001 LINK_COLORS: List[str] = ["green", "blue", "orange", "purple", "turquoise"] NT: TypeVar = TypeVar("NT", bound=NodeBase) +WIRELESS_TYPE: Tuple[Type[WlanNode], Type[EmaneNet]] = (WlanNode, EmaneNet) class Session: @@ -119,7 +120,8 @@ class Session: # dict of nodes: all nodes and nets self.nodes: Dict[int, NodeBase] = {} - self.nodes_lock = threading.Lock() + self.nodes_lock: threading.Lock = threading.Lock() + self.link_manager: LinkManager = LinkManager() # states and hooks handlers self.state: EventTypes = EventTypes.DEFINITION_STATE @@ -187,43 +189,48 @@ class Session: raise CoreError(f"invalid node class: {_class}") return node_type - def _link_wireless( - self, node1: CoreNodeBase, node2: CoreNodeBase, connect: bool - ) -> None: - """ - Objects to deal with when connecting/disconnecting wireless links. - - :param node1: node one for wireless link - :param node2: node two for wireless link - :param connect: link interfaces if True, unlink otherwise - :return: nothing - :raises core.CoreError: when objects to link is less than 2, or no common - networks are found - """ - logger.info( - "handling wireless linking node1(%s) node2(%s): %s", - node1.name, - node2.name, - connect, - ) - common_networks = node1.commonnets(node1) - if not common_networks: - raise CoreError("no common network found for wireless link/unlink") - for common_network, iface1, iface2 in common_networks: - if not isinstance(common_network, (WlanNode, EmaneNet)): - logger.info( - "skipping common network that is not wireless/emane: %s", - common_network, - ) - continue - if connect: - common_network.link(iface1, iface2) - else: - common_network.unlink(iface1, iface2) - def use_ovs(self) -> bool: return self.options.get_config("ovs") == "1" + def linked( + self, node1_id: int, node2_id: int, iface1_id: int, iface2_id: int, linked: bool + ) -> None: + """ + Links or unlinks wired core link interfaces from being connected to the same + bridge. + + :param node1_id: first node in link + :param node2_id: second node in link + :param iface1_id: node1 interface + :param iface2_id: node2 interface + :param linked: True if interfaces should be connected, False for disconnected + :return: nothing + """ + node1 = self.get_node(node1_id, NodeBase) + node2 = self.get_node(node2_id, NodeBase) + logger.info( + "link node(%s):interface(%s) node(%s):interface(%s) linked(%s)", + node1.name, + iface1_id, + node2.name, + iface2_id, + linked, + ) + iface1 = node1.get_iface(iface1_id) + iface2 = node2.get_iface(iface2_id) + core_link = self.link_manager.get_link(node1, iface1, node2, iface2) + if not core_link: + raise CoreError( + f"there is no link for node({node1.name}):interface({iface1_id}) " + f"node({node2.name}):interface({iface2_id})" + ) + if linked: + core_link.ptp.attach(iface1) + core_link.ptp.attach(iface2) + else: + core_link.ptp.detach(iface1) + core_link.ptp.detach(iface2) + def add_link( self, node1_id: int, @@ -231,8 +238,7 @@ class Session: iface1_data: InterfaceData = None, iface2_data: InterfaceData = None, options: LinkOptions = None, - link_type: LinkTypes = LinkTypes.WIRED, - ) -> Tuple[CoreInterface, CoreInterface]: + ) -> Tuple[Optional[CoreInterface], Optional[CoreInterface]]: """ Add a link between nodes. @@ -244,89 +250,126 @@ class Session: data, defaults to none :param options: data for creating link, defaults to no options - :param link_type: type of link to add :return: tuple of created core interfaces, depending on link """ - if not options: - options = LinkOptions() - node1 = self.get_node(node1_id, NodeBase) - node2 = self.get_node(node2_id, NodeBase) - iface1 = None - iface2 = None + options = options if options else LinkOptions() # set mtu mtu = self.options.get_config_int("mtu") or DEFAULT_MTU if iface1_data: iface1_data.mtu = mtu if iface2_data: iface2_data.mtu = mtu - # wireless link - if link_type == LinkTypes.WIRELESS: - if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): - self._link_wireless(node1, node2, connect=True) - else: - raise CoreError( - f"cannot wireless link node1({type(node1)}) node2({type(node2)})" - ) - # wired link + node1 = self.get_node(node1_id, NodeBase) + node2 = self.get_node(node2_id, NodeBase) + # check for invalid linking + if ( + isinstance(node1, WIRELESS_TYPE) + and isinstance(node2, WIRELESS_TYPE) + or isinstance(node1, WIRELESS_TYPE) + and not isinstance(node2, CoreNodeBase) + or not isinstance(node1, CoreNodeBase) + and isinstance(node2, WIRELESS_TYPE) + ): + raise CoreError(f"cannot link node({type(node1)}) node({type(node2)})") + # custom links + iface1 = None + iface2 = None + if isinstance(node1, WlanNode): + iface2 = self._add_wlan_link(node2, iface2_data, node1) + elif isinstance(node2, WlanNode): + iface1 = self._add_wlan_link(node1, iface1_data, node2) + elif isinstance(node1, EmaneNet) and isinstance(node2, CoreNode): + iface2 = self._add_emane_link(node2, iface2_data, node1) + elif isinstance(node2, EmaneNet) and isinstance(node1, CoreNode): + iface1 = self._add_emane_link(node1, iface1_data, node2) else: - # peer to peer link - if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): - logger.info("linking ptp: %s - %s", node1.name, node2.name) - start = self.state.should_start() - ptp = self.create_node(PtpNet, start) - iface1 = node1.new_iface(ptp, iface1_data) - iface2 = node2.new_iface(ptp, iface2_data) - iface1.config(options) - if not options.unidirectional: - iface2.config(options) - # link node to net - elif isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNetworkBase): - logger.info("linking node to net: %s - %s", node1.name, node2.name) - iface1 = node1.new_iface(node2, iface1_data) - if not isinstance(node2, (EmaneNet, WlanNode)): - iface1.config(options) - # link net to node - elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase): - logger.info("linking net to node: %s - %s", node1.name, node2.name) - iface2 = node2.new_iface(node1, iface2_data) - wireless_net = isinstance(node1, (EmaneNet, WlanNode)) - if not options.unidirectional and not wireless_net: - iface2.config(options) - # network to network - elif isinstance(node1, CoreNetworkBase) and isinstance( - node2, CoreNetworkBase - ): - logger.info( - "linking network to network: %s - %s", node1.name, node2.name - ) - iface1 = node1.linknet(node2) - use_local = iface1.net == node1 - iface1.config(options, use_local=use_local) - if not options.unidirectional: - iface1.config(options, use_local=not use_local) - else: - raise CoreError( - f"cannot link node1({type(node1)}) node2({type(node2)})" - ) - - # configure tunnel nodes - key = options.key - if isinstance(node1, TunnelNode): - logger.info("setting tunnel key for: %s", node1.name) - node1.setkey(key, iface1_data) - if isinstance(node2, TunnelNode): - logger.info("setting tunnel key for: %s", node2.name) - node2.setkey(key, iface2_data) + iface1, iface2 = self._add_wired_link( + node1, node2, iface1_data, iface2_data, options + ) + # configure tunnel nodes + key = options.key + if isinstance(node1, TunnelNode): + logger.info("setting tunnel key for: %s", node1.name) + node1.setkey(key, iface1_data) + if isinstance(node2, TunnelNode): + logger.info("setting tunnel key for: %s", node2.name) + node2.setkey(key, iface2_data) self.sdt.add_link(node1_id, node2_id) return iface1, iface2 - def delete_link( + def _add_wlan_link( + self, node: NodeBase, iface_data: InterfaceData, net: WlanNode + ) -> CoreInterface: + """ + Create a wlan link. + + :param node: node to link to wlan network + :param iface_data: data to create interface with + :param net: wlan network to link to + :return: interface created for node + """ + # create interface + iface = node.create_iface(iface_data) + # attach to wlan + net.attach(iface) + # track link + core_link = CoreLink(node, iface, net, None) + self.link_manager.add(core_link) + return iface + + def _add_emane_link( + self, node: CoreNode, iface_data: InterfaceData, net: EmaneNet + ) -> CoreInterface: + """ + Create am emane link. + + :param node: node to link to emane network + :param iface_data: data to create interface with + :param net: emane network to link to + :return: interface created for node + """ + # create iface tuntap + iface = net.create_tuntap(node, iface_data) + # track link + core_link = CoreLink(node, iface, net, None) + self.link_manager.add(core_link) + return iface + + def _add_wired_link( self, - node1_id: int, - node2_id: int, - iface1_id: int = None, - iface2_id: int = None, - link_type: LinkTypes = LinkTypes.WIRED, + node1: NodeBase, + node2: NodeBase, + iface1_data: InterfaceData = None, + iface2_data: InterfaceData = None, + options: LinkOptions = None, + ) -> Tuple[CoreInterface, CoreInterface]: + """ + Create a wired link between two nodes. + + :param node1: first node to be linked + :param node2: second node to be linked + :param iface1_data: data to create interface for node1 + :param iface2_data: data to create interface for node2 + :param options: options to configure interfaces with + :return: interfaces created for both nodes + """ + # create interfaces + iface1 = node1.create_iface(iface1_data, options) + iface2 = node2.create_iface(iface2_data, options) + # join and attach to ptp bridge + ptp = self.create_node(PtpNet, self.state.should_start()) + ptp.attach(iface1) + ptp.attach(iface2) + # track link + core_link = CoreLink(node1, iface1, node2, iface2, ptp) + self.link_manager.add(core_link) + # setup link for gre tunnels if needed + if ptp.up: + self.distributed.create_gre_tunnels(core_link) + return iface1, iface2 + + def delete_link( + self, node1_id: int, node2_id: int, iface1_id: int = None, iface2_id: int = None ) -> None: """ Delete a link between nodes. @@ -335,63 +378,38 @@ class Session: :param node2_id: node two id :param iface1_id: interface id for node one :param iface2_id: interface id for node two - :param link_type: link type to delete :return: nothing :raises core.CoreError: when no common network is found for link being deleted """ node1 = self.get_node(node1_id, NodeBase) node2 = self.get_node(node2_id, NodeBase) logger.info( - "deleting link(%s) node(%s):interface(%s) node(%s):interface(%s)", - link_type.name, + "deleting link node(%s):interface(%s) node(%s):interface(%s)", node1.name, iface1_id, node2.name, iface2_id, ) - - # wireless link - if link_type == LinkTypes.WIRELESS: - if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): - self._link_wireless(node1, node2, connect=False) - else: - raise CoreError( - "cannot delete wireless link " - f"node1({type(node1)}) node2({type(node2)})" - ) - # wired link + iface1 = None + iface2 = None + if isinstance(node1, WlanNode): + iface2 = node2.delete_iface(iface2_id) + node1.detach(iface2) + elif isinstance(node2, WlanNode): + iface1 = node1.delete_iface(iface1_id) + node2.detach(iface1) + elif isinstance(node1, EmaneNet): + iface2 = node2.delete_iface(iface2_id) + node1.detach(iface2) + elif isinstance(node2, EmaneNet): + iface1 = node1.delete_iface(iface1_id) + node2.detach(iface1) else: - if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): - iface1 = node1.get_iface(iface1_id) - iface2 = node2.get_iface(iface2_id) - if iface1.net != iface2.net: - raise CoreError( - f"node1({node1.name}) node2({node2.name}) " - "not connected to same net" - ) - ptp = iface1.net - node1.delete_iface(iface1_id) - node2.delete_iface(iface2_id) - self.delete_node(ptp.id) - elif isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNetworkBase): - node1.delete_iface(iface1_id) - elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase): - node2.delete_iface(iface2_id) - elif isinstance(node1, CoreNetworkBase) and isinstance( - node2, CoreNetworkBase - ): - iface1 = node1.get_linked_iface(node2) - if iface1: - node1.detach(iface1) - iface1.shutdown() - iface2 = node2.get_linked_iface(node1) - if iface2: - node2.detach(iface2) - iface2.shutdown() - if not iface1 and not iface2: - raise CoreError( - f"node1({node1.name}) and node2({node2.name}) are not connected" - ) + iface1 = node1.delete_iface(iface1_id) + iface2 = node2.delete_iface(iface2_id) + core_link = self.link_manager.delete(node1, iface1, node2, iface2) + if core_link.ptp: + self.delete_node(core_link.ptp.id) self.sdt.delete_link(node1_id, node2_id) def update_link( @@ -401,7 +419,6 @@ class Session: iface1_id: int = None, iface2_id: int = None, options: LinkOptions = None, - link_type: LinkTypes = LinkTypes.WIRED, ) -> None: """ Update link information between nodes. @@ -411,7 +428,6 @@ class Session: :param iface1_id: interface id for node one :param iface2_id: interface id for node two :param options: data to update link with - :param link_type: type of link to update :return: nothing :raises core.CoreError: when updating a wireless type link, when there is a unknown link between networks @@ -421,72 +437,26 @@ class Session: node1 = self.get_node(node1_id, NodeBase) node2 = self.get_node(node2_id, NodeBase) logger.info( - "update link(%s) node(%s):interface(%s) node(%s):interface(%s)", - link_type.name, + "update link node(%s):interface(%s) node(%s):interface(%s)", node1.name, iface1_id, node2.name, iface2_id, ) - - # wireless link - if link_type == LinkTypes.WIRELESS: - raise CoreError("cannot update wireless link") - else: - if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): - iface1 = node1.ifaces.get(iface1_id) - if not iface1: - raise CoreError( - f"node({node1.name}) missing interface({iface1_id})" - ) - iface2 = node2.ifaces.get(iface2_id) - if not iface2: - raise CoreError( - f"node({node2.name}) missing interface({iface2_id})" - ) - if iface1.net != iface2.net: - raise CoreError( - f"node1({node1.name}) node2({node2.name}) " - "not connected to same net" - ) - iface1.config(options) - if not options.unidirectional: - iface2.config(options) - elif isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNetworkBase): - iface = node1.get_iface(iface1_id) - if iface.net != node2: - raise CoreError( - f"node1({node1.name}) iface1({iface1_id})" - f" is not linked to node1({node2.name})" - ) - iface.config(options) - elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase): - iface = node2.get_iface(iface2_id) - if iface.net != node1: - raise CoreError( - f"node2({node2.name}) iface2({iface2_id})" - f" is not linked to node1({node1.name})" - ) - iface.config(options) - elif isinstance(node1, CoreNetworkBase) and isinstance( - node2, CoreNetworkBase - ): - iface = node1.get_linked_iface(node2) - if not iface: - iface = node2.get_linked_iface(node1) - if iface: - use_local = iface.net == node1 - iface.config(options, use_local=use_local) - if not options.unidirectional: - iface.config(options, use_local=not use_local) - else: - raise CoreError( - f"node1({node1.name}) and node2({node2.name}) are not linked" - ) - else: - raise CoreError( - f"cannot update link node1({type(node1)}) node2({type(node2)})" - ) + iface1 = node1.get_iface(iface1_id) if iface1_id is not None else None + iface2 = node2.get_iface(iface2_id) if iface2_id is not None else None + core_link = self.link_manager.get_link(node1, iface1, node2, iface2) + if not core_link: + raise CoreError( + f"there is no link for node({node1.name}):interface({iface1_id}) " + f"node({node2.name}):interface({iface2_id})" + ) + if iface1: + iface1.options.update(options) + iface1.set_config() + if iface2 and not options.unidirectional: + iface2.options.update(options) + iface2.set_config() def next_node_id(self) -> int: """ @@ -703,6 +673,7 @@ class Session: """ self.emane.shutdown() self.delete_nodes() + self.link_manager.reset() self.distributed.shutdown() self.hooks.clear() self.emane.reset() @@ -1426,7 +1397,8 @@ class Session: ip4_mask=ip4_mask, mtu=DEFAULT_MTU, ) - iface = node.new_iface(control_net, iface_data) + iface = node.create_iface(iface_data) + control_net.attach(iface) iface.control = True except ValueError: msg = f"Control interface not added to node {node.id}. " diff --git a/daemon/core/gui/dialogs/nodeconfig.py b/daemon/core/gui/dialogs/nodeconfig.py index ee0d7b81..1c477cbf 100644 --- a/daemon/core/gui/dialogs/nodeconfig.py +++ b/daemon/core/gui/dialogs/nodeconfig.py @@ -275,7 +275,7 @@ class NodeConfigDialog(Dialog): ifaces_scroll.listbox.bind("<>", self.iface_select) # interfaces - if self.canvas_node.ifaces: + if nutils.is_container(self.node): self.draw_ifaces() self.draw_spacer() diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index 1de7319f..7f445f91 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -298,7 +298,10 @@ class CanvasNode: other_iface = edge.other_iface(self) label = other_node.core_node.name if other_iface: - label = f"{label}:{other_iface.name}" + iface_label = other_iface.id + if other_iface.name: + iface_label = other_iface.name + label = f"{label}:{iface_label}" func_unlink = functools.partial(self.click_unlink, edge) unlink_menu.add_command(label=label, command=func_unlink) themes.style_menu(unlink_menu) diff --git a/daemon/core/gui/interface.py b/daemon/core/gui/interface.py index d4d09443..83fba104 100644 --- a/daemon/core/gui/interface.py +++ b/daemon/core/gui/interface.py @@ -241,10 +241,10 @@ class InterfaceManager: dst_node = edge.dst.core_node self.determine_subnets(edge.src, edge.dst) src_iface = None - if nutils.is_container(src_node): + if nutils.is_iface_node(src_node): src_iface = self.create_iface(edge.src, edge.linked_wireless) dst_iface = None - if nutils.is_container(dst_node): + if nutils.is_iface_node(dst_node): dst_iface = self.create_iface(edge.dst, edge.linked_wireless) link = Link( type=LinkType.WIRED, @@ -258,22 +258,26 @@ class InterfaceManager: def create_iface(self, canvas_node: CanvasNode, wireless_link: bool) -> Interface: node = canvas_node.core_node - ip4, ip6 = self.get_ips(node) - if wireless_link: - ip4_mask = WIRELESS_IP4_MASK - ip6_mask = WIRELESS_IP6_MASK + if nutils.is_bridge(node): + iface_id = canvas_node.next_iface_id() + iface = Interface(id=iface_id) else: - ip4_mask = IP4_MASK - ip6_mask = IP6_MASK - iface_id = canvas_node.next_iface_id() - name = f"eth{iface_id}" - iface = Interface( - id=iface_id, - name=name, - ip4=ip4, - ip4_mask=ip4_mask, - ip6=ip6, - ip6_mask=ip6_mask, - ) + ip4, ip6 = self.get_ips(node) + if wireless_link: + ip4_mask = WIRELESS_IP4_MASK + ip6_mask = WIRELESS_IP6_MASK + else: + ip4_mask = IP4_MASK + ip6_mask = IP6_MASK + iface_id = canvas_node.next_iface_id() + name = f"eth{iface_id}" + iface = Interface( + id=iface_id, + name=name, + ip4=ip4, + ip4_mask=ip4_mask, + ip6=ip6, + ip6_mask=ip6_mask, + ) logger.info("create node(%s) interface(%s)", node.name, iface) return iface diff --git a/daemon/core/gui/nodeutils.py b/daemon/core/gui/nodeutils.py index 537cedf2..dd6af5db 100644 --- a/daemon/core/gui/nodeutils.py +++ b/daemon/core/gui/nodeutils.py @@ -97,6 +97,10 @@ def is_custom(node: Node) -> bool: return is_model(node) and node.model not in NODE_MODELS +def is_iface_node(node: Node) -> bool: + return is_container(node) or is_bridge(node) + + def get_custom_services(gui_config: GuiConfig, name: str) -> List[str]: for custom_node in gui_config.nodes: if custom_node.name == name: diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 98249b3e..bae9985d 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -320,7 +320,8 @@ class BasicRangeModel(WirelessModel): loss=self.loss, jitter=self.jitter, ) - iface.config(options) + iface.options.update(options) + iface.set_config() def get_position(self, iface: CoreInterface) -> Tuple[float, float, float]: """ diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 34bf6601..6fbfb612 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -13,11 +13,11 @@ import netaddr from core import utils from core.configservice.dependencies import ConfigServiceDependencies -from core.emulator.data import InterfaceData, LinkData -from core.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes +from core.emulator.data import InterfaceData, LinkOptions +from core.emulator.enumerations import LinkTypes, NodeTypes from core.errors import CoreCommandError, CoreError from core.executables import BASH, MOUNT, TEST, VCMD, VNODED -from core.nodes.interface import DEFAULT_MTU, CoreInterface, TunTap, Veth +from core.nodes.interface import DEFAULT_MTU, CoreInterface from core.nodes.netclient import LinuxNetClient, get_net_client logger = logging.getLogger(__name__) @@ -74,6 +74,7 @@ class NodeBase(abc.ABC): self.icon: Optional[str] = None self.position: Position = Position() self.up: bool = False + self.lock: RLock = RLock() self.net_client: LinuxNetClient = get_net_client( self.session.use_ovs(), self.host_cmd ) @@ -96,6 +97,18 @@ class NodeBase(abc.ABC): """ raise NotImplementedError + @abc.abstractmethod + def adopt_iface(self, iface: CoreInterface, name: str) -> None: + """ + Adopt an interface, placing within network namespacing for containers + and setting to bridge masters for network like nodes. + + :param iface: interface to adopt + :param name: proper name to use for interface + :return: nothing + """ + raise NotImplementedError + def host_cmd( self, args: str, @@ -120,6 +133,19 @@ class NodeBase(abc.ABC): else: return self.server.remote_cmd(args, env, cwd, wait) + def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: + """ + Runs a command that is in the context of a node, default is to run a standard + host command. + + :param args: command to run + :param wait: True to wait for status, False otherwise + :param shell: True to use shell, False otherwise + :return: combined stdout and stderr + :raises CoreCommandError: when a non-zero exit status occurs + """ + return self.host_cmd(args, wait=wait, shell=shell) + def setposition(self, x: float = None, y: float = None, z: float = None) -> bool: """ Set the (x,y,z) position of the object. @@ -139,6 +165,70 @@ class NodeBase(abc.ABC): """ return self.position.get() + def create_iface( + self, iface_data: InterfaceData = None, options: LinkOptions = None + ) -> CoreInterface: + """ + Creates an interface and adopts it to a node. + + :param iface_data: data to create interface with + :param options: options to create interface with + :return: created interface + """ + with self.lock: + if iface_data and iface_data.id is not None: + if iface_data.id in self.ifaces: + raise CoreError( + f"node({self.id}) interface({iface_data.id}) already exists" + ) + iface_id = iface_data.id + else: + iface_id = self.next_iface_id() + mtu = DEFAULT_MTU + if iface_data and iface_data.mtu is not None: + mtu = iface_data.mtu + name = f"veth{self.id}.{iface_id}.{self.session.short_session_id()}" + localname = f"{name}p" + iface = CoreInterface( + iface_id, + name, + localname, + self.session.use_ovs(), + mtu, + node=self, + server=self.server, + ) + if iface_data: + if iface_data.mac: + iface.set_mac(iface_data.mac) + for ip in iface_data.get_ips(): + iface.add_ip(ip) + if options: + iface.options.update(options) + self.ifaces[iface_id] = iface + if self.up: + iface.startup() + if iface_data and iface_data.name is not None: + name = iface_data.name + else: + name = iface.name + self.adopt_iface(iface, name) + return iface + + def delete_iface(self, iface_id: int) -> CoreInterface: + """ + Delete an interface. + + :param iface_id: interface id to delete + :return: the removed interface + """ + if iface_id not in self.ifaces: + raise CoreError(f"node({self.name}) interface({iface_id}) does not exist") + iface = self.ifaces.pop(iface_id) + logger.info("node(%s) removing interface(%s)", self.name, iface.name) + iface.shutdown() + return iface + def get_iface(self, iface_id: int) -> CoreInterface: """ Retrieve interface based on id. @@ -191,15 +281,6 @@ class NodeBase(abc.ABC): self.iface_id += 1 return iface_id - def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: - """ - Build link data for this node. - - :param flags: message flags - :return: list of link data - """ - return [] - class CoreNodeBase(NodeBase): """ @@ -227,14 +308,6 @@ class CoreNodeBase(NodeBase): self.directory: Optional[Path] = None self.tmpnodedir: bool = False - @abc.abstractmethod - def startup(self) -> None: - raise NotImplementedError - - @abc.abstractmethod - def shutdown(self) -> None: - raise NotImplementedError - @abc.abstractmethod def create_dir(self, dir_path: Path) -> None: """ @@ -270,19 +343,6 @@ class CoreNodeBase(NodeBase): """ raise NotImplementedError - @abc.abstractmethod - def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: - """ - Runs a command within a node container. - - :param args: command to run - :param wait: True to wait for status, False otherwise - :param shell: True to use shell, False otherwise - :return: combined stdout and stderr - :raises CoreCommandError: when a non-zero exit status occurs - """ - raise NotImplementedError - @abc.abstractmethod def termcmdstring(self, sh: str) -> str: """ @@ -293,19 +353,6 @@ class CoreNodeBase(NodeBase): """ raise NotImplementedError - @abc.abstractmethod - def new_iface( - self, net: "CoreNetworkBase", iface_data: InterfaceData - ) -> CoreInterface: - """ - Create a new interface. - - :param net: network to associate with - :param iface_data: interface data for new interface - :return: interface index - """ - raise NotImplementedError - @abc.abstractmethod def path_exists(self, path: str) -> bool: """ @@ -318,7 +365,7 @@ class CoreNodeBase(NodeBase): def host_path(self, path: Path, is_dir: bool = False) -> Path: """ - Return the name of a node"s file on the host filesystem. + Return the name of a node's file on the host filesystem. :param path: path to translate to host path :param is_dir: True if path is a directory path, False otherwise @@ -393,54 +440,6 @@ class CoreNodeBase(NodeBase): if self.tmpnodedir: self.host_cmd(f"rm -rf {self.directory}") - def add_iface(self, iface: CoreInterface, iface_id: int) -> None: - """ - Add network interface to node and set the network interface index if successful. - - :param iface: network interface to add - :param iface_id: interface id - :return: nothing - """ - if iface_id in self.ifaces: - raise CoreError(f"interface({iface_id}) already exists") - self.ifaces[iface_id] = iface - iface.node_id = iface_id - - def delete_iface(self, iface_id: int) -> None: - """ - Delete a network interface - - :param iface_id: interface index to delete - :return: nothing - """ - if iface_id not in self.ifaces: - raise CoreError(f"node({self.name}) interface({iface_id}) does not exist") - iface = self.ifaces.pop(iface_id) - logger.info("node(%s) removing interface(%s)", self.name, iface.name) - iface.detachnet() - iface.shutdown() - - def attachnet(self, iface_id: int, net: "CoreNetworkBase") -> None: - """ - Attach a network. - - :param iface_id: interface of index to attach - :param net: network to attach - :return: nothing - """ - iface = self.get_iface(iface_id) - iface.attachnet(net) - - def detachnet(self, iface_id: int) -> None: - """ - Detach network interface. - - :param iface_id: interface id to detach - :return: nothing - """ - iface = self.get_iface(iface_id) - iface.detachnet() - def setposition(self, x: float = None, y: float = None, z: float = None) -> None: """ Set position. @@ -455,25 +454,6 @@ class CoreNodeBase(NodeBase): for iface in self.get_ifaces(): iface.setposition() - def commonnets( - self, node: "CoreNodeBase", want_ctrl: bool = False - ) -> List[Tuple["CoreNetworkBase", CoreInterface, CoreInterface]]: - """ - Given another node or net object, return common networks between - this node and that object. A list of tuples is returned, with each tuple - consisting of (network, interface1, interface2). - - :param node: node to get common network with - :param want_ctrl: flag set to determine if control network are wanted - :return: tuples of common networks - """ - common = [] - for iface1 in self.get_ifaces(control=want_ctrl): - for iface2 in node.get_ifaces(): - if iface1.net == iface2.net: - common.append((iface1.net, iface1, iface2)) - return common - class CoreNode(CoreNodeBase): """ @@ -504,7 +484,6 @@ class CoreNode(CoreNodeBase): self.directory: Optional[Path] = directory self.ctrlchnlname: Path = self.session.directory / self.name self.pid: Optional[int] = None - self.lock: RLock = RLock() self._mounts: List[Tuple[Path, Path]] = [] self.node_net_client: LinuxNetClient = self.create_node_net_client( self.session.use_ovs() @@ -585,6 +564,10 @@ class CoreNode(CoreNodeBase): self._mounts = [] # shutdown all interfaces for iface in self.get_ifaces(): + try: + self.node_net_client.device_flush(iface.name) + except CoreCommandError: + pass iface.shutdown() # kill node process if present try: @@ -691,150 +674,6 @@ class CoreNode(CoreNodeBase): self.cmd(f"{MOUNT} -n --bind {src_path} {target_path}") self._mounts.append((src_path, target_path)) - def next_iface_id(self) -> int: - """ - Retrieve a new interface index. - - :return: new interface index - """ - with self.lock: - return super().next_iface_id() - - def newveth(self, iface_id: int = None, ifname: str = None, mtu: int = None) -> int: - """ - Create a new interface. - - :param iface_id: id for the new interface - :param ifname: name for the new interface - :param mtu: mtu for interface - :return: nothing - """ - with self.lock: - mtu = mtu if mtu is not None else DEFAULT_MTU - iface_id = iface_id if iface_id is not None else self.next_iface_id() - ifname = ifname if ifname is not None else f"eth{iface_id}" - sessionid = self.session.short_session_id() - try: - suffix = f"{self.id:x}.{iface_id}.{sessionid}" - except TypeError: - suffix = f"{self.id}.{iface_id}.{sessionid}" - localname = f"veth{suffix}" - name = f"{localname}p" - veth = Veth(self.session, name, localname, mtu, self.server, self) - veth.adopt_node(iface_id, ifname, self.up) - return iface_id - - def newtuntap(self, iface_id: int = None, ifname: str = None) -> int: - """ - Create a new tunnel tap. - - :param iface_id: interface id - :param ifname: interface name - :return: interface index - """ - with self.lock: - iface_id = iface_id if iface_id is not None else self.next_iface_id() - ifname = ifname if ifname is not None else f"eth{iface_id}" - sessionid = self.session.short_session_id() - localname = f"tap{self.id}.{iface_id}.{sessionid}" - name = ifname - tuntap = TunTap(self.session, name, localname, node=self) - if self.up: - tuntap.startup() - try: - self.add_iface(tuntap, iface_id) - except CoreError as e: - tuntap.shutdown() - raise e - return iface_id - - def set_mac(self, iface_id: int, mac: str) -> None: - """ - Set hardware address for an interface. - - :param iface_id: id of interface to set hardware address for - :param mac: mac address to set - :return: nothing - :raises CoreCommandError: when a non-zero exit status occurs - """ - iface = self.get_iface(iface_id) - iface.set_mac(mac) - if self.up: - self.node_net_client.device_mac(iface.name, str(iface.mac)) - - def add_ip(self, iface_id: int, ip: str) -> None: - """ - Add an ip address to an interface in the format "10.0.0.1/24". - - :param iface_id: id of interface to add address to - :param ip: address to add to interface - :return: nothing - :raises CoreError: when ip address provided is invalid - :raises CoreCommandError: when a non-zero exit status occurs - """ - iface = self.get_iface(iface_id) - iface.add_ip(ip) - if self.up: - # ipv4 check - broadcast = None - if netaddr.valid_ipv4(ip): - broadcast = "+" - self.node_net_client.create_address(iface.name, ip, broadcast) - - def remove_ip(self, iface_id: int, ip: str) -> None: - """ - Remove an ip address from an interface in the format "10.0.0.1/24". - - :param iface_id: id of interface to delete address from - :param ip: ip address to remove from interface - :return: nothing - :raises CoreError: when ip address provided is invalid - :raises CoreCommandError: when a non-zero exit status occurs - """ - iface = self.get_iface(iface_id) - iface.remove_ip(ip) - if self.up: - self.node_net_client.delete_address(iface.name, ip) - - def ifup(self, iface_id: int) -> None: - """ - Bring an interface up. - - :param iface_id: index of interface to bring up - :return: nothing - """ - if self.up: - iface = self.get_iface(iface_id) - self.node_net_client.device_up(iface.name) - - def new_iface( - self, net: "CoreNetworkBase", iface_data: InterfaceData - ) -> CoreInterface: - """ - Create a new network interface. - - :param net: network to associate with - :param iface_data: interface data for new interface - :return: interface index - """ - with self.lock: - if net.has_custom_iface: - return net.custom_iface(self, iface_data) - else: - iface_id = iface_data.id - if iface_id is not None and iface_id in self.ifaces: - raise CoreError( - f"node({self.name}) already has interface({iface_id})" - ) - iface_id = self.newveth(iface_id, iface_data.name, iface_data.mtu) - self.attachnet(iface_id, net) - if iface_data.mac: - self.set_mac(iface_id, iface_data.mac) - for ip in iface_data.get_ips(): - self.add_ip(iface_id, ip) - self.ifup(iface_id) - return self.get_iface(iface_id) - def _find_parent_path(self, path: Path) -> Optional[Path]: """ Check if there is a mounted parent directory created for this node. @@ -910,6 +749,48 @@ class CoreNode(CoreNodeBase): if mode is not None: self.host_cmd(f"chmod {mode:o} {host_path}") + def adopt_iface(self, iface: CoreInterface, name: str) -> None: + """ + Adopt interface to the network namespace of the node and setting + the proper name provided. + + :param iface: interface to adopt + :param name: proper name for interface + :return: nothing + """ + # TODO: container, checksums off (container only?) + # TODO: container, get flow id (container only?) + # validate iface belongs to node and get id + iface_id = self.get_iface_id(iface) + if iface_id == -1: + raise CoreError(f"adopting unknown iface({iface.name})") + # add iface to container namespace + self.net_client.device_ns(iface.name, str(self.pid)) + # update iface name to container name + name = name if name else f"eth{iface_id}" + self.node_net_client.device_name(iface.name, name) + iface.name = name + # turn checksums off + self.node_net_client.checksums_off(iface.name) + # retrieve flow id for container + iface.flow_id = self.node_net_client.get_ifindex(iface.name) + logger.debug("interface flow index: %s - %s", iface.name, iface.flow_id) + # set mac address + if iface.mac: + self.node_net_client.device_mac(iface.name, str(iface.mac)) + logger.debug("interface mac: %s - %s", iface.name, iface.mac) + # set all addresses + for ip in iface.ips(): + # ipv4 check + broadcast = None + if netaddr.valid_ipv4(ip): + broadcast = "+" + self.node_net_client.create_address(iface.name, str(ip), broadcast) + # configure iface options + iface.set_config() + # set iface up + self.node_net_client.device_up(iface.name) + class CoreNetworkBase(NodeBase): """ @@ -917,7 +798,6 @@ class CoreNetworkBase(NodeBase): """ linktype: LinkTypes = LinkTypes.WIRED - has_custom_iface: bool = False def __init__( self, @@ -941,57 +821,6 @@ class CoreNetworkBase(NodeBase): self.linked: Dict[CoreInterface, Dict[CoreInterface, bool]] = {} self.linked_lock: threading.Lock = threading.Lock() - @abc.abstractmethod - def startup(self) -> None: - """ - Each object implements its own startup method. - - :return: nothing - """ - raise NotImplementedError - - @abc.abstractmethod - def shutdown(self) -> None: - """ - Each object implements its own shutdown method. - - :return: nothing - """ - raise NotImplementedError - - @abc.abstractmethod - def linknet(self, net: "CoreNetworkBase") -> CoreInterface: - """ - Link network to another. - - :param net: network to link with - :return: created interface - """ - raise NotImplementedError - - @abc.abstractmethod - def custom_iface(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface: - """ - Defines custom logic for creating an interface, if required. - - :param node: node to create interface for - :param iface_data: data for creating interface - :return: created interface - """ - raise NotImplementedError - - def get_linked_iface(self, net: "CoreNetworkBase") -> Optional[CoreInterface]: - """ - Return the interface that links this net with another net. - - :param net: interface to get link for - :return: interface the provided network is linked to - """ - for iface in self.get_ifaces(): - if iface.othernet == net: - return iface - return None - def attach(self, iface: CoreInterface) -> None: """ Attach network interface. @@ -999,9 +828,10 @@ class CoreNetworkBase(NodeBase): :param iface: network interface to attach :return: nothing """ - i = self.next_iface_id() - self.ifaces[i] = iface - iface.net_id = i + iface_id = self.next_iface_id() + self.ifaces[iface_id] = iface + iface.net = self + iface.net_id = iface_id with self.linked_lock: self.linked[iface] = {} @@ -1013,56 +843,11 @@ class CoreNetworkBase(NodeBase): :return: nothing """ del self.ifaces[iface.net_id] + iface.net = None iface.net_id = None with self.linked_lock: del self.linked[iface] - def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: - """ - Build link data objects for this network. Each link object describes a link - between this network and a node. - - :param flags: message type - :return: list of link data - """ - all_links = [] - # build a link message from this network node to each node having a - # connected interface - for iface in self.get_ifaces(): - unidirectional = 0 - linked_node = iface.node - if linked_node is None: - # two layer-2 switches/hubs linked together - if not iface.othernet: - continue - linked_node = iface.othernet - if linked_node.id == self.id: - continue - if iface.local_options != iface.options: - unidirectional = 1 - iface_data = iface.get_data() - link_data = LinkData( - message_type=flags, - type=self.linktype, - node1_id=self.id, - node2_id=linked_node.id, - iface2=iface_data, - options=iface.local_options, - ) - link_data.options.unidirectional = unidirectional - all_links.append(link_data) - if unidirectional: - link_data = LinkData( - message_type=MessageFlags.NONE, - type=self.linktype, - node1_id=linked_node.id, - node2_id=self.id, - options=iface.options, - ) - link_data.options.unidirectional = unidirectional - all_links.append(link_data) - return all_links - class Position: """ diff --git a/daemon/core/nodes/docker.py b/daemon/core/nodes/docker.py index d5e928de..0a0690b1 100644 --- a/daemon/core/nodes/docker.py +++ b/daemon/core/nodes/docker.py @@ -93,10 +93,9 @@ class DockerNode(CoreNode): will run on, default is None for localhost :param image: image to start container with """ - if image is None: - image = "ubuntu" - self.image: str = image super().__init__(session, _id, name, directory, server) + self.image = image if image is not None else "ubuntu" + self.client: Optional[DockerClient] = None def create_node_net_client(self, use_ovs: bool) -> LinuxNetClient: """ @@ -141,7 +140,6 @@ class DockerNode(CoreNode): # nothing to do if node is not up if not self.up: return - with self.lock: self.ifaces.clear() self.client.stop_container() diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index 70eb679f..bb90653f 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -4,7 +4,6 @@ virtual ethernet classes that implement the interfaces available under Linux. import logging import math -import time from pathlib import Path from typing import TYPE_CHECKING, Callable, Dict, List, Optional @@ -20,11 +19,12 @@ from core.nodes.netclient import LinuxNetClient, get_net_client logger = logging.getLogger(__name__) if TYPE_CHECKING: - from core.emulator.distributed import DistributedServer from core.emulator.session import Session - from core.nodes.base import CoreNetworkBase, CoreNode + from core.emulator.distributed import DistributedServer + from core.nodes.base import CoreNetworkBase, CoreNode, NodeBase DEFAULT_MTU: int = 1500 +IFACE_NAME_LENGTH: int = 15 def tc_clear_cmd(name: str) -> str: @@ -78,35 +78,42 @@ class CoreInterface: def __init__( self, - session: "Session", + _id: int, name: str, localname: str, + use_ovs: bool, mtu: int = DEFAULT_MTU, + node: "NodeBase" = None, server: "DistributedServer" = None, - node: "CoreNode" = None, ) -> None: """ Creates a CoreInterface instance. - :param session: core session instance + :param _id: interface id for associated node :param name: interface name :param localname: interface local name + :param use_ovs: True to use ovs, False otherwise :param mtu: mtu value + :param node: node associated with this interface :param server: remote server node will run on, default is None for localhost - :param node: node for interface """ - if len(name) >= 16: - raise CoreError(f"interface name ({name}) too long, max 16") - if len(localname) >= 16: - raise CoreError(f"interface local name ({localname}) too long, max 16") - self.session: "Session" = session - self.node: Optional["CoreNode"] = node + if len(name) >= IFACE_NAME_LENGTH: + raise CoreError( + f"interface name ({name}) too long, max {IFACE_NAME_LENGTH}" + ) + if len(localname) >= IFACE_NAME_LENGTH: + raise CoreError( + f"interface local name ({localname}) too long, max {IFACE_NAME_LENGTH}" + ) + self.id: int = _id + self.node: Optional["NodeBase"] = node + # id of interface for network, used by wlan/emane + self.net_id: Optional[int] = None self.name: str = name self.localname: str = localname self.up: bool = False self.mtu: int = mtu self.net: Optional[CoreNetworkBase] = None - self.othernet: Optional[CoreNetworkBase] = None self.ip4s: List[netaddr.IPNetwork] = [] self.ip6s: List[netaddr.IPNetwork] = [] self.mac: Optional[netaddr.EUI] = None @@ -114,20 +121,12 @@ class CoreInterface: self.poshook: Callable[[CoreInterface], None] = lambda x: None # used with EMANE self.transport_type: TransportType = TransportType.VIRTUAL - # id of interface for node - self.node_id: Optional[int] = None - # id of interface for network - self.net_id: Optional[int] = None # id used to find flow data self.flow_id: Optional[int] = None self.server: Optional["DistributedServer"] = server - self.net_client: LinuxNetClient = get_net_client( - self.session.use_ovs(), self.host_cmd - ) + self.net_client: LinuxNetClient = get_net_client(use_ovs, self.host_cmd) self.control: bool = False # configuration data - self.has_local_netem: bool = False - self.local_options: LinkOptions = LinkOptions() self.has_netem: bool = False self.options: LinkOptions = LinkOptions() @@ -161,7 +160,13 @@ class CoreInterface: :return: nothing """ - pass + self.net_client.create_veth(self.localname, self.name) + if self.mtu > 0: + self.net_client.set_mtu(self.name, self.mtu) + self.net_client.set_mtu(self.localname, self.mtu) + self.net_client.device_up(self.name) + self.net_client.device_up(self.localname) + self.up = True def shutdown(self) -> None: """ @@ -169,29 +174,14 @@ class CoreInterface: :return: nothing """ - pass - - def attachnet(self, net: "CoreNetworkBase") -> None: - """ - Attach network. - - :param net: network to attach - :return: nothing - """ - if self.net: - self.detachnet() - self.net = None - net.attach(self) - self.net = net - - def detachnet(self) -> None: - """ - Detach from a network. - - :return: nothing - """ - if self.net is not None: - self.net.detach(self) + if not self.up: + return + if self.localname: + try: + self.net_client.delete_device(self.localname) + except CoreCommandError: + pass + self.up = False def add_ip(self, ip: str) -> None: """ @@ -303,41 +293,24 @@ class CoreInterface: """ return self.transport_type == TransportType.VIRTUAL - def config(self, options: LinkOptions, use_local: bool = True) -> None: - """ - Configure interface using tc based on existing state and provided - link options. - - :param options: options to configure with - :param use_local: True to use localname for device, False for name - :return: nothing - """ - # determine name, options, and if anything has changed - name = self.localname if use_local else self.name - current_options = self.local_options if use_local else self.options - changed = current_options.update(options) - # nothing more to do when nothing has changed or not up - if not changed or not self.up: - return + def set_config(self) -> None: # clear current settings - if current_options.is_clear(): - clear_local_netem = use_local and self.has_local_netem - clear_netem = not use_local and self.has_netem - if clear_local_netem or clear_netem: - cmd = tc_clear_cmd(name) - self.host_cmd(cmd) - if use_local: - self.has_local_netem = False + if self.options.is_clear(): + if self.has_netem: + cmd = tc_clear_cmd(self.name) + if self.node: + self.node.cmd(cmd) else: - self.has_netem = False + self.host_cmd(cmd) + self.has_netem = False # set updated settings else: - cmd = tc_cmd(name, current_options, self.mtu) - self.host_cmd(cmd) - if use_local: - self.has_local_netem = True + cmd = tc_cmd(self.name, self.options, self.mtu) + if self.node: + self.node.cmd(cmd) else: - self.has_netem = True + self.host_cmd(cmd) + self.has_netem = True def get_data(self) -> InterfaceData: """ @@ -345,231 +318,22 @@ class CoreInterface: :return: interface data """ - if self.node: - iface_id = self.node.get_iface_id(self) - else: - iface_id = self.othernet.get_iface_id(self) - data = InterfaceData( - id=iface_id, name=self.name, mac=str(self.mac) if self.mac else None - ) ip4 = self.get_ip4() - if ip4: - data.ip4 = str(ip4.ip) - data.ip4_mask = ip4.prefixlen + ip4_addr = str(ip4.ip) if ip4 else None + ip4_mask = ip4.prefixlen if ip4 else None ip6 = self.get_ip6() - if ip6: - data.ip6 = str(ip6.ip) - data.ip6_mask = ip6.prefixlen - return data - - -class Veth(CoreInterface): - """ - Provides virtual ethernet functionality for core nodes. - """ - - def adopt_node(self, iface_id: int, name: str, start: bool) -> None: - """ - Adopt this interface to the provided node, configuring and associating - with the node as needed. - - :param iface_id: interface id for node - :param name: name of interface fo rnode - :param start: True to start interface, False otherwise - :return: nothing - """ - if start: - self.startup() - self.net_client.device_ns(self.name, str(self.node.pid)) - self.node.node_net_client.checksums_off(self.name) - self.flow_id = self.node.node_net_client.get_ifindex(self.name) - logger.debug("interface flow index: %s - %s", self.name, self.flow_id) - mac = self.node.node_net_client.get_mac(self.name) - logger.debug("interface mac: %s - %s", self.name, mac) - self.set_mac(mac) - self.node.node_net_client.device_name(self.name, name) - self.name = name - try: - self.node.add_iface(self, iface_id) - except CoreError as e: - self.shutdown() - raise e - - def startup(self) -> None: - """ - Interface startup logic. - - :return: nothing - :raises CoreCommandError: when there is a command exception - """ - self.net_client.create_veth(self.localname, self.name) - if self.mtu > 0: - self.net_client.set_mtu(self.name, self.mtu) - self.net_client.set_mtu(self.localname, self.mtu) - self.net_client.device_up(self.localname) - self.up = True - - def shutdown(self) -> None: - """ - Interface shutdown logic. - - :return: nothing - """ - if not self.up: - return - if self.node: - try: - self.node.node_net_client.device_flush(self.name) - except CoreCommandError: - pass - if self.localname: - try: - self.net_client.delete_device(self.localname) - except CoreCommandError: - pass - self.up = False - - -class TunTap(CoreInterface): - """ - TUN/TAP virtual device in TAP mode - """ - - def startup(self) -> None: - """ - Startup logic for a tunnel tap. - - :return: nothing - """ - # TODO: more sophisticated TAP creation here - # Debian does not support -p (tap) option, RedHat does. - # For now, this is disabled to allow the TAP to be created by another - # system (e.g. EMANE"s emanetransportd) - # check_call(["tunctl", "-t", self.name]) - # self.install() - self.up = True - - def shutdown(self) -> None: - """ - Shutdown functionality for a tunnel tap. - - :return: nothing - """ - if not self.up: - return - try: - self.node.node_net_client.device_flush(self.name) - except CoreCommandError: - logger.exception("error shutting down tunnel tap") - self.up = False - - def waitfor( - self, func: Callable[[], int], attempts: int = 10, maxretrydelay: float = 0.25 - ) -> bool: - """ - Wait for func() to return zero with exponential backoff. - - :param func: function to wait for a result of zero - :param attempts: number of attempts to wait for a zero result - :param maxretrydelay: maximum retry delay - :return: True if wait succeeded, False otherwise - """ - delay = 0.01 - result = False - for i in range(1, attempts + 1): - r = func() - if r == 0: - result = True - break - msg = f"attempt {i} failed with nonzero exit status {r}" - if i < attempts + 1: - msg += ", retrying..." - logger.info(msg) - time.sleep(delay) - delay += delay - if delay > maxretrydelay: - delay = maxretrydelay - else: - msg += ", giving up" - logger.info(msg) - - return result - - def waitfordevicelocal(self) -> None: - """ - Check for presence of a local device - tap device may not - appear right away waits - - :return: wait for device local response - """ - logger.debug("waiting for device local: %s", self.localname) - - def localdevexists(): - try: - self.net_client.device_show(self.localname) - return 0 - except CoreCommandError: - return 1 - - self.waitfor(localdevexists) - - def waitfordevicenode(self) -> None: - """ - Check for presence of a node device - tap device may not appear right away waits. - - :return: nothing - """ - logger.debug("waiting for device node: %s", self.name) - - def nodedevexists(): - try: - self.node.node_net_client.device_show(self.name) - return 0 - except CoreCommandError: - return 1 - - count = 0 - while True: - result = self.waitfor(nodedevexists) - if result: - break - - # TODO: emane specific code - # check if this is an EMANE interface; if so, continue - # waiting if EMANE is still running - should_retry = count < 5 - is_emane = self.session.emane.is_emane_net(self.net) - is_emane_running = self.session.emane.emanerunning(self.node) - if all([should_retry, is_emane, is_emane_running]): - count += 1 - else: - raise RuntimeError("node device failed to exist") - - def install(self) -> None: - """ - Install this TAP into its namespace. This is not done from the - startup() method but called at a later time when a userspace - program (running on the host) has had a chance to open the socket - end of the TAP. - - :return: nothing - :raises CoreCommandError: when there is a command exception - """ - self.waitfordevicelocal() - netns = str(self.node.pid) - self.net_client.device_ns(self.localname, netns) - self.node.node_net_client.device_name(self.localname, self.name) - self.node.node_net_client.device_up(self.name) - - def set_ips(self) -> None: - """ - Set interface ip addresses. - - :return: nothing - """ - self.waitfordevicenode() - for ip in self.ips(): - self.node.node_net_client.create_address(self.name, str(ip)) + ip6_addr = str(ip6.ip) if ip6 else None + ip6_mask = ip6.prefixlen if ip6 else None + mac = str(self.mac) if self.mac else None + return InterfaceData( + id=self.id, + name=self.name, + mac=mac, + ip4=ip4_addr, + ip4_mask=ip4_mask, + ip6=ip6_addr, + ip6_mask=ip6_mask, + ) class GreTap(CoreInterface): @@ -594,7 +358,7 @@ class GreTap(CoreInterface): """ Creates a GreTap instance. - :param session: core session instance + :param session: session for this gre tap :param remoteip: remote address :param key: gre tap key :param node: related core node @@ -612,7 +376,7 @@ class GreTap(CoreInterface): sessionid = session.short_session_id() localname = f"gt.{self.id}.{sessionid}" name = f"{localname}p" - super().__init__(session, name, localname, mtu, server, node) + super().__init__(0, name, localname, session.use_ovs(), mtu, node, server) self.transport_type: TransportType = TransportType.RAW self.remote_ip: str = remoteip self.ttl: int = ttl diff --git a/daemon/core/nodes/lxd.py b/daemon/core/nodes/lxd.py index 54fc8341..e755e9c9 100644 --- a/daemon/core/nodes/lxd.py +++ b/daemon/core/nodes/lxd.py @@ -6,6 +6,7 @@ from tempfile import NamedTemporaryFile from typing import TYPE_CHECKING, Callable, Dict, Optional from core import utils +from core.emulator.data import InterfaceData, LinkOptions from core.emulator.distributed import DistributedServer from core.emulator.enumerations import NodeTypes from core.errors import CoreCommandError @@ -89,10 +90,9 @@ class LxcNode(CoreNode): will run on, default is None for localhost :param image: image to start container with """ - if image is None: - image = "ubuntu" - self.image: str = image super().__init__(session, _id, name, directory, server) + self.image: str = image if image is not None else "ubuntu" + self.client: Optional[LxdClient] = None def alive(self) -> bool: """ @@ -125,7 +125,6 @@ class LxcNode(CoreNode): # nothing to do if node is not up if not self.up: return - with self.lock: self.ifaces.clear() self.client.stop_container() @@ -212,7 +211,10 @@ class LxcNode(CoreNode): if mode is not None: self.cmd(f"chmod {mode:o} {dst_path}") - def add_iface(self, iface: CoreInterface, iface_id: int) -> None: - super().add_iface(iface, iface_id) + def create_iface( + self, iface_data: InterfaceData = None, options: LinkOptions = None + ) -> CoreInterface: + iface = super().create_iface(iface_data, options) # adding small delay to allow time for adding addresses to work correctly time.sleep(0.5) + return iface diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 0bff5a7d..eb1e4657 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -22,8 +22,8 @@ from core.emulator.enumerations import ( ) from core.errors import CoreCommandError, CoreError from core.executables import NFTABLES -from core.nodes.base import CoreNetworkBase, CoreNode -from core.nodes.interface import CoreInterface, GreTap, Veth +from core.nodes.base import CoreNetworkBase +from core.nodes.interface import CoreInterface, GreTap from core.nodes.netclient import get_net_client logger = logging.getLogger(__name__) @@ -280,6 +280,17 @@ class CoreNetwork(CoreNetworkBase): self.up = True nft_queue.start() + def adopt_iface(self, iface: CoreInterface, name: str) -> None: + """ + Adopt interface and set it to use this bridge as master. + + :param iface: interface to adpopt + :param name: formal name for interface + :return: nothing + """ + iface.net_client.set_iface_master(self.brname, iface.name) + iface.set_config() + def shutdown(self) -> None: """ Linux bridge shutdown logic. @@ -309,9 +320,9 @@ class CoreNetwork(CoreNetworkBase): :param iface: network interface to attach :return: nothing """ + super().attach(iface) if self.up: iface.net_client.set_iface_master(self.brname, iface.localname) - super().attach(iface) def detach(self, iface: CoreInterface) -> None: """ @@ -320,9 +331,9 @@ class CoreNetwork(CoreNetworkBase): :param iface: network interface to detach :return: nothing """ + super().detach(iface) if self.up: iface.net_client.delete_iface(self.brname, iface.localname) - super().detach(iface) def is_linked(self, iface1: CoreInterface, iface2: CoreInterface) -> bool: """ @@ -378,67 +389,6 @@ class CoreNetwork(CoreNetworkBase): self.linked[iface1][iface2] = True nft_queue.update(self) - def linknet(self, net: CoreNetworkBase) -> CoreInterface: - """ - Link this bridge with another by creating a veth pair and installing - each device into each bridge. - - :param net: network to link with - :return: created interface - """ - sessionid = self.session.short_session_id() - try: - _id = f"{self.id:x}" - except TypeError: - _id = str(self.id) - try: - net_id = f"{net.id:x}" - except TypeError: - net_id = str(net.id) - localname = f"veth{_id}.{net_id}.{sessionid}" - name = f"veth{net_id}.{_id}.{sessionid}" - iface = Veth(self.session, name, localname) - if self.up: - iface.startup() - self.attach(iface) - if net.up and net.brname: - iface.net_client.set_iface_master(net.brname, iface.name) - i = net.next_iface_id() - net.ifaces[i] = iface - with net.linked_lock: - net.linked[iface] = {} - iface.net = self - iface.othernet = net - return iface - - def get_linked_iface(self, net: CoreNetworkBase) -> Optional[CoreInterface]: - """ - Return the interface of that links this net with another net - (that were linked using linknet()). - - :param net: interface to get link for - :return: interface the provided network is linked to - """ - for iface in self.get_ifaces(): - if iface.othernet == net: - return iface - return None - - def add_ips(self, ips: List[str]) -> None: - """ - Add ip addresses on the bridge in the format "10.0.0.1/24". - - :param ips: ip address to add - :return: nothing - """ - if not self.up: - return - for ip in ips: - self.net_client.create_address(self.brname, ip) - - def custom_iface(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface: - raise CoreError(f"{type(self).__name__} does not support, custom interfaces") - class GreTapBridge(CoreNetwork): """ @@ -686,15 +636,6 @@ class CtrlNet(CoreNetwork): super().shutdown() - def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: - """ - Do not include CtrlNet in link messages describing this session. - - :param flags: message flags - :return: list of link data - """ - return [] - class PtpNet(CoreNetwork): """ @@ -714,49 +655,6 @@ class PtpNet(CoreNetwork): raise CoreError("ptp links support at most 2 network interfaces") super().attach(iface) - def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: - """ - Get peer to peer link. - - :param flags: message flags - :return: list of link data - """ - all_links = [] - if len(self.ifaces) != 2: - return all_links - ifaces = self.get_ifaces() - iface1 = ifaces[0] - iface2 = ifaces[1] - unidirectional = 0 if iface1.local_options == iface2.local_options else 1 - iface1_data = iface1.get_data() - iface2_data = iface2.get_data() - link_data = LinkData( - message_type=flags, - type=self.linktype, - node1_id=iface1.node.id, - node2_id=iface2.node.id, - iface1=iface1_data, - iface2=iface2_data, - options=iface1.local_options, - ) - link_data.options.unidirectional = unidirectional - all_links.append(link_data) - # build a 2nd link message for the upstream link parameters - # (swap if1 and if2) - if unidirectional: - link_data = LinkData( - message_type=MessageFlags.NONE, - type=self.linktype, - node1_id=iface2.node.id, - node2_id=iface1.node.id, - iface1=InterfaceData(id=iface2_data.id), - iface2=InterfaceData(id=iface1_data.id), - options=iface2.local_options, - ) - link_data.options.unidirectional = unidirectional - all_links.append(link_data) - return all_links - class SwitchNode(CoreNetwork): """ @@ -883,10 +781,10 @@ class WlanNode(CoreNetwork): :param flags: message flags :return: list of link data """ - links = super().links(flags) if self.model: - links.extend(self.model.links(flags)) - return links + return self.model.links(flags) + else: + return [] class TunnelNode(GreTapBridge): diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 0a686da8..ae1b07af 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -3,17 +3,18 @@ PhysicalNode class for including real systems in the emulated network. """ import logging -import threading from pathlib import Path from typing import TYPE_CHECKING, List, Optional, Tuple -from core.emulator.data import InterfaceData +import netaddr + +from core.emulator.data import InterfaceData, LinkOptions from core.emulator.distributed import DistributedServer from core.emulator.enumerations import NodeTypes, TransportType from core.errors import CoreCommandError, CoreError -from core.executables import MOUNT, TEST, UMOUNT -from core.nodes.base import CoreNetworkBase, CoreNodeBase -from core.nodes.interface import DEFAULT_MTU, CoreInterface +from core.executables import BASH, TEST, UMOUNT +from core.nodes.base import CoreNode, CoreNodeBase +from core.nodes.interface import CoreInterface logger = logging.getLogger(__name__) @@ -21,185 +22,6 @@ if TYPE_CHECKING: from core.emulator.session import Session -class PhysicalNode(CoreNodeBase): - def __init__( - self, - session: "Session", - _id: int = None, - name: str = None, - directory: Path = None, - server: DistributedServer = None, - ) -> None: - super().__init__(session, _id, name, server) - if not self.server: - raise CoreError("physical nodes must be assigned to a remote server") - self.directory: Optional[Path] = directory - self.lock: threading.RLock = threading.RLock() - self._mounts: List[Tuple[Path, Path]] = [] - - def startup(self) -> None: - with self.lock: - self.makenodedir() - self.up = True - - def shutdown(self) -> None: - if not self.up: - return - with self.lock: - while self._mounts: - _, target_path = self._mounts.pop(-1) - self.umount(target_path) - for iface in self.get_ifaces(): - iface.shutdown() - self.rmnodedir() - - def path_exists(self, path: str) -> bool: - """ - Determines if a file or directory path exists. - - :param path: path to file or directory - :return: True if path exists, False otherwise - """ - try: - self.host_cmd(f"{TEST} -e {path}") - return True - except CoreCommandError: - return False - - def termcmdstring(self, sh: str = "/bin/sh") -> str: - """ - Create a terminal command string. - - :param sh: shell to execute command in - :return: str - """ - return sh - - def set_mac(self, iface_id: int, mac: str) -> None: - """ - Set mac address for an interface. - - :param iface_id: index of interface to set hardware address for - :param mac: mac address to set - :return: nothing - :raises CoreCommandError: when a non-zero exit status occurs - """ - iface = self.get_iface(iface_id) - iface.set_mac(mac) - if self.up: - self.net_client.device_mac(iface.name, str(iface.mac)) - - def add_ip(self, iface_id: int, ip: str) -> None: - """ - Add an ip address to an interface in the format "10.0.0.1/24". - - :param iface_id: id of interface to add address to - :param ip: address to add to interface - :return: nothing - :raises CoreError: when ip address provided is invalid - :raises CoreCommandError: when a non-zero exit status occurs - """ - iface = self.get_iface(iface_id) - iface.add_ip(ip) - if self.up: - self.net_client.create_address(iface.name, ip) - - def remove_ip(self, iface_id: int, ip: str) -> None: - """ - Remove an ip address from an interface in the format "10.0.0.1/24". - - :param iface_id: id of interface to delete address from - :param ip: ip address to remove from interface - :return: nothing - :raises CoreError: when ip address provided is invalid - :raises CoreCommandError: when a non-zero exit status occurs - """ - iface = self.get_iface(iface_id) - iface.remove_ip(ip) - if self.up: - self.net_client.delete_address(iface.name, ip) - - def adopt_iface( - self, iface: CoreInterface, iface_id: int, mac: str, ips: List[str] - ) -> None: - """ - When a link message is received linking this node to another part of - the emulation, no new interface is created; instead, adopt the - GreTap interface as the node interface. - """ - iface.name = f"gt{iface_id}" - iface.node = self - self.add_iface(iface, iface_id) - # use a more reasonable name, e.g. "gt0" instead of "gt.56286.150" - if self.up: - self.net_client.device_down(iface.localname) - self.net_client.device_name(iface.localname, iface.name) - iface.localname = iface.name - if mac: - self.set_mac(iface_id, mac) - for ip in ips: - self.add_ip(iface_id, ip) - if self.up: - self.net_client.device_up(iface.localname) - - def next_iface_id(self) -> int: - with self.lock: - while self.iface_id in self.ifaces: - self.iface_id += 1 - iface_id = self.iface_id - self.iface_id += 1 - return iface_id - - def new_iface( - self, net: CoreNetworkBase, iface_data: InterfaceData - ) -> CoreInterface: - logger.info("creating interface") - ips = iface_data.get_ips() - iface_id = iface_data.id - if iface_id is None: - iface_id = self.next_iface_id() - name = iface_data.name - if name is None: - name = f"gt{iface_id}" - _, remote_tap = self.session.distributed.create_gre_tunnel( - net, self.server, iface_data.mtu, self.up - ) - self.adopt_iface(remote_tap, iface_id, iface_data.mac, ips) - return remote_tap - - def privatedir(self, dir_path: Path) -> None: - if not str(dir_path).startswith("/"): - raise CoreError(f"private directory path not fully qualified: {dir_path}") - host_path = self.host_path(dir_path, is_dir=True) - self.host_cmd(f"mkdir -p {host_path}") - self.mount(host_path, dir_path) - - def mount(self, src_path: Path, target_path: Path) -> None: - logger.debug("node(%s) mounting: %s at %s", self.name, src_path, target_path) - self.cmd(f"mkdir -p {target_path}") - self.host_cmd(f"{MOUNT} --bind {src_path} {target_path}", cwd=self.directory) - self._mounts.append((src_path, target_path)) - - def umount(self, target_path: Path) -> None: - logger.info("unmounting '%s'", target_path) - try: - self.host_cmd(f"{UMOUNT} -l {target_path}", cwd=self.directory) - except CoreCommandError: - logger.exception("unmounting failed for %s", target_path) - - def cmd(self, args: str, wait: bool = True, shell: bool = False) -> str: - return self.host_cmd(args, wait=wait) - - def create_dir(self, dir_path: Path) -> None: - raise CoreError("physical node does not support creating directories") - - def create_file(self, file_path: Path, contents: str, mode: int = 0o644) -> None: - raise CoreError("physical node does not support creating files") - - def copy_file(self, src_path: Path, dst_path: Path, mode: int = None) -> None: - raise CoreError("physical node does not support copying files") - - class Rj45Node(CoreNodeBase): """ RJ45Node is a physical interface on the host linked to the emulated @@ -214,7 +36,6 @@ class Rj45Node(CoreNodeBase): session: "Session", _id: int = None, name: str = None, - mtu: int = DEFAULT_MTU, server: DistributedServer = None, ) -> None: """ @@ -223,17 +44,14 @@ class Rj45Node(CoreNodeBase): :param session: core session instance :param _id: node id :param name: node name - :param mtu: rj45 mtu :param server: remote server node will run on, default is None for localhost """ super().__init__(session, _id, name, server) self.iface: CoreInterface = CoreInterface( - session, name, name, mtu, server, self + self.iface_id, name, name, session.use_ovs(), node=self, server=server ) self.iface.transport_type = TransportType.RAW - self.lock: threading.RLock = threading.RLock() - self.iface_id: Optional[int] = None self.old_up: bool = False self.old_addrs: List[Tuple[str, Optional[str]]] = [] @@ -245,7 +63,7 @@ class Rj45Node(CoreNodeBase): :raises CoreCommandError: when there is a command exception """ # interface will also be marked up during net.attach() - self.savestate() + self.save_state() self.net_client.device_up(self.iface.localname) self.up = True @@ -266,7 +84,7 @@ class Rj45Node(CoreNodeBase): except CoreCommandError: pass self.up = False - self.restorestate() + self.restore_state() def path_exists(self, path: str) -> bool: """ @@ -281,33 +99,28 @@ class Rj45Node(CoreNodeBase): except CoreCommandError: return False - def new_iface( - self, net: CoreNetworkBase, iface_data: InterfaceData + def create_iface( + self, iface_data: InterfaceData = None, options: LinkOptions = None ) -> CoreInterface: - """ - This is called when linking with another node. Since this node - represents an interface, we do not create another object here, - but attach ourselves to the given network. - - :param net: new network instance - :param iface_data: interface data for new interface - :return: interface index - :raises ValueError: when an interface has already been created, one max - """ with self.lock: - iface_id = iface_data.id - if iface_id is None: - iface_id = 0 - if self.iface.net is not None: + if self.iface.id in self.ifaces: raise CoreError( - f"RJ45({self.name}) nodes support at most 1 network interface" + f"rj45({self.name}) nodes support at most 1 network interface" ) - self.ifaces[iface_id] = self.iface - self.iface_id = iface_id - self.iface.attachnet(net) + if iface_data and iface_data.mtu is not None: + self.iface.mtu = iface_data.mtu + self.iface.ip4s.clear() + self.iface.ip6s.clear() for ip in iface_data.get_ips(): - self.add_ip(ip) - return self.iface + self.iface.add_ip(ip) + self.ifaces[self.iface.id] = self.iface + if self.up: + for ip in self.iface.ips(): + self.net_client.create_address(self.iface.name, str(ip)) + return self.iface + + def adopt_iface(self, iface: CoreInterface, name: str) -> None: + raise CoreError(f"rj45({self.name}) does not support adopt interface") def delete_iface(self, iface_id: int) -> None: """ @@ -318,16 +131,10 @@ class Rj45Node(CoreNodeBase): """ self.get_iface(iface_id) self.ifaces.pop(iface_id) - if self.iface.net is None: - raise CoreError( - f"RJ45({self.name}) is not currently connected to a network" - ) - self.iface.detachnet() - self.iface.net = None self.shutdown() def get_iface(self, iface_id: int) -> CoreInterface: - if iface_id != self.iface_id or iface_id not in self.ifaces: + if iface_id not in self.ifaces: raise CoreError(f"node({self.name}) interface({iface_id}) does not exist") return self.iface @@ -341,42 +148,17 @@ class Rj45Node(CoreNodeBase): """ if iface is not self.iface: raise CoreError(f"node({self.name}) does not have interface({iface.name})") - return self.iface_id + return self.iface.id - def add_ip(self, ip: str) -> None: - """ - Add an ip address to an interface in the format "10.0.0.1/24". - - :param ip: address to add to interface - :return: nothing - :raises CoreError: when ip address provided is invalid - :raises CoreCommandError: when a non-zero exit status occurs - """ - self.iface.add_ip(ip) - if self.up: - self.net_client.create_address(self.name, ip) - - def remove_ip(self, ip: str) -> None: - """ - Remove an ip address from an interface in the format "10.0.0.1/24". - - :param ip: ip address to remove from interface - :return: nothing - :raises CoreError: when ip address provided is invalid - :raises CoreCommandError: when a non-zero exit status occurs - """ - self.iface.remove_ip(ip) - if self.up: - self.net_client.delete_address(self.name, ip) - - def savestate(self) -> None: + def save_state(self) -> None: """ Save the addresses and other interface state before using the - interface for emulation purposes. TODO: save/restore the PROMISC flag + interface for emulation purposes. :return: nothing :raises CoreCommandError: when there is a command exception """ + # TODO: save/restore the PROMISC flag self.old_up = False self.old_addrs: List[Tuple[str, Optional[str]]] = [] localname = self.iface.localname @@ -397,7 +179,7 @@ class Rj45Node(CoreNodeBase): self.old_addrs.append((items[1], None)) logger.info("saved rj45 state: addrs(%s) up(%s)", self.old_addrs, self.old_up) - def restorestate(self) -> None: + def restore_state(self) -> None: """ Restore the addresses and other interface state after using it. @@ -437,3 +219,69 @@ class Rj45Node(CoreNodeBase): def copy_file(self, src_path: Path, dst_path: Path, mode: int = None) -> None: raise CoreError("rj45 does not support copying files") + + +class PhysicalNode(CoreNode): + def __init__( + self, + session: "Session", + _id: int = None, + name: str = None, + directory: Path = None, + server: DistributedServer = None, + ) -> None: + if not self.server: + raise CoreError("physical nodes must be assigned to a remote server") + super().__init__(session, _id, name, directory, server) + + def startup(self) -> None: + with self.lock: + self.makenodedir() + self.up = True + + def shutdown(self) -> None: + if not self.up: + return + with self.lock: + while self._mounts: + _, target_path = self._mounts.pop(-1) + self.umount(target_path) + for iface in self.get_ifaces(): + iface.shutdown() + self.rmnodedir() + + def _create_cmd(self, args: str, shell: bool = False) -> str: + if shell: + args = f'{BASH} -c "{args}"' + return args + + def adopt_iface(self, iface: CoreInterface, name: str) -> None: + # validate iface belongs to node and get id + iface_id = self.get_iface_id(iface) + if iface_id == -1: + raise CoreError(f"adopting unknown iface({iface.name})") + # turn checksums off + self.node_net_client.checksums_off(iface.name) + # retrieve flow id for container + iface.flow_id = self.node_net_client.get_ifindex(iface.name) + logger.debug("interface flow index: %s - %s", iface.name, iface.flow_id) + if iface.mac: + self.net_client.device_mac(iface.name, str(iface.mac)) + # set all addresses + for ip in iface.ips(): + # ipv4 check + broadcast = None + if netaddr.valid_ipv4(ip): + broadcast = "+" + self.node_net_client.create_address(iface.name, str(ip), broadcast) + # configure iface options + iface.set_config() + # set iface up + self.net_client.device_up(iface.name) + + def umount(self, target_path: Path) -> None: + logger.info("unmounting '%s'", target_path) + try: + self.host_cmd(f"{UMOUNT} -l {target_path}", cwd=self.directory) + except CoreCommandError: + logger.exception("unmounting failed for %s", target_path) diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 647300fc..7edc6511 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, Generic, List, Optional, Type, TypeVar +from typing import TYPE_CHECKING, Any, Dict, Generic, Optional, Type, TypeVar from lxml import etree @@ -8,11 +8,12 @@ import core.nodes.base import core.nodes.physical from core import utils from core.emane.nodes import EmaneNet -from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions +from core.emulator.data import InterfaceData, LinkOptions, NodeOptions from core.emulator.enumerations import EventTypes, NodeTypes from core.errors import CoreXmlError from core.nodes.base import CoreNodeBase, NodeBase from core.nodes.docker import DockerNode +from core.nodes.interface import CoreInterface from core.nodes.lxd import LxcNode from core.nodes.network import CtrlNet, GreTapBridge, WlanNode from core.services.coreservices import CoreService @@ -269,8 +270,8 @@ class CoreXmlWriter: def write_session(self) -> None: # generate xml content - links = self.write_nodes() - self.write_links(links) + self.write_nodes() + self.write_links() self.write_mobility_configs() self.write_emane_configs() self.write_service_configs() @@ -449,8 +450,7 @@ class CoreXmlWriter: if node_types.getchildren(): self.scenario.append(node_types) - def write_nodes(self) -> List[LinkData]: - links = [] + def write_nodes(self) -> None: for node_id in self.session.nodes: node = self.session.nodes[node_id] # network node @@ -464,10 +464,6 @@ class CoreXmlWriter: elif isinstance(node, core.nodes.base.CoreNodeBase): self.write_device(node) - # add known links - links.extend(node.links()) - return links - def write_network(self, node: NodeBase) -> None: # ignore p2p and other nodes that are not part of the api if not node.apitype: @@ -476,15 +472,21 @@ class CoreXmlWriter: network = NetworkElement(self.session, node) self.networks.append(network.element) - def write_links(self, links: List[LinkData]) -> None: + def write_links(self) -> None: link_elements = etree.Element("links") - # add link data - for link_data in links: - # skip basic range links - if link_data.iface1 is None and link_data.iface2 is None: - continue - link_element = self.create_link_element(link_data) + for core_link in self.session.link_manager.links(): + node1, iface1 = core_link.node1, core_link.iface1 + node2, iface2 = core_link.node2, core_link.iface2 + unidirectional = core_link.is_unidirectional() + link_element = self.create_link_element( + node1, iface1, node2, iface2, core_link.options(), unidirectional + ) link_elements.append(link_element) + if unidirectional: + link_element = self.create_link_element( + node2, iface2, node1, iface1, iface2.options, unidirectional + ) + link_elements.append(link_element) if link_elements.getchildren(): self.scenario.append(link_elements) @@ -493,67 +495,71 @@ class CoreXmlWriter: self.devices.append(device.element) def create_iface_element( - self, element_name: str, node_id: int, iface_data: InterfaceData + self, element_name: str, iface: CoreInterface ) -> etree.Element: iface_element = etree.Element(element_name) - node = self.session.get_node(node_id, NodeBase) - if isinstance(node, CoreNodeBase): - iface = node.get_iface(iface_data.id) - # check if emane interface - if isinstance(iface.net, EmaneNet): - 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) - add_attribute(iface_element, "ip4", iface_data.ip4) - add_attribute(iface_element, "ip4_mask", iface_data.ip4_mask) - add_attribute(iface_element, "ip6", iface_data.ip6) - add_attribute(iface_element, "ip6_mask", iface_data.ip6_mask) + # check if interface if connected to emane + if isinstance(iface.node, CoreNodeBase) and isinstance(iface.net, EmaneNet): + nem_id = self.session.emane.get_nem_id(iface) + add_attribute(iface_element, "nem", nem_id) + ip4 = iface.get_ip4() + ip4_mask = None + if ip4: + ip4_mask = ip4.prefixlen + ip4 = str(ip4.ip) + ip6 = iface.get_ip6() + ip6_mask = None + if ip6: + ip6_mask = ip6.prefixlen + ip6 = str(ip6.ip) + add_attribute(iface_element, "id", iface.id) + add_attribute(iface_element, "name", iface.name) + add_attribute(iface_element, "mac", iface.mac) + add_attribute(iface_element, "ip4", ip4) + add_attribute(iface_element, "ip4_mask", ip4_mask) + add_attribute(iface_element, "ip6", ip6) + add_attribute(iface_element, "ip6_mask", ip6_mask) return iface_element - def create_link_element(self, link_data: LinkData) -> etree.Element: + def create_link_element( + self, + node1: NodeBase, + iface1: Optional[CoreInterface], + node2: NodeBase, + iface2: Optional[CoreInterface], + options: LinkOptions, + unidirectional: bool, + ) -> etree.Element: link_element = etree.Element("link") - add_attribute(link_element, "node1", link_data.node1_id) - add_attribute(link_element, "node2", link_data.node2_id) - + add_attribute(link_element, "node1", node1.id) + add_attribute(link_element, "node2", node2.id) # check for interface one - if link_data.iface1 is not None: - iface1 = self.create_iface_element( - "iface1", link_data.node1_id, link_data.iface1 - ) + if iface1 is not None: + iface1 = self.create_iface_element("iface1", iface1) link_element.append(iface1) - # check for interface two - if link_data.iface2 is not None: - iface2 = self.create_iface_element( - "iface2", link_data.node2_id, link_data.iface2 - ) + if iface2 is not None: + iface2 = self.create_iface_element("iface2", iface2) link_element.append(iface2) - # check for options, don't write for emane/wlan links - node1 = self.session.get_node(link_data.node1_id, NodeBase) - node2 = self.session.get_node(link_data.node2_id, NodeBase) is_node1_wireless = isinstance(node1, (WlanNode, EmaneNet)) is_node2_wireless = isinstance(node2, (WlanNode, EmaneNet)) - if not any([is_node1_wireless, is_node2_wireless]): - options_data = link_data.options - options = etree.Element("options") - add_attribute(options, "delay", options_data.delay) - add_attribute(options, "bandwidth", options_data.bandwidth) - add_attribute(options, "loss", options_data.loss) - add_attribute(options, "dup", options_data.dup) - add_attribute(options, "jitter", options_data.jitter) - add_attribute(options, "mer", options_data.mer) - add_attribute(options, "burst", options_data.burst) - add_attribute(options, "mburst", options_data.mburst) - add_attribute(options, "unidirectional", options_data.unidirectional) - add_attribute(options, "network_id", link_data.network_id) - add_attribute(options, "key", options_data.key) - add_attribute(options, "buffer", options_data.buffer) - if options.items(): - link_element.append(options) - + if not (is_node1_wireless or is_node2_wireless): + unidirectional = 1 if unidirectional else 0 + options_element = etree.Element("options") + add_attribute(options_element, "delay", options.delay) + add_attribute(options_element, "bandwidth", options.bandwidth) + add_attribute(options_element, "loss", options.loss) + add_attribute(options_element, "dup", options.dup) + add_attribute(options_element, "jitter", options.jitter) + add_attribute(options_element, "mer", options.mer) + add_attribute(options_element, "burst", options.burst) + add_attribute(options_element, "mburst", options.mburst) + add_attribute(options_element, "unidirectional", unidirectional) + add_attribute(options_element, "key", options.key) + add_attribute(options_element, "buffer", options.buffer) + if options_element.items(): + link_element.append(options_element) return link_element diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index a85a364e..c364ce24 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -61,6 +61,8 @@ service CoreApi { } rpc DeleteLink (DeleteLinkRequest) returns (DeleteLinkResponse) { } + rpc Linked (LinkedRequest) returns (LinkedResponse) { + } // mobility rpc rpc GetMobilityConfig (mobility.GetMobilityConfigRequest) returns (mobility.GetMobilityConfigResponse) { @@ -683,3 +685,15 @@ message Server { string name = 1; string host = 2; } + +message LinkedRequest { + int32 session_id = 1; + int32 node1_id = 2; + int32 node2_id = 3; + int32 iface1_id = 4; + int32 iface2_id = 5; + bool linked = 6; +} + +message LinkedResponse { +} diff --git a/daemon/pyproject.toml b/daemon/pyproject.toml index a92cc994..9e11d3f6 100644 --- a/daemon/pyproject.toml +++ b/daemon/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "core" -version = "8.1.0" +version = "8.2.0" description = "CORE Common Open Research Emulator" authors = ["Boeing Research and Technology"] license = "BSD-2-Clause" diff --git a/daemon/scripts/core-cli b/daemon/scripts/core-cli index fbbe6ede..6f2c1f5b 100755 --- a/daemon/scripts/core-cli +++ b/daemon/scripts/core-cli @@ -1,22 +1,24 @@ #!/usr/bin/env python3 +import json import sys from argparse import ( ArgumentDefaultsHelpFormatter, ArgumentParser, ArgumentTypeError, Namespace, - _SubParsersAction, ) from functools import wraps from pathlib import Path -from typing import Optional, Tuple +from typing import Any, Dict, Optional, Tuple import grpc import netaddr +from google.protobuf.json_format import MessageToDict from netaddr import EUI, AddrFormatError, IPNetwork from core.api.grpc.client import CoreGrpcClient from core.api.grpc.wrappers import ( + ConfigOption, Geo, Interface, Link, @@ -29,6 +31,15 @@ from core.api.grpc.wrappers import ( NODE_TYPES = [x for x in NodeType if x != NodeType.PEER_TO_PEER] +def protobuf_to_json(message: Any) -> Dict[str, Any]: + return MessageToDict(message, including_default_value_fields=True, preserving_proto_field_name=True) + + +def print_json(data: Any) -> None: + data = json.dumps(data, indent=2) + print(data) + + def coreclient(func): @wraps(func) def wrapper(*args, **kwargs): @@ -94,11 +105,11 @@ def geo_type(value: str) -> Tuple[float, float, float]: return lon, lat, alt -def file_type(value: str) -> str: +def file_type(value: str) -> Path: path = Path(value) if not path.is_file(): raise ArgumentTypeError(f"invalid file: {value}") - return str(path.absolute()) + return path def get_current_session(core: CoreGrpcClient, session_id: Optional[int]) -> int: @@ -140,12 +151,15 @@ def print_iface(iface: Interface) -> None: def get_wlan_config(core: CoreGrpcClient, args: Namespace) -> None: session_id = get_current_session(core, args.session) config = core.get_wlan_config(session_id, args.node) - size = 0 - for option in config.values(): - size = max(size, len(option.name)) - print(f"{'Name':<{size}.{size}} | Value") - for option in config.values(): - print(f"{option.name:<{size}.{size}} | {option.value}") + if args.json: + print_json(ConfigOption.to_dict(config)) + else: + size = 0 + for option in config.values(): + size = max(size, len(option.name)) + print(f"{'Name':<{size}.{size}} | Value") + for option in config.values(): + print(f"{option.name:<{size}.{size}} | {option.value}") @coreclient @@ -163,80 +177,102 @@ def set_wlan_config(core: CoreGrpcClient, args: Namespace) -> None: if args.range: config["range"] = str(args.range) result = core.set_wlan_config(session_id, args.node, config) - print(f"set wlan config: {result}") + if args.json: + print_json(dict(result=result)) + else: + print(f"set wlan config: {result}") @coreclient def open_xml(core: CoreGrpcClient, args: Namespace) -> None: result, session_id = core.open_xml(args.file, args.start) - print(f"opened xml: {result},{session_id}") + if args.json: + print_json(dict(result=result, session_id=session_id)) + else: + print(f"opened xml: {result},{session_id}") @coreclient def query_sessions(core: CoreGrpcClient, args: Namespace) -> None: sessions = core.get_sessions() - print("Session ID | Session State | Nodes") - for session in sessions: - print(f"{session.id:<10} | {session.state.name:<13} | {session.nodes}") + if args.json: + sessions = [protobuf_to_json(x.to_proto()) for x in sessions] + print_json(sessions) + else: + print("Session ID | Session State | Nodes") + for session in sessions: + print(f"{session.id:<10} | {session.state.name:<13} | {session.nodes}") @coreclient def query_session(core: CoreGrpcClient, args: Namespace) -> None: session = core.get_session(args.id) - print("Nodes") - print("Node ID | Node Name | Node Type") - for node in session.nodes.values(): - print(f"{node.id:<7} | {node.name:<9} | {node.type.name}") - print("\nLinks") - for link in session.links: - n1 = session.nodes[link.node1_id].name - n2 = session.nodes[link.node2_id].name - print(f"Node | ", end="") - print_iface_header() - print(f"{n1:<6} | ", end="") - if link.iface1: - print_iface(link.iface1) - else: + if args.json: + session = protobuf_to_json(session.to_proto()) + print_json(session) + else: + print("Nodes") + print("ID | Name | Type | XY | Geo") + for node in session.nodes.values(): + xy_pos = f"{int(node.position.x)},{int(node.position.y)}" + geo_pos = f"{node.geo.lon:.7f},{node.geo.lat:.7f},{node.geo.alt:f}" + print(f"{node.id:<7} | {node.name[:7]:<7} | {node.type.name[:7]:<7} | {xy_pos:<9} | {geo_pos}") + print("\nLinks") + for link in session.links: + n1 = session.nodes[link.node1_id].name + n2 = session.nodes[link.node2_id].name + print(f"Node | ", end="") + print_iface_header() + print(f"{n1:<6} | ", end="") + if link.iface1: + print_iface(link.iface1) + else: + print() + print(f"{n2:<6} | ", end="") + if link.iface2: + print_iface(link.iface2) + else: + print() print() - print(f"{n2:<6} | ", end="") - if link.iface2: - print_iface(link.iface2) - else: - print() - print() @coreclient def query_node(core: CoreGrpcClient, args: Namespace) -> None: session = core.get_session(args.id) node, ifaces, _ = core.get_node(args.id, args.node) - print("ID | Name | Type | XY") - xy_pos = f"{int(node.position.x)},{int(node.position.y)}" - print(f"{node.id:<4} | {node.name[:7]:<7} | {node.type.name[:7]:<7} | {xy_pos}") - if node.geo: - print("Geo") - print(f"{node.geo.lon:.7f},{node.geo.lat:.7f},{node.geo.alt:f}") - if ifaces: - print("Interfaces") - print("Connected To | ", end="") - print_iface_header() - for iface in ifaces: - if iface.net_id == node.id: - if iface.node_id: - name = session.nodes[iface.node_id].name + if args.json: + node = protobuf_to_json(node.to_proto()) + ifaces = [protobuf_to_json(x.to_proto()) for x in ifaces] + print_json(dict(node=node, ifaces=ifaces)) + else: + print("ID | Name | Type | XY | Geo") + xy_pos = f"{int(node.position.x)},{int(node.position.y)}" + geo_pos = f"{node.geo.lon:.7f},{node.geo.lat:.7f},{node.geo.alt:f}" + print(f"{node.id:<7} | {node.name[:7]:<7} | {node.type.name[:7]:<7} | {xy_pos:<9} | {geo_pos}") + if ifaces: + print("Interfaces") + print("Connected To | ", end="") + print_iface_header() + for iface in ifaces: + if iface.net_id == node.id: + if iface.node_id: + name = session.nodes[iface.node_id].name + else: + name = session.nodes[iface.net2_id].name else: - name = session.nodes[iface.net2_id].name - else: - net_node = session.nodes.get(iface.net_id) - name = net_node.name if net_node else "" - print(f"{name:<12} | ", end="") - print_iface(iface) + net_node = session.nodes.get(iface.net_id) + name = net_node.name if net_node else "" + print(f"{name:<12} | ", end="") + print_iface(iface) @coreclient def delete_session(core: CoreGrpcClient, args: Namespace) -> None: result = core.delete_session(args.id) - print(f"delete session({args.id}): {result}") + if args.json: + print_json(dict(result=result)) + else: + print(f"delete session({args.id}): {result}") @coreclient @@ -263,14 +299,20 @@ def add_node(core: CoreGrpcClient, args: Namespace) -> None: geo=geo, ) node_id = core.add_node(session_id, node) - print(f"created node: {node_id}") + if args.json: + print_json(dict(node_id=node_id)) + else: + print(f"created node: {node_id}") @coreclient def edit_node(core: CoreGrpcClient, args: Namespace) -> None: session_id = get_current_session(core, args.session) result = core.edit_node(session_id, args.id, args.icon) - print(f"edit node: {result}") + if args.json: + print_json(dict(result=result)) + else: + print(f"edit node: {result}") @coreclient @@ -285,14 +327,20 @@ def move_node(core: CoreGrpcClient, args: Namespace) -> None: lon, lat, alt = args.geo geo = Geo(lon=lon, lat=lat, alt=alt) result = core.move_node(session_id, args.id, pos, geo) - print(f"move node: {result}") + if args.json: + print_json(dict(result=result)) + else: + print(f"move node: {result}") @coreclient def delete_node(core: CoreGrpcClient, args: Namespace) -> None: session_id = get_current_session(core, args.session) result = core.delete_node(session_id, args.id) - print(f"deleted node: {result}") + if args.json: + print_json(dict(result=result)) + else: + print(f"deleted node: {result}") @coreclient @@ -313,8 +361,13 @@ def add_link(core: CoreGrpcClient, args: Namespace) -> None: unidirectional=args.uni, ) link = Link(args.node1, args.node2, iface1=iface1, iface2=iface2, options=options) - result, _, _ = core.add_link(session_id, link) - print(f"add link: {result}") + result, iface1, iface2 = core.add_link(session_id, link) + if args.json: + iface1 = protobuf_to_json(iface1.to_proto()) + iface2 = protobuf_to_json(iface2.to_proto()) + print_json(dict(result=result, iface1=iface1, iface2=iface2)) + else: + print(f"add link: {result}") @coreclient @@ -332,7 +385,10 @@ def edit_link(core: CoreGrpcClient, args: Namespace) -> None: iface2 = Interface(args.iface2) link = Link(args.node1, args.node2, iface1=iface1, iface2=iface2, options=options) result = core.edit_link(session_id, link) - print(f"edit link: {result}") + if args.json: + print_json(dict(result=result)) + else: + print(f"edit link: {result}") @coreclient @@ -342,10 +398,13 @@ def delete_link(core: CoreGrpcClient, args: Namespace) -> None: iface2 = Interface(args.iface2) link = Link(args.node1, args.node2, iface1=iface1, iface2=iface2) result = core.delete_link(session_id, link) - print(f"delete link: {result}") + if args.json: + print_json(dict(result=result)) + else: + print(f"delete link: {result}") -def setup_sessions_parser(parent: _SubParsersAction) -> None: +def setup_sessions_parser(parent) -> None: parser = parent.add_parser("session", help="session interactions") parser.formatter_class = ArgumentDefaultsHelpFormatter parser.add_argument("-i", "--id", type=int, help="session id to use", required=True) @@ -358,7 +417,7 @@ def setup_sessions_parser(parent: _SubParsersAction) -> None: delete_parser.set_defaults(func=delete_session) -def setup_node_parser(parent: _SubParsersAction) -> None: +def setup_node_parser(parent) -> None: parser = parent.add_parser("node", help="node interactions") parser.formatter_class = ArgumentDefaultsHelpFormatter parser.add_argument("-s", "--session", type=int, help="session to interact with") @@ -402,7 +461,7 @@ def setup_node_parser(parent: _SubParsersAction) -> None: delete_parser.set_defaults(func=delete_node) -def setup_link_parser(parent: _SubParsersAction) -> None: +def setup_link_parser(parent) -> None: parser = parent.add_parser("link", help="link interactions") parser.formatter_class = ArgumentDefaultsHelpFormatter parser.add_argument("-s", "--session", type=int, help="session to interact with") @@ -455,7 +514,7 @@ def setup_link_parser(parent: _SubParsersAction) -> None: delete_parser.set_defaults(func=delete_link) -def setup_query_parser(parent: _SubParsersAction) -> None: +def setup_query_parser(parent) -> None: parser = parent.add_parser("query", help="query interactions") subparsers = parser.add_subparsers(help="query commands") subparsers.required = True @@ -477,7 +536,7 @@ def setup_query_parser(parent: _SubParsersAction) -> None: node_parser.set_defaults(func=query_node) -def setup_xml_parser(parent: _SubParsersAction) -> None: +def setup_xml_parser(parent) -> None: parser = parent.add_parser("xml", help="open session xml") parser.formatter_class = ArgumentDefaultsHelpFormatter parser.add_argument("-f", "--file", type=file_type, help="xml file to open", required=True) @@ -485,7 +544,7 @@ def setup_xml_parser(parent: _SubParsersAction) -> None: parser.set_defaults(func=open_xml) -def setup_wlan_parser(parent: _SubParsersAction) -> None: +def setup_wlan_parser(parent) -> None: parser = parent.add_parser("wlan", help="wlan specific interactions") parser.formatter_class = ArgumentDefaultsHelpFormatter parser.add_argument("-s", "--session", type=int, help="session to interact with") @@ -511,6 +570,9 @@ def setup_wlan_parser(parent: _SubParsersAction) -> None: def main() -> None: parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) + parser.add_argument( + "-js", "--json", action="store_true", help="print responses to terminal as json" + ) subparsers = parser.add_subparsers(help="supported commands") subparsers.required = True subparsers.dest = "command" diff --git a/daemon/tests/test_core.py b/daemon/tests/test_core.py index 3fbd91cb..71a3d972 100644 --- a/daemon/tests/test_core.py +++ b/daemon/tests/test_core.py @@ -9,7 +9,6 @@ from typing import List, Type import pytest from core.emulator.data import IpPrefixes, NodeOptions -from core.emulator.enumerations import MessageFlags from core.emulator.session import Session from core.errors import CoreCommandError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility @@ -63,44 +62,6 @@ class TestCore: status = ping(node1, node2, ip_prefixes) assert not status - def test_iface(self, session: Session, ip_prefixes: IpPrefixes): - """ - Test interface methods. - - :param session: session for test - :param ip_prefixes: generates ip addresses for nodes - """ - - # create ptp - ptp_node = session.add_node(PtpNet) - - # create nodes - node1 = session.add_node(CoreNode) - node2 = session.add_node(CoreNode) - - # link nodes to ptp net - for node in [node1, node2]: - iface = ip_prefixes.create_iface(node) - session.add_link(node.id, ptp_node.id, iface1_data=iface) - - # instantiate session - session.instantiate() - - # check link data gets generated - assert ptp_node.links(MessageFlags.ADD) - - # check common nets exist between linked nodes - assert node1.commonnets(node2) - assert node2.commonnets(node1) - - # check we can retrieve interface id - assert 0 in node1.ifaces - assert 0 in node2.ifaces - - # delete interface and test that if no longer exists - node1.delete_iface(0) - assert 0 not in node1.ifaces - def test_wlan_ping(self, session: Session, ip_prefixes: IpPrefixes): """ Test basic wlan network. diff --git a/daemon/tests/test_distributed.py b/daemon/tests/test_distributed.py index 01362cae..35b7af4e 100644 --- a/daemon/tests/test_distributed.py +++ b/daemon/tests/test_distributed.py @@ -29,12 +29,14 @@ class TestDistributed: # when session.distributed.add_server(server_name, host) + node1 = session.add_node(HubNode) options = NodeOptions(server=server_name) - node = session.add_node(HubNode, options=options) + node2 = session.add_node(HubNode, options=options) + session.add_link(node1.id, node2.id) session.instantiate() # then - assert node.server is not None - assert node.server.name == server_name - assert node.server.host == host - assert len(session.distributed.tunnels) > 0 + assert node2.server is not None + assert node2.server.name == server_name + assert node2.server.host == host + assert len(session.distributed.tunnels) == 1 diff --git a/daemon/tests/test_grpc.py b/daemon/tests/test_grpc.py index 0d7ec930..7e4e8be2 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -34,7 +34,7 @@ from core.api.grpc.wrappers import ( from core.emane.models.ieee80211abg import EmaneIeee80211abgModel from core.emane.nodes import EmaneNet from core.emulator.data import EventData, IpPrefixes, NodeData, NodeOptions -from core.emulator.enumerations import EventTypes, ExceptionLevels +from core.emulator.enumerations import EventTypes, ExceptionLevels, MessageFlags from core.errors import CoreError from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility from core.nodes.base import CoreNode @@ -413,7 +413,7 @@ class TestGrpc: session = grpc_server.coreemu.create_session() switch = session.add_node(SwitchNode) node = session.add_node(CoreNode) - assert len(switch.links()) == 0 + assert len(session.link_manager.links()) == 0 iface = InterfaceHelper("10.0.0.0/24").create_iface(node.id, 0) link = Link(node.id, switch.id, iface1=iface) @@ -423,7 +423,7 @@ class TestGrpc: # then assert result is True - assert len(switch.links()) == 1 + assert len(session.link_manager.links()) == 1 assert iface1.id == iface.id assert iface1.ip4 == iface.ip4 @@ -445,11 +445,10 @@ class TestGrpc: session = grpc_server.coreemu.create_session() switch = session.add_node(SwitchNode) node = session.add_node(CoreNode) - iface = ip_prefixes.create_iface(node) - session.add_link(node.id, switch.id, iface) + iface_data = ip_prefixes.create_iface(node) + iface, _ = session.add_link(node.id, switch.id, iface_data) options = LinkOptions(bandwidth=30000) - link = switch.links()[0] - assert options.bandwidth != link.options.bandwidth + assert iface.options.bandwidth != options.bandwidth link = Link(node.id, switch.id, iface1=Interface(id=iface.id), options=options) # then @@ -458,8 +457,7 @@ class TestGrpc: # then assert result is True - link = switch.links()[0] - assert options.bandwidth == link.options.bandwidth + assert options.bandwidth == iface.options.bandwidth def test_delete_link(self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes): # given @@ -470,13 +468,7 @@ class TestGrpc: node2 = session.add_node(CoreNode) iface2 = ip_prefixes.create_iface(node2) session.add_link(node1.id, node2.id, iface1, iface2) - link_node = None - for node_id in session.nodes: - node = session.nodes[node_id] - if node.id not in {node1.id, node2.id}: - link_node = node - break - assert len(link_node.links()) == 1 + assert len(session.link_manager.links()) == 1 link = Link( node1.id, node2.id, @@ -490,7 +482,7 @@ class TestGrpc: # then assert result is True - assert len(link_node.links()) == 0 + assert len(session.link_manager.links()) == 0 def test_get_wlan_config(self, grpc_server: CoreGrpcServer): # given @@ -755,9 +747,11 @@ class TestGrpc: session = grpc_server.coreemu.create_session() wlan = session.add_node(WlanNode) node = session.add_node(CoreNode) - iface = ip_prefixes.create_iface(node) - session.add_link(node.id, wlan.id, iface) - link_data = wlan.links()[0] + iface_data = ip_prefixes.create_iface(node) + session.add_link(node.id, wlan.id, iface_data) + core_link = list(session.link_manager.links())[0] + link_data = core_link.get_data(MessageFlags.ADD) + queue = Queue() def handle_event(event: Event) -> None: @@ -932,3 +926,26 @@ class TestGrpc: with pytest.raises(grpc.RpcError): with client.context_connect(): client.move_nodes(streamer) + + def test_wlan_link(self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes): + # given + client = CoreGrpcClient() + session = grpc_server.coreemu.create_session() + wlan = session.add_node(WlanNode) + node1 = session.add_node(CoreNode) + node2 = session.add_node(CoreNode) + iface1_data = ip_prefixes.create_iface(node1) + iface2_data = ip_prefixes.create_iface(node2) + session.add_link(node1.id, wlan.id, iface1_data) + session.add_link(node2.id, wlan.id, iface2_data) + session.instantiate() + assert len(session.link_manager.links()) == 2 + + # when + with client.context_connect(): + result1 = client.wlan_link(session.id, wlan.id, node1.id, node2.id, True) + result2 = client.wlan_link(session.id, wlan.id, node1.id, node2.id, False) + + # then + assert result1 is True + assert result2 is True diff --git a/daemon/tests/test_links.py b/daemon/tests/test_links.py index 791eb77a..eea88fb3 100644 --- a/daemon/tests/test_links.py +++ b/daemon/tests/test_links.py @@ -46,14 +46,17 @@ class TestLinks: ) # then + assert len(session.link_manager.links()) == 1 assert node1.get_iface(iface1_data.id) assert node2.get_iface(iface2_data.id) assert iface1 is not None + assert iface1.options == LINK_OPTIONS + assert iface1.has_netem + assert node1.get_iface(iface1_data.id) assert iface2 is not None - assert iface1.local_options == LINK_OPTIONS - assert iface1.has_local_netem - assert iface2.local_options == LINK_OPTIONS - assert iface2.has_local_netem + assert iface2.options == LINK_OPTIONS + assert iface2.has_netem + assert node1.get_iface(iface1_data.id) def test_add_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -62,16 +65,20 @@ class TestLinks: iface1_data = ip_prefixes.create_iface(node1) # when - iface, _ = session.add_link( + iface1, iface2 = session.add_link( node1.id, node2.id, iface1_data=iface1_data, options=LINK_OPTIONS ) # then - assert node2.links() + assert len(session.link_manager.links()) == 1 + assert iface1 is not None + assert iface1.options == LINK_OPTIONS + assert iface1.has_netem assert node1.get_iface(iface1_data.id) - assert iface is not None - assert iface.local_options == LINK_OPTIONS - assert iface.has_local_netem + assert iface2 is not None + assert iface2.options == LINK_OPTIONS + assert iface2.has_netem + assert node2.get_iface(iface1_data.id) def test_add_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -80,32 +87,37 @@ class TestLinks: iface2_data = ip_prefixes.create_iface(node2) # when - _, iface = session.add_link( + iface1, iface2 = session.add_link( node1.id, node2.id, iface2_data=iface2_data, options=LINK_OPTIONS ) # then - assert node1.links() - assert node2.get_iface(iface2_data.id) - assert iface is not None - assert iface.local_options == LINK_OPTIONS - assert iface.has_local_netem + assert len(session.link_manager.links()) == 1 + assert iface1 is not None + assert iface1.options == LINK_OPTIONS + assert iface1.has_netem + assert node1.get_iface(iface1.id) + assert iface2 is not None + assert iface2.options == LINK_OPTIONS + assert iface2.has_netem + assert node2.get_iface(iface2.id) - def test_add_net_to_net(self, session): + def test_add_net_to_net(self, session: Session): # given node1 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode) # when - iface, _ = session.add_link(node1.id, node2.id, options=LINK_OPTIONS) + iface1, iface2 = session.add_link(node1.id, node2.id, options=LINK_OPTIONS) # then - assert node1.links() - assert iface is not None - assert iface.local_options == LINK_OPTIONS - assert iface.options == LINK_OPTIONS - assert iface.has_local_netem - assert iface.has_netem + assert len(session.link_manager.links()) == 1 + assert iface1 is not None + assert iface1.options == LINK_OPTIONS + assert iface1.has_netem + assert iface2 is not None + assert iface2.options == LINK_OPTIONS + assert iface2.has_netem def test_add_node_to_node_uni(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -141,48 +153,52 @@ class TestLinks: ) # then + assert len(session.link_manager.links()) == 1 assert node1.get_iface(iface1_data.id) assert node2.get_iface(iface2_data.id) assert iface1 is not None + assert iface1.options == link_options1 + assert iface1.has_netem assert iface2 is not None - assert iface1.local_options == link_options1 - assert iface1.has_local_netem - assert iface2.local_options == link_options2 - assert iface2.has_local_netem + assert iface2.options == link_options2 + assert iface2.has_netem def test_update_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): # given node1 = session.add_node(CoreNode) node2 = session.add_node(SwitchNode) iface1_data = ip_prefixes.create_iface(node1) - iface1, _ = session.add_link(node1.id, node2.id, iface1_data) - assert iface1.local_options != LINK_OPTIONS + iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data) + assert len(session.link_manager.links()) == 1 + assert iface1.options != LINK_OPTIONS + assert iface2.options != LINK_OPTIONS # when - session.update_link( - node1.id, node2.id, iface1_id=iface1_data.id, options=LINK_OPTIONS - ) + session.update_link(node1.id, node2.id, iface1.id, iface2.id, LINK_OPTIONS) # then - assert iface1.local_options == LINK_OPTIONS - assert iface1.has_local_netem + assert iface1.options == LINK_OPTIONS + assert iface1.has_netem + assert iface2.options == LINK_OPTIONS + assert iface2.has_netem def test_update_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): # given node1 = session.add_node(SwitchNode) node2 = session.add_node(CoreNode) iface2_data = ip_prefixes.create_iface(node2) - _, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data) - assert iface2.local_options != LINK_OPTIONS + iface1, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data) + assert iface1.options != LINK_OPTIONS + assert iface2.options != LINK_OPTIONS # when - session.update_link( - node1.id, node2.id, iface2_id=iface2_data.id, options=LINK_OPTIONS - ) + session.update_link(node1.id, node2.id, iface1.id, iface2.id, LINK_OPTIONS) # then - assert iface2.local_options == LINK_OPTIONS - assert iface2.has_local_netem + assert iface1.options == LINK_OPTIONS + assert iface1.has_netem + assert iface2.options == LINK_OPTIONS + assert iface2.has_netem def test_update_ptp(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -191,55 +207,68 @@ class TestLinks: iface1_data = ip_prefixes.create_iface(node1) iface2_data = ip_prefixes.create_iface(node2) iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data) - assert iface1.local_options != LINK_OPTIONS - assert iface2.local_options != LINK_OPTIONS + assert iface1.options != LINK_OPTIONS + assert iface2.options != LINK_OPTIONS # when - session.update_link( - node1.id, node2.id, iface1_data.id, iface2_data.id, LINK_OPTIONS - ) + session.update_link(node1.id, node2.id, iface1.id, iface2.id, LINK_OPTIONS) # then - assert iface1.local_options == LINK_OPTIONS - assert iface1.has_local_netem - assert iface2.local_options == LINK_OPTIONS - assert iface2.has_local_netem + assert iface1.options == LINK_OPTIONS + assert iface1.has_netem + assert iface2.options == LINK_OPTIONS + assert iface2.has_netem def test_update_net_to_net(self, session: Session, ip_prefixes: IpPrefixes): # given node1 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode) - iface1, _ = session.add_link(node1.id, node2.id) - assert iface1.local_options != LINK_OPTIONS + iface1, iface2 = session.add_link(node1.id, node2.id) + assert iface1.options != LINK_OPTIONS + assert iface2.options != LINK_OPTIONS # when - session.update_link(node1.id, node2.id, options=LINK_OPTIONS) + session.update_link(node1.id, node2.id, iface1.id, iface2.id, LINK_OPTIONS) # then - assert iface1.local_options == LINK_OPTIONS - assert iface1.has_local_netem assert iface1.options == LINK_OPTIONS assert iface1.has_netem + assert iface2.options == LINK_OPTIONS + assert iface2.has_netem + + def test_update_error(self, session: Session, ip_prefixes: IpPrefixes): + # given + node1 = session.add_node(CoreNode) + node2 = session.add_node(CoreNode) + iface1_data = ip_prefixes.create_iface(node1) + iface2_data = ip_prefixes.create_iface(node2) + iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data) + assert iface1.options != LINK_OPTIONS + assert iface2.options != LINK_OPTIONS + + # when + with pytest.raises(CoreError): + session.delete_link(node1.id, INVALID_ID, iface1.id, iface2.id) def test_clear_net_to_net(self, session: Session, ip_prefixes: IpPrefixes): # given node1 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode) - iface1, _ = session.add_link(node1.id, node2.id, options=LINK_OPTIONS) - assert iface1.local_options == LINK_OPTIONS - assert iface1.has_local_netem + iface1, iface2 = session.add_link(node1.id, node2.id, options=LINK_OPTIONS) assert iface1.options == LINK_OPTIONS assert iface1.has_netem + assert iface2.options == LINK_OPTIONS + assert iface2.has_netem # when options = LinkOptions(delay=0, bandwidth=0, loss=0.0, dup=0, jitter=0, buffer=0) - session.update_link(node1.id, node2.id, options=options) + session.update_link(node1.id, node2.id, iface1.id, iface2.id, options) # then - assert iface1.local_options.is_clear() - assert not iface1.has_local_netem assert iface1.options.is_clear() assert not iface1.has_netem + assert iface2.options.is_clear() + assert not iface2.has_netem def test_delete_node_to_node(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -247,82 +276,100 @@ class TestLinks: node2 = session.add_node(CoreNode) iface1_data = ip_prefixes.create_iface(node1) iface2_data = ip_prefixes.create_iface(node2) - session.add_link(node1.id, node2.id, iface1_data, iface2_data) - assert node1.get_iface(iface1_data.id) - assert node2.get_iface(iface2_data.id) + iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data) + assert len(session.link_manager.links()) == 1 + assert node1.get_iface(iface1.id) + assert node2.get_iface(iface2.id) # when - session.delete_link(node1.id, node2.id, iface1_data.id, iface2_data.id) + session.delete_link(node1.id, node2.id, iface1.id, iface2.id) # then - assert iface1_data.id not in node1.ifaces - assert iface2_data.id not in node2.ifaces + assert len(session.link_manager.links()) == 0 + assert iface1.id not in node1.ifaces + assert iface2.id not in node2.ifaces def test_delete_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): # given node1 = session.add_node(CoreNode) node2 = session.add_node(SwitchNode) iface1_data = ip_prefixes.create_iface(node1) - session.add_link(node1.id, node2.id, iface1_data) - assert node1.get_iface(iface1_data.id) + iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data) + assert len(session.link_manager.links()) == 1 + assert node1.get_iface(iface1.id) + assert node2.get_iface(iface2.id) # when - session.delete_link(node1.id, node2.id, iface1_id=iface1_data.id) + session.delete_link(node1.id, node2.id, iface1.id, iface2.id) # then - assert iface1_data.id not in node1.ifaces + assert len(session.link_manager.links()) == 0 + assert iface1.id not in node1.ifaces + assert iface2.id not in node2.ifaces def test_delete_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): # given node1 = session.add_node(SwitchNode) node2 = session.add_node(CoreNode) iface2_data = ip_prefixes.create_iface(node2) - session.add_link(node1.id, node2.id, iface2_data=iface2_data) - assert node2.get_iface(iface2_data.id) + iface1, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data) + assert len(session.link_manager.links()) == 1 + assert node1.get_iface(iface1.id) + assert node2.get_iface(iface2.id) # when - session.delete_link(node1.id, node2.id, iface2_id=iface2_data.id) + session.delete_link(node1.id, node2.id, iface1.id, iface2.id) # then - assert iface2_data.id not in node2.ifaces + assert len(session.link_manager.links()) == 0 + assert iface1.id not in node1.ifaces + assert iface2.id not in node2.ifaces def test_delete_net_to_net(self, session: Session, ip_prefixes: IpPrefixes): # given node1 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode) - session.add_link(node1.id, node2.id) - assert node1.get_linked_iface(node2) + iface1, iface2 = session.add_link(node1.id, node2.id) + assert len(session.link_manager.links()) == 1 + assert node1.get_iface(iface1.id) + assert node2.get_iface(iface2.id) # when - session.delete_link(node1.id, node2.id) + session.delete_link(node1.id, node2.id, iface1.id, iface2.id) # then - assert not node1.get_linked_iface(node2) + assert len(session.link_manager.links()) == 0 + assert iface1.id not in node1.ifaces + assert iface2.id not in node2.ifaces def test_delete_node_error(self, session: Session, ip_prefixes: IpPrefixes): # given node1 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode) - session.add_link(node1.id, node2.id) - assert node1.get_linked_iface(node2) + iface1, iface2 = session.add_link(node1.id, node2.id) + assert len(session.link_manager.links()) == 1 + assert node1.get_iface(iface1.id) + assert node2.get_iface(iface2.id) # when with pytest.raises(CoreError): - session.delete_link(node1.id, INVALID_ID) + session.delete_link(node1.id, INVALID_ID, iface1.id, iface2.id) with pytest.raises(CoreError): - session.delete_link(INVALID_ID, node2.id) + session.delete_link(INVALID_ID, node2.id, iface1.id, iface2.id) def test_delete_net_to_net_error(self, session: Session, ip_prefixes: IpPrefixes): # given node1 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode) node3 = session.add_node(SwitchNode) - session.add_link(node1.id, node2.id) - assert node1.get_linked_iface(node2) + iface1, iface2 = session.add_link(node1.id, node2.id) + assert len(session.link_manager.links()) == 1 + assert node1.get_iface(iface1.id) + assert node2.get_iface(iface2.id) # when with pytest.raises(CoreError): - session.delete_link(node1.id, node3.id) + session.delete_link(node1.id, node3.id, iface1.id, iface2.id) def test_delete_node_to_net_error(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -330,12 +377,14 @@ class TestLinks: node2 = session.add_node(SwitchNode) node3 = session.add_node(SwitchNode) iface1_data = ip_prefixes.create_iface(node1) - iface1, _ = session.add_link(node1.id, node2.id, iface1_data) - assert iface1 + iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data) + assert len(session.link_manager.links()) == 1 + assert node1.get_iface(iface1.id) + assert node2.get_iface(iface2.id) # when with pytest.raises(CoreError): - session.delete_link(node1.id, node3.id) + session.delete_link(node1.id, node3.id, iface1.id, iface2.id) def test_delete_net_to_node_error(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -343,12 +392,14 @@ class TestLinks: node2 = session.add_node(CoreNode) node3 = session.add_node(SwitchNode) iface2_data = ip_prefixes.create_iface(node2) - _, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data) - assert iface2 + iface1, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data) + assert len(session.link_manager.links()) == 1 + assert node1.get_iface(iface1.id) + assert node2.get_iface(iface2.id) # when with pytest.raises(CoreError): - session.delete_link(node1.id, node3.id) + session.delete_link(node1.id, node3.id, iface1.id, iface2.id) def test_delete_node_to_node_error(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -358,9 +409,10 @@ class TestLinks: iface1_data = ip_prefixes.create_iface(node1) iface2_data = ip_prefixes.create_iface(node2) iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data) - assert iface1 - assert iface2 + assert len(session.link_manager.links()) == 1 + assert node1.get_iface(iface1.id) + assert node2.get_iface(iface2.id) # when with pytest.raises(CoreError): - session.delete_link(node1.id, node3.id) + session.delete_link(node1.id, node3.id, iface1.id, iface2.id) diff --git a/daemon/tests/test_nodes.py b/daemon/tests/test_nodes.py index 3f0fbab1..112045fa 100644 --- a/daemon/tests/test_nodes.py +++ b/daemon/tests/test_nodes.py @@ -60,6 +60,40 @@ class TestNodes: with pytest.raises(CoreError): session.get_node(node.id, CoreNode) + def test_node_add_iface(self, session: Session): + # given + node = session.add_node(CoreNode) + + # when + iface = node.create_iface() + + # then + assert iface.id in node.ifaces + + def test_node_get_iface(self, session: Session): + # given + node = session.add_node(CoreNode) + iface = node.create_iface() + assert iface.id in node.ifaces + + # when + iface2 = node.get_iface(iface.id) + + # then + assert iface == iface2 + + def test_node_delete_iface(self, session: Session): + # given + node = session.add_node(CoreNode) + iface = node.create_iface() + assert iface.id in node.ifaces + + # when + node.delete_iface(iface.id) + + # then + assert iface.id not in node.ifaces + @pytest.mark.parametrize( "mac,expected", [ @@ -70,12 +104,11 @@ class TestNodes: def test_node_set_mac(self, session: Session, mac: str, expected: str): # given node = session.add_node(CoreNode) - switch = session.add_node(SwitchNode) iface_data = InterfaceData() - iface = node.new_iface(switch, iface_data) + iface = node.create_iface(iface_data) # when - node.set_mac(iface.node_id, mac) + iface.set_mac(mac) # then assert str(iface.mac) == expected @@ -86,13 +119,12 @@ class TestNodes: def test_node_set_mac_exception(self, session: Session, mac: str): # given node = session.add_node(CoreNode) - switch = session.add_node(SwitchNode) iface_data = InterfaceData() - iface = node.new_iface(switch, iface_data) + iface = node.create_iface(iface_data) # when with pytest.raises(CoreError): - node.set_mac(iface.node_id, mac) + iface.set_mac(mac) @pytest.mark.parametrize( "ip,expected,is_ip6", @@ -106,12 +138,11 @@ class TestNodes: def test_node_add_ip(self, session: Session, ip: str, expected: str, is_ip6: bool): # given node = session.add_node(CoreNode) - switch = session.add_node(SwitchNode) iface_data = InterfaceData() - iface = node.new_iface(switch, iface_data) + iface = node.create_iface(iface_data) # when - node.add_ip(iface.node_id, ip) + iface.add_ip(ip) # then if is_ip6: @@ -122,14 +153,13 @@ class TestNodes: def test_node_add_ip_exception(self, session): # given node = session.add_node(CoreNode) - switch = session.add_node(SwitchNode) iface_data = InterfaceData() - iface = node.new_iface(switch, iface_data) + iface = node.create_iface(iface_data) ip = "256.168.0.1/24" # when with pytest.raises(CoreError): - node.add_ip(iface.node_id, ip) + iface.add_ip(ip) @pytest.mark.parametrize("net_type", NET_TYPES) def test_net(self, session, net_type): diff --git a/daemon/tests/test_xml.py b/daemon/tests/test_xml.py index 653e77f6..b3e57bef 100644 --- a/daemon/tests/test_xml.py +++ b/daemon/tests/test_xml.py @@ -10,7 +10,7 @@ from core.emulator.session import Session from core.errors import CoreError from core.location.mobility import BasicRangeModel from core.nodes.base import CoreNode -from core.nodes.network import PtpNet, SwitchNode, WlanNode +from core.nodes.network import SwitchNode, WlanNode from core.services.utility import SshService @@ -65,25 +65,18 @@ class TestXml: :param tmpdir: tmpdir to create data in :param ip_prefixes: generates ip addresses for nodes """ - # create ptp - ptp_node = session.add_node(PtpNet) - # create nodes node1 = session.add_node(CoreNode) node2 = session.add_node(CoreNode) - # link nodes to ptp net - for node in [node1, node2]: - iface_data = ip_prefixes.create_iface(node) - session.add_link(node.id, ptp_node.id, iface1_data=iface_data) + # link nodes + iface1_data = ip_prefixes.create_iface(node1) + iface2_data = ip_prefixes.create_iface(node2) + session.add_link(node1.id, node2.id, iface1_data, iface2_data) # instantiate session session.instantiate() - # get ids for nodes - node1_id = node1.id - node2_id = node2.id - # save xml xml_file = tmpdir.join("session.xml") file_path = Path(xml_file.strpath) @@ -98,16 +91,19 @@ class TestXml: # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(node1_id, CoreNode) + assert not session.get_node(node1.id, CoreNode) with pytest.raises(CoreError): - assert not session.get_node(node2_id, CoreNode) + assert not session.get_node(node2.id, CoreNode) + # verify no links are known + assert len(session.link_manager.links()) == 0 # load saved xml session.open_xml(file_path, start=True) # verify nodes have been recreated - assert session.get_node(node1_id, CoreNode) - assert session.get_node(node2_id, CoreNode) + assert session.get_node(node1.id, CoreNode) + assert session.get_node(node2.id, CoreNode) + assert len(session.link_manager.links()) == 1 def test_xml_ptp_services( self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes @@ -119,18 +115,15 @@ class TestXml: :param tmpdir: tmpdir to create data in :param ip_prefixes: generates ip addresses for nodes """ - # create ptp - ptp_node = session.add_node(PtpNet) - # create nodes options = NodeOptions(model="host") node1 = session.add_node(CoreNode, options=options) node2 = session.add_node(CoreNode) # link nodes to ptp net - for node in [node1, node2]: - iface_data = ip_prefixes.create_iface(node) - session.add_link(node.id, ptp_node.id, iface1_data=iface_data) + iface1_data = ip_prefixes.create_iface(node1) + iface2_data = ip_prefixes.create_iface(node2) + session.add_link(node1.id, node2.id, iface1_data, iface2_data) # set custom values for node service session.services.set_service(node1.id, SshService.name) @@ -143,10 +136,6 @@ class TestXml: # instantiate session session.instantiate() - # get ids for nodes - node1_id = node1.id - node2_id = node2.id - # save xml xml_file = tmpdir.join("session.xml") file_path = Path(xml_file.strpath) @@ -161,9 +150,9 @@ class TestXml: # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(node1_id, CoreNode) + assert not session.get_node(node1.id, CoreNode) with pytest.raises(CoreError): - assert not session.get_node(node2_id, CoreNode) + assert not session.get_node(node2.id, CoreNode) # load saved xml session.open_xml(file_path, start=True) @@ -172,8 +161,8 @@ class TestXml: service = session.services.get_service(node1.id, SshService.name) # verify nodes have been recreated - assert session.get_node(node1_id, CoreNode) - assert session.get_node(node2_id, CoreNode) + assert session.get_node(node1.id, CoreNode) + assert session.get_node(node2.id, CoreNode) assert service.config_data.get(service_file) == file_data def test_xml_mobility( @@ -187,8 +176,8 @@ class TestXml: :param ip_prefixes: generates ip addresses for nodes """ # create wlan - wlan_node = session.add_node(WlanNode) - session.mobility.set_model(wlan_node, BasicRangeModel, {"test": "1"}) + wlan = session.add_node(WlanNode) + session.mobility.set_model(wlan, BasicRangeModel, {"test": "1"}) # create nodes options = NodeOptions(model="mdr") @@ -199,16 +188,11 @@ class TestXml: # link nodes for node in [node1, node2]: iface_data = ip_prefixes.create_iface(node) - session.add_link(node.id, wlan_node.id, iface1_data=iface_data) + session.add_link(node.id, wlan.id, iface1_data=iface_data) # instantiate session session.instantiate() - # get ids for nodes - wlan_id = wlan_node.id - node1_id = node1.id - node2_id = node2.id - # save xml xml_file = tmpdir.join("session.xml") file_path = Path(xml_file.strpath) @@ -223,20 +207,20 @@ class TestXml: # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(node1_id, CoreNode) + assert not session.get_node(node1.id, CoreNode) with pytest.raises(CoreError): - assert not session.get_node(node2_id, CoreNode) + assert not session.get_node(node2.id, CoreNode) # load saved xml session.open_xml(file_path, start=True) # retrieve configuration we set originally - value = str(session.mobility.get_config("test", wlan_id, BasicRangeModel.name)) + value = str(session.mobility.get_config("test", wlan.id, BasicRangeModel.name)) # verify nodes and configuration were restored - assert session.get_node(node1_id, CoreNode) - assert session.get_node(node2_id, CoreNode) - assert session.get_node(wlan_id, WlanNode) + assert session.get_node(node1.id, CoreNode) + assert session.get_node(node2.id, CoreNode) + assert session.get_node(wlan.id, WlanNode) assert value == "1" def test_network_to_network(self, session: Session, tmpdir: TemporaryFile): @@ -256,10 +240,6 @@ class TestXml: # instantiate session session.instantiate() - # get ids for nodes - node1_id = switch1.id - node2_id = switch2.id - # save xml xml_file = tmpdir.join("session.xml") file_path = Path(xml_file.strpath) @@ -274,19 +254,19 @@ class TestXml: # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(node1_id, SwitchNode) + assert not session.get_node(switch1.id, SwitchNode) with pytest.raises(CoreError): - assert not session.get_node(node2_id, SwitchNode) + assert not session.get_node(switch2.id, SwitchNode) # load saved xml session.open_xml(file_path, start=True) # verify nodes have been recreated - switch1 = session.get_node(node1_id, SwitchNode) - switch2 = session.get_node(node2_id, SwitchNode) + switch1 = session.get_node(switch1.id, SwitchNode) + switch2 = session.get_node(switch2.id, SwitchNode) assert switch1 assert switch2 - assert len(switch1.links() + switch2.links()) == 1 + assert len(session.link_manager.links()) == 1 def test_link_options( self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes @@ -316,10 +296,6 @@ class TestXml: # instantiate session session.instantiate() - # get ids for nodes - node1_id = node1.id - node2_id = switch.id - # save xml xml_file = tmpdir.join("session.xml") file_path = Path(xml_file.strpath) @@ -334,27 +310,25 @@ class TestXml: # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(node1_id, CoreNode) + assert not session.get_node(node1.id, CoreNode) with pytest.raises(CoreError): - assert not session.get_node(node2_id, SwitchNode) + assert not session.get_node(switch.id, SwitchNode) # load saved xml session.open_xml(file_path, start=True) # verify nodes have been recreated - assert session.get_node(node1_id, CoreNode) - assert session.get_node(node2_id, SwitchNode) - links = [] - for node_id in session.nodes: - node = session.nodes[node_id] - links += node.links() - link = links[0] - assert options.loss == link.options.loss - assert options.bandwidth == link.options.bandwidth - assert options.jitter == link.options.jitter - assert options.delay == link.options.delay - assert options.dup == link.options.dup - assert options.buffer == link.options.buffer + assert session.get_node(node1.id, CoreNode) + assert session.get_node(switch.id, SwitchNode) + assert len(session.link_manager.links()) == 1 + link = list(session.link_manager.links())[0] + link_options = link.options() + assert options.loss == link_options.loss + assert options.bandwidth == link_options.bandwidth + assert options.jitter == link_options.jitter + assert options.delay == link_options.delay + assert options.dup == link_options.dup + assert options.buffer == link_options.buffer def test_link_options_ptp( self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes @@ -385,10 +359,6 @@ class TestXml: # instantiate session session.instantiate() - # get ids for nodes - node1_id = node1.id - node2_id = node2.id - # save xml xml_file = tmpdir.join("session.xml") file_path = Path(xml_file.strpath) @@ -403,27 +373,25 @@ class TestXml: # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(node1_id, CoreNode) + assert not session.get_node(node1.id, CoreNode) with pytest.raises(CoreError): - assert not session.get_node(node2_id, CoreNode) + assert not session.get_node(node2.id, CoreNode) # load saved xml session.open_xml(file_path, start=True) # verify nodes have been recreated - assert session.get_node(node1_id, CoreNode) - assert session.get_node(node2_id, CoreNode) - links = [] - for node_id in session.nodes: - node = session.nodes[node_id] - links += node.links() - link = links[0] - assert options.loss == link.options.loss - assert options.bandwidth == link.options.bandwidth - assert options.jitter == link.options.jitter - assert options.delay == link.options.delay - assert options.dup == link.options.dup - assert options.buffer == link.options.buffer + assert session.get_node(node1.id, CoreNode) + assert session.get_node(node2.id, CoreNode) + assert len(session.link_manager.links()) == 1 + link = list(session.link_manager.links())[0] + link_options = link.options() + assert options.loss == link_options.loss + assert options.bandwidth == link_options.bandwidth + assert options.jitter == link_options.jitter + assert options.delay == link_options.delay + assert options.dup == link_options.dup + assert options.buffer == link_options.buffer def test_link_options_bidirectional( self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes @@ -450,7 +418,9 @@ class TestXml: options1.dup = 5 options1.jitter = 5 options1.buffer = 50 - session.add_link(node1.id, node2.id, iface1_data, iface2_data, options1) + iface1, iface2 = session.add_link( + node1.id, node2.id, iface1_data, iface2_data, options1 + ) options2 = LinkOptions() options2.unidirectional = 1 options2.bandwidth = 10000 @@ -459,17 +429,11 @@ class TestXml: options2.dup = 10 options2.jitter = 10 options2.buffer = 100 - session.update_link( - node2.id, node1.id, iface2_data.id, iface1_data.id, options2 - ) + session.update_link(node2.id, node1.id, iface2.id, iface1.id, options2) # instantiate session session.instantiate() - # get ids for nodes - node1_id = node1.id - node2_id = node2.id - # save xml xml_file = tmpdir.join("session.xml") file_path = Path(xml_file.strpath) @@ -484,32 +448,26 @@ class TestXml: # verify nodes have been removed from session with pytest.raises(CoreError): - assert not session.get_node(node1_id, CoreNode) + assert not session.get_node(node1.id, CoreNode) with pytest.raises(CoreError): - assert not session.get_node(node2_id, CoreNode) + assert not session.get_node(node2.id, CoreNode) # load saved xml session.open_xml(file_path, start=True) # verify nodes have been recreated - assert session.get_node(node1_id, CoreNode) - assert session.get_node(node2_id, CoreNode) - links = [] - for node_id in session.nodes: - node = session.nodes[node_id] - links += node.links() - assert len(links) == 2 - link1 = links[0] - link2 = links[1] - assert options1.bandwidth == link1.options.bandwidth - assert options1.delay == link1.options.delay - assert options1.loss == link1.options.loss - assert options1.dup == link1.options.dup - assert options1.jitter == link1.options.jitter - assert options1.buffer == link1.options.buffer - assert options2.bandwidth == link2.options.bandwidth - assert options2.delay == link2.options.delay - assert options2.loss == link2.options.loss - assert options2.dup == link2.options.dup - assert options2.jitter == link2.options.jitter - assert options2.buffer == link2.options.buffer + assert session.get_node(node1.id, CoreNode) + assert session.get_node(node2.id, CoreNode) + assert len(session.link_manager.links()) == 1 + assert options1.bandwidth == iface1.options.bandwidth + assert options1.delay == iface1.options.delay + assert options1.loss == iface1.options.loss + assert options1.dup == iface1.options.dup + assert options1.jitter == iface1.options.jitter + assert options1.buffer == iface1.options.buffer + assert options2.bandwidth == iface2.options.bandwidth + assert options2.delay == iface2.options.delay + assert options2.loss == iface2.options.loss + assert options2.dup == iface2.options.dup + assert options2.jitter == iface2.options.jitter + assert options2.buffer == iface2.options.buffer diff --git a/docs/python.md b/docs/python.md index fe776662..ba4b4f33 100644 --- a/docs/python.md +++ b/docs/python.md @@ -341,19 +341,11 @@ EMANE Model Configuration: ```python from core import utils -# emane network specific config -session.emane.set_model_config(emane.id, EmaneIeee80211abgModel.name, { - "unicastrate": "3", -}) - -# node specific config -session.emane.set_model_config(node.id, EmaneIeee80211abgModel.name, { - "unicastrate": "3", -}) - -# node interface specific config +# standardized way to retrieve an appropriate config id +# iface id can be omitted, to allow a general configuration for a model, per node config_id = utils.iface_config_id(node.id, iface_id) -session.emane.set_model_config(config_id, EmaneIeee80211abgModel.name, { +# set emane configuration for the config id +session.emane.set_config(config_id, EmaneIeee80211abgModel.name, { "unicastrate": "3", }) ```