From d684b8eb5a6bd131d3b2aa7ae2c8b139a2c18912 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 11 Mar 2022 11:04:01 -0800 Subject: [PATCH 01/15] docs: updated emane python example to use updated set_config --- docs/python.md | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) 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", }) ``` From cd7f1a641e8e8ac83e1e8950fe8df5f65d677203 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 17 Mar 2022 15:28:38 -0700 Subject: [PATCH 02/15] daemon: initial pass to revamp how node linking and link management is done, provides a consistent way to link all wired nodes and allows them to be configured for tc for the same behavior across the board --- daemon/core/api/grpc/events.py | 4 +- daemon/core/api/grpc/grpcutils.py | 270 +++++++++++----- daemon/core/api/grpc/server.py | 70 ++--- daemon/core/api/tlv/corehandlers.py | 15 +- daemon/core/emane/emanemanager.py | 31 +- daemon/core/emane/nodes.py | 171 ++++++++-- daemon/core/emulator/links.py | 256 +++++++++++++++ daemon/core/emulator/session.py | 363 +++++++++------------ daemon/core/location/mobility.py | 4 +- daemon/core/nodes/base.py | 472 ++++++++-------------------- daemon/core/nodes/interface.py | 368 ++++------------------ daemon/core/nodes/network.py | 133 +------- daemon/core/xml/corexml.py | 138 ++++---- daemon/tests/test_core.py | 39 --- daemon/tests/test_grpc.py | 57 ++-- daemon/tests/test_gui.py | 60 +--- daemon/tests/test_links.py | 242 ++++++++------ daemon/tests/test_nodes.py | 54 +++- daemon/tests/test_xml.py | 202 +++++------- 19 files changed, 1393 insertions(+), 1556 deletions(-) create mode 100644 daemon/core/emulator/links.py 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..7a45ac4a 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 @@ -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,78 @@ 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)) + return link_protos + + +def convert_iface(iface: CoreInterface) -> core_pb2.Interface: + """ + Convert interface to protobuf. + + :param iface: interface to convert + :return: protobuf interface + """ + ip4 = iface.get_ip4() + ip4_mask = ip4.prefixlen if ip4 else None + ip4 = str(ip4) if ip4 else None + ip6 = iface.get_ip6() + ip6_mask = ip6.prefixlen if ip6 else None + ip6 = str(ip6) 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 +430,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 +590,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 +605,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 +645,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 +668,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 +684,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 +700,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 +718,23 @@ 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 = [] 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) + links = [] + 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 +768,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 060bc4b6..f290dc72 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -76,12 +76,7 @@ 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 @@ -564,12 +559,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( @@ -705,18 +700,16 @@ 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) + iface1_data = node1_iface.get_data() iface2_data = None if node2_iface: - iface2_data = grpcutils.iface_to_data(node2_iface) + iface2_data = node2_iface.get_data() source = request.source if request.source else None link_data = LinkData( message_type=MessageFlags.ADD, @@ -731,9 +724,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 ) @@ -1163,7 +1156,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 @@ -1188,32 +1182,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: @@ -1302,15 +1273,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: diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index 1937aea8..4764aa00 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -43,7 +43,6 @@ from core.emulator.enumerations import ( ConfigDataTypes, EventTypes, ExceptionLevels, - LinkTypes, MessageFlags, NodeTypes, RegisterTlvs, @@ -782,10 +781,6 @@ class CoreHandler(socketserver.BaseRequestHandler): ip6=message.get_tlv(LinkTlvs.IFACE2_IP6.value), ip6_mask=message.get_tlv(LinkTlvs.IFACE2_IP6_MASK.value), ) - link_type = LinkTypes.WIRED - link_type_value = message.get_tlv(LinkTlvs.TYPE.value) - if link_type_value is not None: - link_type = LinkTypes(link_type_value) options = LinkOptions() options.delay = message.get_tlv(LinkTlvs.DELAY.value) options.bandwidth = message.get_tlv(LinkTlvs.BANDWIDTH.value) @@ -811,20 +806,16 @@ class CoreHandler(socketserver.BaseRequestHandler): iface2_data.id = 0 if message.flags & MessageFlags.ADD.value: - self.session.add_link( - node1_id, node2_id, iface1_data, iface2_data, options, link_type - ) + self.session.add_link(node1_id, node2_id, iface1_data, iface2_data, options) elif message.flags & MessageFlags.DELETE.value: if isinstance(node1, Rj45Node): iface1_data.id = node1.iface_id if isinstance(node2, Rj45Node): iface2_data.id = node2.iface_id - self.session.delete_link( - node1_id, node2_id, iface1_data.id, iface2_data.id, link_type - ) + self.session.delete_link(node1_id, node2_id, iface1_data.id, iface2_data.id) else: self.session.update_link( - node1_id, node2_id, iface1_data.id, iface2_data.id, options, link_type + node1_id, node2_id, iface1_data.id, iface2_data.id, options ) return () 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..a7c46ccd 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, @@ -77,10 +185,10 @@ class EmaneNet(CoreNetworkBase): 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 +196,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) @@ -137,17 +248,39 @@ class EmaneNet(CoreNetworkBase): 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/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 219555d5..26a8efdf 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,40 +189,6 @@ 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" @@ -231,8 +199,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 +211,120 @@ 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): + 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 interface + if iface1_data and isinstance(node1, CoreNetworkBase): + iface1_data.id = None + iface1 = node1.create_iface(iface1_data, options) + if iface2_data and isinstance(node2, CoreNetworkBase): + iface2_data.id = None + 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) + 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 +333,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 +374,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 +383,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 +392,28 @@ 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) + node1_input = node1 if isinstance(node1, CoreNode) else None + iface1.set_config(node1_input) + if iface2 and not options.unidirectional: + iface2.options.update(options) + node2_input = node2 if isinstance(node2, CoreNode) else None + iface2.set_config(node2_input) def next_node_id(self) -> int: """ @@ -723,6 +650,7 @@ class Session: """ self.emane.shutdown() self.delete_nodes() + self.link_manager.reset() self.distributed.shutdown() self.hooks.clear() self.emane.reset() @@ -1480,7 +1408,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/location/mobility.py b/daemon/core/location/mobility.py index ebcb8fe4..7662b765 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -321,7 +321,9 @@ class BasicRangeModel(WirelessModel): loss=self.loss, jitter=self.jitter, ) - iface.config(options) + iface.options.update(options) + if isinstance(iface.node, CoreNode): + iface.set_config(iface.node) 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..845eafc1 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, @@ -139,6 +152,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 +268,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 +295,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: """ @@ -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,47 @@ 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 + 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(self) + # set iface up + self.node_net_client.device_up(iface.name) + class CoreNetworkBase(NodeBase): """ @@ -917,7 +797,6 @@ class CoreNetworkBase(NodeBase): """ linktype: LinkTypes = LinkTypes.WIRED - has_custom_iface: bool = False def __init__( self, @@ -941,57 +820,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. @@ -1001,6 +829,7 @@ class CoreNetworkBase(NodeBase): """ i = self.next_iface_id() self.ifaces[i] = iface + iface.net = self iface.net_id = i with self.linked_lock: self.linked[iface] = {} @@ -1013,56 +842,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/interface.py b/daemon/core/nodes/interface.py index 70eb679f..f1857950 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, node: "CoreNode" = None) -> 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 node: + 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 node: + 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/network.py b/daemon/core/nodes/network.py index 262d422c..322a2732 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,50 +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]: - """ - Build CORE API TLVs for a point-to-point link. One Link message - describes this network. - - :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): """ 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/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_grpc.py b/daemon/tests/test_grpc.py index e56322ad..76829e99 100644 --- a/daemon/tests/test_grpc.py +++ b/daemon/tests/test_grpc.py @@ -36,7 +36,7 @@ from core.api.tlv.enumerations import ConfigFlags 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 @@ -415,7 +415,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) @@ -425,7 +425,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 @@ -447,11 +447,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 @@ -460,8 +459,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 @@ -472,13 +470,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, @@ -492,7 +484,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 @@ -757,9 +749,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: @@ -958,3 +952,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_gui.py b/daemon/tests/test_gui.py index b14f1fb1..a59eef21 100644 --- a/daemon/tests/test_gui.py +++ b/daemon/tests/test_gui.py @@ -121,9 +121,7 @@ class TestGui: coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch_id, SwitchNode) - all_links = switch_node.links() - assert len(all_links) == 1 + assert len(coretlv.session.link_manager.links()) == 1 def test_link_add_net_to_node(self, coretlv: CoreHandler): node1_id = 1 @@ -145,9 +143,7 @@ class TestGui: coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch_id, SwitchNode) - all_links = switch_node.links() - assert len(all_links) == 1 + assert len(coretlv.session.link_manager.links()) == 1 def test_link_add_node_to_node(self, coretlv: CoreHandler): node1_id = 1 @@ -173,11 +169,7 @@ class TestGui: coretlv.handle_message(message) - all_links = [] - for node_id in coretlv.session.nodes: - node = coretlv.session.nodes[node_id] - all_links += node.links() - assert len(all_links) == 1 + assert len(coretlv.session.link_manager.links()) == 1 def test_link_update(self, coretlv: CoreHandler): node1_id = 1 @@ -197,11 +189,10 @@ class TestGui: ], ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch_id, SwitchNode) - all_links = switch_node.links() - assert len(all_links) == 1 - link = all_links[0] - assert link.options.bandwidth is None + assert len(coretlv.session.link_manager.links()) == 1 + link = list(coretlv.session.link_manager.links())[0] + + assert link.options().bandwidth is None bandwidth = 50000 message = coreapi.CoreLinkMessage.create( @@ -210,16 +201,13 @@ class TestGui: (LinkTlvs.N1_NUMBER, node1_id), (LinkTlvs.N2_NUMBER, switch_id), (LinkTlvs.IFACE1_NUMBER, 0), + (LinkTlvs.IFACE2_NUMBER, 0), (LinkTlvs.BANDWIDTH, bandwidth), ], ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch_id, SwitchNode) - all_links = switch_node.links() - assert len(all_links) == 1 - link = all_links[0] - assert link.options.bandwidth == bandwidth + assert link.options().bandwidth == bandwidth def test_link_delete_node_to_node(self, coretlv: CoreHandler): node1_id = 1 @@ -242,11 +230,7 @@ class TestGui: ], ) coretlv.handle_message(message) - all_links = [] - for node_id in coretlv.session.nodes: - node = coretlv.session.nodes[node_id] - all_links += node.links() - assert len(all_links) == 1 + assert len(coretlv.session.link_manager.links()) == 1 message = coreapi.CoreLinkMessage.create( MessageFlags.DELETE.value, @@ -259,11 +243,7 @@ class TestGui: ) coretlv.handle_message(message) - all_links = [] - for node_id in coretlv.session.nodes: - node = coretlv.session.nodes[node_id] - all_links += node.links() - assert len(all_links) == 0 + assert len(coretlv.session.link_manager.links()) == 0 def test_link_delete_node_to_net(self, coretlv: CoreHandler): node1_id = 1 @@ -283,9 +263,7 @@ class TestGui: ], ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch_id, SwitchNode) - all_links = switch_node.links() - assert len(all_links) == 1 + assert len(coretlv.session.link_manager.links()) == 1 message = coreapi.CoreLinkMessage.create( MessageFlags.DELETE.value, @@ -293,13 +271,12 @@ class TestGui: (LinkTlvs.N1_NUMBER, node1_id), (LinkTlvs.N2_NUMBER, switch_id), (LinkTlvs.IFACE1_NUMBER, 0), + (LinkTlvs.IFACE2_NUMBER, 0), ], ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch_id, SwitchNode) - all_links = switch_node.links() - assert len(all_links) == 0 + assert len(coretlv.session.link_manager.links()) == 0 def test_link_delete_net_to_node(self, coretlv: CoreHandler): node1_id = 1 @@ -319,23 +296,20 @@ class TestGui: ], ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch_id, SwitchNode) - all_links = switch_node.links() - assert len(all_links) == 1 + assert len(coretlv.session.link_manager.links()) == 1 message = coreapi.CoreLinkMessage.create( MessageFlags.DELETE.value, [ (LinkTlvs.N1_NUMBER, switch_id), (LinkTlvs.N2_NUMBER, node1_id), + (LinkTlvs.IFACE1_NUMBER, 0), (LinkTlvs.IFACE2_NUMBER, 0), ], ) coretlv.handle_message(message) - switch_node = coretlv.session.get_node(switch_id, SwitchNode) - all_links = switch_node.links() - assert len(all_links) == 0 + assert len(coretlv.session.link_manager.links()) == 0 def test_session_update(self, coretlv: CoreHandler): session_id = coretlv.session.id 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 From 7b16f9cb740261980c6ab4a7db0ed404320998ac Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 17 Mar 2022 16:31:03 -0700 Subject: [PATCH 03/15] daemon: moved node cmd to base, allowing interface config to not need a node parameter --- daemon/core/emulator/session.py | 6 ++---- daemon/core/location/mobility.py | 3 +-- daemon/core/nodes/base.py | 28 ++++++++++++++-------------- daemon/core/nodes/docker.py | 6 ++---- daemon/core/nodes/interface.py | 10 +++++----- daemon/core/nodes/lxd.py | 14 ++++++++------ 6 files changed, 32 insertions(+), 35 deletions(-) diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 26a8efdf..9525f7ca 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -408,12 +408,10 @@ class Session: ) if iface1: iface1.options.update(options) - node1_input = node1 if isinstance(node1, CoreNode) else None - iface1.set_config(node1_input) + iface1.set_config() if iface2 and not options.unidirectional: iface2.options.update(options) - node2_input = node2 if isinstance(node2, CoreNode) else None - iface2.set_config(node2_input) + iface2.set_config() def next_node_id(self) -> int: """ diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index 7662b765..31127346 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -322,8 +322,7 @@ class BasicRangeModel(WirelessModel): jitter=self.jitter, ) iface.options.update(options) - if isinstance(iface.node, CoreNode): - iface.set_config(iface.node) + 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 845eafc1..d7c2c02f 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -133,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. @@ -330,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: """ @@ -786,7 +786,7 @@ class CoreNode(CoreNodeBase): broadcast = "+" self.node_net_client.create_address(iface.name, str(ip), broadcast) # configure iface options - iface.set_config(self) + iface.set_config() # set iface up self.node_net_client.device_up(iface.name) 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 f1857950..bb90653f 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -293,21 +293,21 @@ class CoreInterface: """ return self.transport_type == TransportType.VIRTUAL - def set_config(self, node: "CoreNode" = None) -> None: + def set_config(self) -> None: # clear current settings if self.options.is_clear(): if self.has_netem: cmd = tc_clear_cmd(self.name) - if node: - node.cmd(cmd) + if self.node: + self.node.cmd(cmd) else: self.host_cmd(cmd) self.has_netem = False # set updated settings else: cmd = tc_cmd(self.name, self.options, self.mtu) - if node: - node.cmd(cmd) + if self.node: + self.node.cmd(cmd) else: self.host_cmd(cmd) self.has_netem = True 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 From 4f58d5d8eb2f1342cce2f8754a78781c3bb5406f Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 17 Mar 2022 16:43:31 -0700 Subject: [PATCH 04/15] daemon: removed unused EmaneNet function --- daemon/core/emane/nodes.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index a7c46ccd..18d6b865 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -181,9 +181,6 @@ class EmaneNet(CoreNetworkBase): return self.model.linkconfig(iface, options, iface2) - def config(self, conf: str) -> None: - self.conf = conf - def startup(self) -> None: self.up = True From 3c8f6a9512d97016fc547dce313db8cb4496859c Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 17 Mar 2022 16:44:12 -0700 Subject: [PATCH 05/15] daemon: fix convert interface grpc utility to properly get ip4/ip6 addresses --- daemon/core/api/grpc/grpcutils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 7a45ac4a..c0e49484 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -367,10 +367,10 @@ def convert_iface(iface: CoreInterface) -> core_pb2.Interface: """ ip4 = iface.get_ip4() ip4_mask = ip4.prefixlen if ip4 else None - ip4 = str(ip4) 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) 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, From 8c24e9cfa64b05ebdfb20150e4d24fd284b41d00 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 17 Mar 2022 21:21:03 -0700 Subject: [PATCH 06/15] daemon: updates to fix rj45 to account for iface link revamp --- daemon/core/nodes/physical.py | 88 +++++++++-------------------------- 1 file changed, 23 insertions(+), 65 deletions(-) diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 0a686da8..908053d5 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -7,7 +7,7 @@ import threading from pathlib import Path from typing import TYPE_CHECKING, List, Optional, Tuple -from core.emulator.data import InterfaceData +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 @@ -229,11 +229,9 @@ class Rj45Node(CoreNodeBase): """ super().__init__(session, _id, name, server) self.iface: CoreInterface = CoreInterface( - session, name, name, mtu, server, self + self.iface_id, name, name, session.use_ovs(), mtu, self, 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 +243,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 +264,7 @@ class Rj45Node(CoreNodeBase): except CoreCommandError: pass self.up = False - self.restorestate() + self.restore_state() def path_exists(self, path: str) -> bool: """ @@ -281,33 +279,24 @@ 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) + self.ifaces[self.iface.id] = self.iface for ip in iface_data.get_ips(): - self.add_ip(ip) - return self.iface + self.iface.add_ip(ip) + 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 +307,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 +324,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 +355,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. From b71272519d8df528b9e3b58777dc30b3cf5bedee Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 18 Mar 2022 12:31:04 -0700 Subject: [PATCH 07/15] daemon: revamp to align gre tunnels for distributed to align with changes, also moved gre tunnels for wired links to occur directly when linking, allowing runtime distributed functionality, also updates to phyical node to leverage a core node, but avoid using namespacing --- daemon/core/emulator/distributed.py | 24 ++- daemon/core/emulator/session.py | 3 + daemon/core/nodes/base.py | 11 +- daemon/core/nodes/physical.py | 264 ++++++++-------------------- daemon/tests/test_distributed.py | 12 +- 5 files changed, 113 insertions(+), 201 deletions(-) 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/session.py b/daemon/core/emulator/session.py index 9525f7ca..929d5fac 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -321,6 +321,9 @@ class Session: # 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( diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index d7c2c02f..6fbfb612 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -776,8 +776,9 @@ class CoreNode(CoreNodeBase): 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 - self.node_net_client.device_mac(iface.name, str(iface.mac)) - logger.debug("interface mac: %s - %s", iface.name, iface.mac) + 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 @@ -827,10 +828,10 @@ class CoreNetworkBase(NodeBase): :param iface: network interface to attach :return: nothing """ - i = self.next_iface_id() - self.ifaces[i] = iface + iface_id = self.next_iface_id() + self.ifaces[iface_id] = iface iface.net = self - iface.net_id = i + iface.net_id = iface_id with self.linked_lock: self.linked[iface] = {} diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 908053d5..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 +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,13 +44,12 @@ 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( - self.iface_id, name, name, session.use_ovs(), mtu, self, server + self.iface_id, name, name, session.use_ovs(), node=self, server=server ) self.iface.transport_type = TransportType.RAW self.old_up: bool = False @@ -287,9 +107,13 @@ class Rj45Node(CoreNodeBase): raise CoreError( f"rj45({self.name}) nodes support at most 1 network interface" ) - self.ifaces[self.iface.id] = self.iface + 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.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)) @@ -395,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/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 From dc9b6adc985f61fb36df88e872508d3dd9eee1f8 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Fri, 18 Mar 2022 15:53:13 -0700 Subject: [PATCH 08/15] daemon/gui: adjustments to account for network node wired links having proper interface ids, which allow for unique configuration with the new linking semantics --- daemon/core/api/grpc/grpcutils.py | 37 +++++++++++++------------ daemon/core/api/grpc/server.py | 12 ++++++-- daemon/core/emulator/session.py | 6 +--- daemon/core/gui/dialogs/nodeconfig.py | 2 +- daemon/core/gui/graph/node.py | 5 +++- daemon/core/gui/interface.py | 40 +++++++++++++++------------ daemon/core/gui/nodeutils.py | 4 +++ 7 files changed, 61 insertions(+), 45 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index c0e49484..5cec8c2e 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -28,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__) @@ -365,22 +365,25 @@ def convert_iface(iface: CoreInterface) -> core_pb2.Interface: :param iface: interface to convert :return: protobuf interface """ - 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, - ) + 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]: diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index f290dc72..4be0737d 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -81,7 +81,7 @@ 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__) @@ -706,10 +706,16 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer): ) iface1_data = None if node1_iface: - iface1_data = node1_iface.get_data() + 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 = node2_iface.get_data() + 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, diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 929d5fac..4ca2485d 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -307,12 +307,8 @@ class Session: :param options: options to configure interfaces with :return: interfaces created for both nodes """ - # create interface - if iface1_data and isinstance(node1, CoreNetworkBase): - iface1_data.id = None + # create interfaces iface1 = node1.create_iface(iface1_data, options) - if iface2_data and isinstance(node2, CoreNetworkBase): - iface2_data.id = None iface2 = node2.create_iface(iface2_data, options) # join and attach to ptp bridge ptp = self.create_node(PtpNet, self.state.should_start()) 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: From e5e14ad67b2f5a74b0b5cbe310b186c864e892a3 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 21 Mar 2022 09:55:57 -0700 Subject: [PATCH 09/15] grpc: added wlan/emane links to grpc calls to allow knowing initial wireless link state, same as before changes --- daemon/core/api/grpc/grpcutils.py | 8 +++++++- daemon/core/emane/nodes.py | 4 ++-- daemon/core/nodes/network.py | 6 +++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 5cec8c2e..e2cba7e9 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -355,6 +355,9 @@ def get_links(session: Session, node: NodeBase) -> List[core_pb2.Link]: 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 @@ -729,13 +732,16 @@ def convert_session(session: Session) -> wrappers.Session: """ 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) - 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) diff --git a/daemon/core/emane/nodes.py b/daemon/core/emane/nodes.py index 18d6b865..a32814e1 100644 --- a/daemon/core/emane/nodes.py +++ b/daemon/core/emane/nodes.py @@ -219,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() @@ -240,7 +240,7 @@ 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 diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 322a2732..eb1e4657 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -781,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): From 7ed007496ca5f3507fde006b489ea63c2e3b51e6 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 21 Mar 2022 15:42:14 -0700 Subject: [PATCH 10/15] daemon: add linked method to session and grpc support leveraging new linking semantics, update add_link validation check --- daemon/core/api/grpc/client.py | 32 +++++++++++++++++- daemon/core/api/grpc/server.py | 15 ++++++++- daemon/core/emulator/session.py | 48 ++++++++++++++++++++++++++- daemon/proto/core/api/grpc/core.proto | 14 ++++++++ 4 files changed, 106 insertions(+), 3 deletions(-) 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/server.py b/daemon/core/api/grpc/server.py index 4be0737d..ca0dd7f5 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, @@ -1315,3 +1315,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/emulator/session.py b/daemon/core/emulator/session.py index 4ca2485d..0d7930a9 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -192,6 +192,45 @@ class Session: 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, @@ -223,7 +262,14 @@ class Session: 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): + 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 diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index f0cb242d..e3b14341 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) { @@ -684,3 +686,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 { +} From 5398cdd2d5c6e5f2fb2ee2b1d54b83fdbf8661bd Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 21 Mar 2022 15:57:13 -0700 Subject: [PATCH 11/15] core-cli: fixed xml load call to use the right parameter type --- daemon/scripts/core-cli | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/daemon/scripts/core-cli b/daemon/scripts/core-cli index fbbe6ede..365e6411 100755 --- a/daemon/scripts/core-cli +++ b/daemon/scripts/core-cli @@ -94,11 +94,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: From b94139510024c94c32ac3e2c0eeee596e7f8f851 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 21 Mar 2022 20:59:18 -0700 Subject: [PATCH 12/15] core-cli: updates to add back json output --- daemon/core/api/grpc/wrappers.py | 9 ++ daemon/scripts/core-cli | 198 ++++++++++++++++++++----------- 2 files changed, 139 insertions(+), 68 deletions(-) diff --git a/daemon/core/api/grpc/wrappers.py b/daemon/core/api/grpc/wrappers.py index ffeb6793..94a1598c 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/scripts/core-cli b/daemon/scripts/core-cli index 365e6411..781f64f0 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): @@ -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("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: + 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") + 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 + 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" From 2df8aa4379057b7a62b179c3d7056af97527ac84 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 21 Mar 2022 21:04:43 -0700 Subject: [PATCH 13/15] updated version to 8.2.0 for next release --- configure.ac | 2 +- daemon/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/configure.ac b/configure.ac index a3d61abc..78980b56 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/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" From 6d8ae4af2e76f465ae0e3ed5df085d8d49126c86 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 21 Mar 2022 21:35:05 -0700 Subject: [PATCH 14/15] core-cli: add geo position to node output for query session and node --- daemon/scripts/core-cli | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/daemon/scripts/core-cli b/daemon/scripts/core-cli index 781f64f0..6f2c1f5b 100755 --- a/daemon/scripts/core-cli +++ b/daemon/scripts/core-cli @@ -212,9 +212,11 @@ def query_session(core: CoreGrpcClient, args: Namespace) -> None: print_json(session) else: print("Nodes") - print("Node ID | Node Name | Node Type") + print("ID | Name | Type | XY | Geo") for node in session.nodes.values(): - print(f"{node.id:<7} | {node.name:<9} | {node.type.name}") + 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 @@ -243,12 +245,10 @@ def query_node(core: CoreGrpcClient, args: Namespace) -> None: ifaces = [protobuf_to_json(x.to_proto()) for x in ifaces] print_json(dict(node=node, ifaces=ifaces)) else: - print("ID | Name | Type | XY") + print("ID | Name | Type | XY | Geo") 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}") + 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="") From d7b2c3cac3453b234aef0e6ae69612a4faab30d5 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Mon, 21 Mar 2022 21:43:24 -0700 Subject: [PATCH 15/15] updated changelog for 8.2.0 release --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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