merged latest updates from develop

This commit is contained in:
Blake Harnden 2022-03-22 10:03:03 -07:00
commit d83bfed608
34 changed files with 1840 additions and 1901 deletions

View file

@ -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 ## 2022-02-18 CORE 8.1.0
* Installation * Installation

View file

@ -2,7 +2,7 @@
# Process this file with autoconf to produce a configure script. # Process this file with autoconf to produce a configure script.
# this defines the CORE version number, must be static for AC_INIT # 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 # autoconf and automake initialization
AC_CONFIG_SRCDIR([netns/version.h.in]) AC_CONFIG_SRCDIR([netns/version.h.in])

View file

@ -16,7 +16,7 @@ from core.api.grpc.configservices_pb2 import (
GetConfigServiceDefaultsRequest, GetConfigServiceDefaultsRequest,
GetNodeConfigServiceRequest, 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 ( from core.api.grpc.emane_pb2 import (
EmaneLinkRequest, EmaneLinkRequest,
GetEmaneEventChannelRequest, GetEmaneEventChannelRequest,
@ -1049,6 +1049,36 @@ class CoreGrpcClient:
""" """
self.stub.EmanePathlosses(streamer.iter()) 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: def connect(self) -> None:
""" """
Open connection to server, must be closed manually. Open connection to server, must be closed manually.

View file

@ -3,7 +3,7 @@ from queue import Empty, Queue
from typing import Iterable, Optional from typing import Iterable, Optional
from core.api.grpc import core_pb2 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 ( from core.emulator.data import (
ConfigData, ConfigData,
EventData, EventData,
@ -51,7 +51,7 @@ def handle_link_event(link_data: LinkData) -> core_pb2.Event:
:param link_data: link data :param link_data: link data
:return: link event that has message type and link information :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 message_type = link_data.message_type.value
link_event = core_pb2.LinkEvent(message_type=message_type, link=link) link_event = core_pb2.LinkEvent(message_type=message_type, link=link)
return core_pb2.Event(link_event=link_event, source=link_data.source) return core_pb2.Event(link_event=link_event, source=link_data.source)

View file

@ -1,7 +1,7 @@
import logging import logging
import time import time
from pathlib import Path 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 import grpc
from grpc import ServicerContext from grpc import ServicerContext
@ -20,6 +20,7 @@ from core.config import ConfigurableOptions
from core.emane.nodes import EmaneNet from core.emane.nodes import EmaneNet
from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions from core.emulator.data import InterfaceData, LinkData, LinkOptions, NodeOptions
from core.emulator.enumerations import LinkTypes, NodeTypes from core.emulator.enumerations import LinkTypes, NodeTypes
from core.emulator.links import CoreLink
from core.emulator.session import Session from core.emulator.session import Session
from core.errors import CoreError from core.errors import CoreError
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
@ -27,7 +28,7 @@ from core.nodes.base import CoreNode, CoreNodeBase, NodeBase
from core.nodes.docker import DockerNode from core.nodes.docker import DockerNode
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
from core.nodes.lxd import LxcNode 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 from core.services.coreservices import CoreService
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -110,7 +111,7 @@ def link_iface(iface_proto: core_pb2.Interface) -> InterfaceData:
def add_link_data( def add_link_data(
link_proto: core_pb2.Link link_proto: core_pb2.Link
) -> Tuple[InterfaceData, InterfaceData, LinkOptions, LinkTypes]: ) -> Tuple[InterfaceData, InterfaceData, LinkOptions]:
""" """
Convert link proto to link interfaces and options data. Convert link proto to link interfaces and options data.
@ -119,7 +120,6 @@ def add_link_data(
""" """
iface1_data = link_iface(link_proto.iface1) iface1_data = link_iface(link_proto.iface1)
iface2_data = link_iface(link_proto.iface2) iface2_data = link_iface(link_proto.iface2)
link_type = LinkTypes(link_proto.type)
options = LinkOptions() options = LinkOptions()
options_proto = link_proto.options options_proto = link_proto.options
if options_proto: if options_proto:
@ -134,7 +134,7 @@ def add_link_data(
options.buffer = options_proto.buffer options.buffer = options_proto.buffer
options.unidirectional = options_proto.unidirectional options.unidirectional = options_proto.unidirectional
options.key = options_proto.key options.key = options_proto.key
return iface1_data, iface2_data, options, link_type return iface1_data, iface2_data, options
def create_nodes( def create_nodes(
@ -174,8 +174,8 @@ def create_links(
for link_proto in link_protos: for link_proto in link_protos:
node1_id = link_proto.node1_id node1_id = link_proto.node1_id
node2_id = link_proto.node2_id node2_id = link_proto.node2_id
iface1, iface2, options, link_type = add_link_data(link_proto) iface1, iface2, options = add_link_data(link_proto)
args = (node1_id, node2_id, iface1, iface2, options, link_type) args = (node1_id, node2_id, iface1, iface2, options)
funcs.append((session.add_link, args, {})) funcs.append((session.add_link, args, {}))
start = time.monotonic() start = time.monotonic()
results, exceptions = utils.threadpool(funcs) results, exceptions = utils.threadpool(funcs)
@ -198,8 +198,8 @@ def edit_links(
for link_proto in link_protos: for link_proto in link_protos:
node1_id = link_proto.node1_id node1_id = link_proto.node1_id
node2_id = link_proto.node2_id node2_id = link_proto.node2_id
iface1, iface2, options, link_type = add_link_data(link_proto) iface1, iface2, options = add_link_data(link_proto)
args = (node1_id, node2_id, iface1.id, iface2.id, options, link_type) args = (node1_id, node2_id, iface1.id, iface2.id, options)
funcs.append((session.update_link, args, {})) funcs.append((session.update_link, args, {}))
start = time.monotonic() start = time.monotonic()
results, exceptions = utils.threadpool(funcs) results, exceptions = utils.threadpool(funcs)
@ -344,61 +344,84 @@ def get_node_proto(
) )
def get_links(node: NodeBase): def get_links(session: Session, node: NodeBase) -> List[core_pb2.Link]:
""" """
Retrieve a list of links for grpc to use. Retrieve a list of links for grpc to use.
:param session: session to get links for node
:param node: node to get links from :param node: node to get links from
:return: protobuf links :return: protobuf links
""" """
link_protos = []
for core_link in session.link_manager.node_links(node):
link_protos.extend(convert_core_link(core_link))
if isinstance(node, (WlanNode, EmaneNet)):
for link_data in node.links():
link_protos.append(convert_link_data(link_data))
return link_protos
def convert_iface(iface: CoreInterface) -> core_pb2.Interface:
"""
Convert interface to protobuf.
:param iface: interface to convert
:return: protobuf interface
"""
if isinstance(iface.node, CoreNetwork):
return core_pb2.Interface(id=iface.id)
else:
ip4 = iface.get_ip4()
ip4_mask = ip4.prefixlen if ip4 else None
ip4 = str(ip4.ip) if ip4 else None
ip6 = iface.get_ip6()
ip6_mask = ip6.prefixlen if ip6 else None
ip6 = str(ip6.ip) if ip6 else None
mac = str(iface.mac) if iface.mac else None
return core_pb2.Interface(
id=iface.id,
name=iface.name,
mac=mac,
ip4=ip4,
ip4_mask=ip4_mask,
ip6=ip6,
ip6_mask=ip6_mask,
)
def convert_core_link(core_link: CoreLink) -> List[core_pb2.Link]:
"""
Convert core link to protobuf data.
:param core_link: core link to convert
:return: protobuf link data
"""
links = [] links = []
for link in node.links(): node1, iface1 = core_link.node1, core_link.iface1
link_proto = convert_link(link) node2, iface2 = core_link.node2, core_link.iface2
links.append(link_proto) 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 return links
def convert_iface(iface_data: InterfaceData) -> core_pb2.Interface: def convert_link_data(link_data: LinkData) -> core_pb2.Link:
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:
""" """
Convert link_data into core protobuf link. Convert link_data into core protobuf link.
:param link_data: link to convert :param link_data: link to convert
:return: core protobuf Link :return: core protobuf Link
""" """
iface1 = None iface1 = None
if link_data.iface1 is not None: if link_data.iface1 is not None:
iface1 = convert_iface(link_data.iface1) iface1 = convert_iface_data(link_data.iface1)
iface2 = None iface2 = None
if link_data.iface2 is not 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) options = convert_link_options(link_data.options)
return core_pb2.Link( return core_pb2.Link(
type=link_data.type.value, type=link_data.type.value,
@ -413,6 +436,89 @@ def convert_link(link_data: LinkData) -> core_pb2.Link:
) )
def convert_iface_data(iface_data: InterfaceData) -> core_pb2.Interface:
"""
Convert interface data to protobuf.
:param iface_data: interface data to convert
:return: interface protobuf
"""
return core_pb2.Interface(
id=iface_data.id,
name=iface_data.name,
mac=iface_data.mac,
ip4=iface_data.ip4,
ip4_mask=iface_data.ip4_mask,
ip6=iface_data.ip6,
ip6_mask=iface_data.ip6_mask,
)
def convert_link_options(options: LinkOptions) -> core_pb2.LinkOptions:
"""
Convert link options to protobuf.
:param options: link options to convert
:return: link options protobuf
"""
return core_pb2.LinkOptions(
jitter=options.jitter,
key=options.key,
mburst=options.mburst,
mer=options.mer,
loss=options.loss,
bandwidth=options.bandwidth,
burst=options.burst,
delay=options.delay,
dup=options.dup,
buffer=options.buffer,
unidirectional=options.unidirectional,
)
def convert_link(
node1: NodeBase,
iface1: Optional[CoreInterface],
node2: NodeBase,
iface2: Optional[CoreInterface],
options: LinkOptions,
unidirectional: bool,
) -> core_pb2.Link:
"""
Convert link objects to link protobuf.
:param node1: first node in link
:param iface1: node1 interface
:param node2: second node in link
:param iface2: node2 interface
:param options: link options
:param unidirectional: if this link is considered unidirectional
:return: protobuf link
"""
if iface1 is not None:
iface1 = convert_iface(iface1)
if iface2 is not None:
iface2 = convert_iface(iface2)
is_node1_wireless = isinstance(node1, (WlanNode, EmaneNet))
is_node2_wireless = isinstance(node2, (WlanNode, EmaneNet))
if not (is_node1_wireless or is_node2_wireless):
options = convert_link_options(options)
options.unidirectional = unidirectional
else:
options = None
return core_pb2.Link(
type=LinkTypes.WIRED.value,
node1_id=node1.id,
node2_id=node2.id,
iface1=iface1,
iface2=iface2,
options=options,
network_id=None,
label=None,
color=None,
)
def get_net_stats() -> Dict[str, Dict]: def get_net_stats() -> Dict[str, Dict]:
""" """
Retrieve status about the current interfaces in the system Retrieve status about the current interfaces in the system
@ -490,39 +596,13 @@ def get_service_configuration(service: CoreService) -> NodeServiceData:
) )
def iface_to_data(iface: CoreInterface) -> InterfaceData: def iface_to_proto(iface: CoreInterface) -> core_pb2.Interface:
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:
""" """
Convenience for converting a core interface to the protobuf representation. 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 :param iface: interface to convert
:return: interface proto :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_net = iface.get_ip4()
ip4 = str(ip4_net.ip) if ip4_net else None ip4 = str(ip4_net.ip) if ip4_net else None
ip4_mask = ip4_net.prefixlen if ip4_net else None ip4_mask = ip4_net.prefixlen if ip4_net else None
@ -531,10 +611,7 @@ def iface_to_proto(node_id: int, iface: CoreInterface) -> core_pb2.Interface:
ip6_mask = ip6_net.prefixlen if ip6_net else None ip6_mask = ip6_net.prefixlen if ip6_net else None
mac = str(iface.mac) if iface.mac else None mac = str(iface.mac) if iface.mac else None
return core_pb2.Interface( return core_pb2.Interface(
id=_id, id=iface.id,
net_id=net_id,
net2_id=net2_id,
node_id=node_id,
name=iface.name, name=iface.name,
mac=mac, mac=mac,
mtu=iface.mtu, mtu=iface.mtu,
@ -574,6 +651,12 @@ def get_nem_id(
def get_emane_model_configs_dict(session: Session) -> Dict[int, List[NodeEmaneConfig]]: 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 = {} configs = {}
for _id, model_configs in session.emane.node_configs.items(): for _id, model_configs in session.emane.node_configs.items():
for model_name in model_configs: for model_name in model_configs:
@ -591,6 +674,12 @@ def get_emane_model_configs_dict(session: Session) -> Dict[int, List[NodeEmaneCo
def get_hooks(session: Session) -> List[core_pb2.Hook]: 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 = [] hooks = []
for state in session.hooks: for state in session.hooks:
state_hooks = session.hooks[state] state_hooks = session.hooks[state]
@ -601,6 +690,12 @@ def get_hooks(session: Session) -> List[core_pb2.Hook]:
def get_default_services(session: Session) -> List[ServiceDefaults]: 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 = [] default_services = []
for name, services in session.services.default_services.items(): for name, services in session.services.default_services.items():
default_service = ServiceDefaults(node_type=name, services=services) default_service = ServiceDefaults(node_type=name, services=services)
@ -611,6 +706,14 @@ def get_default_services(session: Session) -> List[ServiceDefaults]:
def get_mobility_node( def get_mobility_node(
session: Session, node_id: int, context: ServicerContext session: Session, node_id: int, context: ServicerContext
) -> Union[WlanNode, EmaneNet]: ) -> 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: try:
return session.get_node(node_id, WlanNode) return session.get_node(node_id, WlanNode)
except CoreError: except CoreError:
@ -621,17 +724,26 @@ def get_mobility_node(
def convert_session(session: Session) -> wrappers.Session: 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) emane_configs = get_emane_model_configs_dict(session)
nodes = []
links = []
for _id in session.nodes: for _id in session.nodes:
node = session.nodes[_id] node = session.nodes[_id]
if not isinstance(node, (PtpNet, CtrlNet)): if not isinstance(node, (PtpNet, CtrlNet)):
node_emane_configs = emane_configs.get(node.id, []) node_emane_configs = emane_configs.get(node.id, [])
node_proto = get_node_proto(session, node, node_emane_configs) node_proto = get_node_proto(session, node, node_emane_configs)
nodes.append(node_proto) nodes.append(node_proto)
node_links = get_links(node) if isinstance(node, (WlanNode, EmaneNet)):
links.extend(node_links) 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) default_services = get_default_services(session)
x, y, z = session.location.refxyz x, y, z = session.location.refxyz
lat, lon, alt = session.location.refgeo lat, lon, alt = session.location.refgeo
@ -665,6 +777,15 @@ def convert_session(session: Session) -> wrappers.Session:
def configure_node( def configure_node(
session: Session, node: core_pb2.Node, core_node: NodeBase, context: ServicerContext session: Session, node: core_pb2.Node, core_node: NodeBase, context: ServicerContext
) -> None: ) -> 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: for emane_config in node.emane_configs:
_id = utils.iface_config_id(node.id, emane_config.iface_id) _id = utils.iface_config_id(node.id, emane_config.iface_id)
config = {k: v.value for k, v in emane_config.config.items()} config = {k: v.value for k, v in emane_config.config.items()}

View file

@ -26,7 +26,7 @@ from core.api.grpc.configservices_pb2 import (
GetNodeConfigServiceRequest, GetNodeConfigServiceRequest,
GetNodeConfigServiceResponse, 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 ( from core.api.grpc.emane_pb2 import (
EmaneLinkRequest, EmaneLinkRequest,
EmaneLinkResponse, EmaneLinkResponse,
@ -76,17 +76,12 @@ from core.configservice.base import ConfigServiceBootError
from core.emane.modelmanager import EmaneModelManager from core.emane.modelmanager import EmaneModelManager
from core.emulator.coreemu import CoreEmu from core.emulator.coreemu import CoreEmu
from core.emulator.data import InterfaceData, LinkData, LinkOptions from core.emulator.data import InterfaceData, LinkData, LinkOptions
from core.emulator.enumerations import ( from core.emulator.enumerations import EventTypes, ExceptionLevels, MessageFlags
EventTypes,
ExceptionLevels,
LinkTypes,
MessageFlags,
)
from core.emulator.session import NT, Session from core.emulator.session import NT, Session
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
from core.nodes.base import CoreNode, NodeBase 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 from core.services.coreservices import ServiceManager
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -565,12 +560,12 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
ifaces = [] ifaces = []
for iface_id in node.ifaces: for iface_id in node.ifaces:
iface = node.ifaces[iface_id] 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) ifaces.append(iface_proto)
emane_configs = grpcutils.get_emane_model_configs_dict(session) emane_configs = grpcutils.get_emane_model_configs_dict(session)
node_emane_configs = emane_configs.get(node.id, []) node_emane_configs = emane_configs.get(node.id, [])
node_proto = grpcutils.get_node_proto(session, node, node_emane_configs) 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) return core_pb2.GetNodeResponse(node=node_proto, ifaces=ifaces, links=links)
def MoveNode( def MoveNode(
@ -706,18 +701,22 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
node2_id = request.link.node2_id node2_id = request.link.node2_id
self.get_node(session, node1_id, context, NodeBase) self.get_node(session, node1_id, context, NodeBase)
self.get_node(session, node2_id, context, NodeBase) self.get_node(session, node2_id, context, NodeBase)
iface1_data, iface2_data, options, link_type = grpcutils.add_link_data( iface1_data, iface2_data, options = grpcutils.add_link_data(request.link)
request.link
)
node1_iface, node2_iface = session.add_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 iface1_data = None
if node1_iface: if node1_iface:
iface1_data = grpcutils.iface_to_data(node1_iface) if isinstance(node1_iface.node, CoreNetwork):
iface1_data = InterfaceData(id=node1_iface.id)
else:
iface1_data = node1_iface.get_data()
iface2_data = None iface2_data = None
if node2_iface: if node2_iface:
iface2_data = grpcutils.iface_to_data(node2_iface) if isinstance(node2_iface.node, CoreNetwork):
iface2_data = InterfaceData(id=node2_iface.id)
else:
iface2_data = node2_iface.get_data()
source = request.source if request.source else None source = request.source if request.source else None
link_data = LinkData( link_data = LinkData(
message_type=MessageFlags.ADD, message_type=MessageFlags.ADD,
@ -732,9 +731,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
iface1_proto = None iface1_proto = None
iface2_proto = None iface2_proto = None
if node1_iface: if node1_iface:
iface1_proto = grpcutils.iface_to_proto(node1_id, node1_iface) iface1_proto = grpcutils.iface_to_proto(node1_iface)
if node2_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( return core_pb2.AddLinkResponse(
result=True, iface1=iface1_proto, iface2=iface2_proto result=True, iface1=iface1_proto, iface2=iface2_proto
) )
@ -1164,7 +1163,8 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
self, request: core_pb2.GetInterfacesRequest, context: ServicerContext self, request: core_pb2.GetInterfacesRequest, context: ServicerContext
) -> core_pb2.GetInterfacesResponse: ) -> 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 request: get-interfaces request
:param context: context object :param context: context object
@ -1189,32 +1189,9 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
""" """
logger.debug("emane link: %s", request) logger.debug("emane link: %s", request)
session = self.get_session(request.session_id, context) session = self.get_session(request.session_id, context)
nem1 = request.nem1 flag = MessageFlags.ADD if request.linked else MessageFlags.DELETE
iface1 = session.emane.get_iface(nem1) link = session.emane.get_nem_link(request.nem1, request.nem2, flag)
if not iface1: if link:
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,
)
session.broadcast_link(link) session.broadcast_link(link)
return EmaneLinkResponse(result=True) return EmaneLinkResponse(result=True)
else: else:
@ -1303,15 +1280,18 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
if not isinstance(wlan.model, BasicRangeModel): if not isinstance(wlan.model, BasicRangeModel):
context.abort( context.abort(
grpc.StatusCode.NOT_FOUND, 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) node1 = self.get_node(session, request.node1_id, context, CoreNode)
node2 = self.get_node(session, request.node2_id, context, CoreNode) node2 = self.get_node(session, request.node2_id, context, CoreNode)
node1_iface, node2_iface = None, None node1_iface, node2_iface = None, None
for net, iface1, iface2 in node1.commonnets(node2): for iface in node1.get_ifaces(control=False):
if net == wlan: if iface.net == wlan:
node1_iface = iface1 node1_iface = iface
node2_iface = iface2 break
for iface in node2.get_ifaces(control=False):
if iface.net == wlan:
node2_iface = iface
break break
result = False result = False
if node1_iface and node2_iface: if node1_iface and node2_iface:
@ -1336,3 +1316,16 @@ class CoreGrpcServer(core_pb2_grpc.CoreApiServicer):
nem2 = grpcutils.get_nem_id(session, node2, request.iface2_id, context) nem2 = grpcutils.get_nem_id(session, node2, request.iface2_id, context)
session.emane.publish_pathloss(nem1, nem2, request.rx1, request.rx2) session.emane.publish_pathloss(nem1, nem2, request.rx1, request.rx2)
return EmanePathlossesResponse() 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()

View file

@ -637,6 +637,15 @@ class SessionSummary:
dir=proto.dir, 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 @dataclass
class Hook: class Hook:

View file

@ -12,12 +12,12 @@ from core import utils
from core.emane.emanemodel import EmaneModel from core.emane.emanemodel import EmaneModel
from core.emane.linkmonitor import EmaneLinkMonitor from core.emane.linkmonitor import EmaneLinkMonitor
from core.emane.modelmanager import EmaneModelManager 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.data import LinkData
from core.emulator.enumerations import LinkTypes, MessageFlags, RegisterTlvs from core.emulator.enumerations import LinkTypes, MessageFlags, RegisterTlvs
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.nodes.base import CoreNetworkBase, CoreNode, NodeBase from core.nodes.base import CoreNode, NodeBase
from core.nodes.interface import CoreInterface, TunTap from core.nodes.interface import CoreInterface
from core.xml import emanexml from core.xml import emanexml
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -224,11 +224,9 @@ class EmaneManager:
:return: net, node, or interface model configuration :return: net, node, or interface model configuration
""" """
model_name = emane_net.model.name model_name = emane_net.model.name
config = None
# try to retrieve interface specific configuration # try to retrieve interface specific configuration
if iface.node_id is not None: key = utils.iface_config_id(iface.node.id, iface.id)
key = utils.iface_config_id(iface.node.id, iface.node_id) config = self.get_config(key, model_name, default=False)
config = self.get_config(key, model_name, default=False)
# attempt to retrieve node specific config, when iface config is not present # attempt to retrieve node specific config, when iface config is not present
if not config: if not config:
config = self.get_config(iface.node.id, model_name, default=False) config = self.get_config(iface.node.id, model_name, default=False)
@ -272,7 +270,8 @@ class EmaneManager:
nodes = set() nodes = set()
for emane_net in self._emane_nets.values(): for emane_net in self._emane_nets.values():
for iface in emane_net.get_ifaces(): for iface in emane_net.get_ifaces():
nodes.add(iface.node) if isinstance(iface.node, CoreNode):
nodes.add(iface.node)
return nodes return nodes
def setup(self) -> EmaneState: def setup(self) -> EmaneState:
@ -323,7 +322,7 @@ class EmaneManager:
for emane_net, iface in self.get_ifaces(): for emane_net, iface in self.get_ifaces():
self.start_iface(emane_net, iface) 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_id = self.next_nem_id(iface)
nem_port = self.get_nem_port(iface) nem_port = self.get_nem_port(iface)
logger.info( logger.info(
@ -338,7 +337,7 @@ class EmaneManager:
self.start_daemon(iface) self.start_daemon(iface)
self.install_iface(iface, config) self.install_iface(iface, config)
def get_ifaces(self) -> List[Tuple[EmaneNet, CoreInterface]]: def get_ifaces(self) -> List[Tuple[EmaneNet, TunTap]]:
ifaces = [] ifaces = []
for emane_net in self._emane_nets.values(): for emane_net in self._emane_nets.values():
if not emane_net.model: if not emane_net.model:
@ -352,8 +351,9 @@ class EmaneManager:
iface.name, iface.name,
) )
continue continue
ifaces.append((emane_net, iface)) if isinstance(iface, TunTap):
return sorted(ifaces, key=lambda x: (x[1].node.id, x[1].node_id)) ifaces.append((emane_net, iface))
return sorted(ifaces, key=lambda x: (x[1].node.id, x[1].id))
def setup_control_channels( def setup_control_channels(
self, nem_id: int, iface: CoreInterface, config: Dict[str, str] self, nem_id: int, iface: CoreInterface, config: Dict[str, str]
@ -622,9 +622,9 @@ class EmaneManager:
args = f"{emanecmd} -f {log_file} {platform_xml}" args = f"{emanecmd} -f {log_file} {platform_xml}"
node.host_cmd(args, cwd=self.session.directory) 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") external = config.get("external", "0")
if isinstance(iface, TunTap) and external == "0": if external == "0":
iface.set_ips() iface.set_ips()
# at this point we register location handlers for generating # at this point we register location handlers for generating
# EMANE location events # EMANE location events
@ -732,9 +732,6 @@ class EmaneManager:
self.session.broadcast_node(node) self.session.broadcast_node(node)
return True return True
def is_emane_net(self, net: Optional[CoreNetworkBase]) -> bool:
return isinstance(net, EmaneNet)
def emanerunning(self, node: CoreNode) -> bool: def emanerunning(self, node: CoreNode) -> bool:
""" """
Return True if an EMANE process associated with the given node is running, Return True if an EMANE process associated with the given node is running,

View file

@ -4,7 +4,8 @@ share the same MAC+PHY model.
""" """
import logging 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.data import InterfaceData, LinkData, LinkOptions
from core.emulator.distributed import DistributedServer from core.emulator.distributed import DistributedServer
@ -15,7 +16,7 @@ from core.emulator.enumerations import (
NodeTypes, NodeTypes,
RegisterTlvs, RegisterTlvs,
) )
from core.errors import CoreError from core.errors import CoreCommandError, CoreError
from core.nodes.base import CoreNetworkBase, CoreNode from core.nodes.base import CoreNetworkBase, CoreNode
from core.nodes.interface import CoreInterface from core.nodes.interface import CoreInterface
@ -39,6 +40,114 @@ except ImportError:
logger.debug("compatible emane python bindings not installed") 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): class EmaneNet(CoreNetworkBase):
""" """
EMANE node contains NEM configuration and causes connected nodes EMANE node contains NEM configuration and causes connected nodes
@ -49,7 +158,6 @@ class EmaneNet(CoreNetworkBase):
apitype: NodeTypes = NodeTypes.EMANE apitype: NodeTypes = NodeTypes.EMANE
linktype: LinkTypes = LinkTypes.WIRED linktype: LinkTypes = LinkTypes.WIRED
type: str = "wlan" type: str = "wlan"
has_custom_iface: bool = True
def __init__( def __init__(
self, self,
@ -73,14 +181,11 @@ class EmaneNet(CoreNetworkBase):
return return
self.model.linkconfig(iface, options, iface2) self.model.linkconfig(iface, options, iface2)
def config(self, conf: str) -> None:
self.conf = conf
def startup(self) -> None: def startup(self) -> None:
pass self.up = True
def shutdown(self) -> None: def shutdown(self) -> None:
pass self.up = False
def link(self, iface1: CoreInterface, iface2: CoreInterface) -> None: def link(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
pass pass
@ -88,10 +193,13 @@ class EmaneNet(CoreNetworkBase):
def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None: def unlink(self, iface1: CoreInterface, iface2: CoreInterface) -> None:
pass 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: 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: if not self.model:
raise CoreError(f"no model set to update for node({self.name})") 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) logger.info("node(%s) updating model(%s): %s", self.id, self.model.name, config)
@ -111,7 +219,7 @@ class EmaneNet(CoreNetworkBase):
self.mobility.update_config(config) self.mobility.update_config(config)
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
links = super().links(flags) links = []
emane_manager = self.session.emane emane_manager = self.session.emane
# gather current emane links # gather current emane links
nem_ids = set() nem_ids = set()
@ -132,22 +240,44 @@ class EmaneNet(CoreNetworkBase):
# ignore incomplete links # ignore incomplete links
if (nem2, nem1) not in emane_links: if (nem2, nem1) not in emane_links:
continue continue
link = emane_manager.get_nem_link(nem1, nem2) link = emane_manager.get_nem_link(nem1, nem2, flags)
if link: if link:
links.append(link) links.append(link)
return links return links
def custom_iface(self, node: CoreNode, iface_data: InterfaceData) -> CoreInterface: def create_tuntap(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 Create a tuntap interface for the provided node.
# namespace after it has been bound removes addressing;
# save addresses with the interface now :param node: node to create tuntap interface for
iface_id = node.newtuntap(iface_data.id, iface_data.name) :param iface_data: interface data to create interface with
node.attachnet(iface_id, self) :return: created tuntap interface
iface = node.get_iface(iface_id) """
iface.set_mac(iface_data.mac) with node.lock:
for ip in iface_data.get_ips(): if iface_data.id is not None and iface_data.id in node.ifaces:
iface.add_ip(ip) 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: if self.session.state == EventTypes.RUNTIME_STATE:
self.session.emane.start_iface(self, iface) self.session.emane.start_iface(self, iface)
return iface return iface
def adopt_iface(self, iface: CoreInterface, name: str) -> None:
raise CoreError(
f"emane network({self.name}) do not support adopting interfaces"
)

View file

@ -15,6 +15,7 @@ from fabric import Connection
from invoke import UnexpectedExit from invoke import UnexpectedExit
from core import utils from core import utils
from core.emulator.links import CoreLink
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.executables import get_requirements from core.executables import get_requirements
from core.nodes.interface import GreTap from core.nodes.interface import GreTap
@ -183,21 +184,36 @@ class DistributedController:
def start(self) -> None: def start(self) -> None:
""" """
Start distributed network tunnels. Start distributed network tunnels for control networks.
:return: nothing :return: nothing
""" """
mtu = self.session.options.get_config_int("mtu") mtu = self.session.options.get_config_int("mtu")
for node_id in self.session.nodes: for node_id in self.session.nodes:
node = self.session.nodes[node_id] node = self.session.nodes[node_id]
if not isinstance(node, CoreNetwork): if not isinstance(node, CtrlNet) or node.serverintf is not None:
continue
if isinstance(node, CtrlNet) and node.serverintf is not None:
continue continue
for name in self.servers: for name in self.servers:
server = self.servers[name] server = self.servers[name]
self.create_gre_tunnel(node, server, mtu, True) 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( def create_gre_tunnel(
self, node: CoreNetwork, server: DistributedServer, mtu: int, start: bool self, node: CoreNetwork, server: DistributedServer, mtu: int, start: bool
) -> Tuple[GreTap, GreTap]: ) -> Tuple[GreTap, GreTap]:

View file

@ -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()

View file

@ -35,10 +35,10 @@ from core.emulator.distributed import DistributedController
from core.emulator.enumerations import ( from core.emulator.enumerations import (
EventTypes, EventTypes,
ExceptionLevels, ExceptionLevels,
LinkTypes,
MessageFlags, MessageFlags,
NodeTypes, NodeTypes,
) )
from core.emulator.links import CoreLink, LinkManager
from core.emulator.sessionconfig import SessionConfig from core.emulator.sessionconfig import SessionConfig
from core.errors import CoreError from core.errors import CoreError
from core.location.event import EventLoop from core.location.event import EventLoop
@ -86,6 +86,7 @@ CONTAINER_NODES: Set[Type[NodeBase]] = {DockerNode, LxcNode}
CTRL_NET_ID: int = 9001 CTRL_NET_ID: int = 9001
LINK_COLORS: List[str] = ["green", "blue", "orange", "purple", "turquoise"] LINK_COLORS: List[str] = ["green", "blue", "orange", "purple", "turquoise"]
NT: TypeVar = TypeVar("NT", bound=NodeBase) NT: TypeVar = TypeVar("NT", bound=NodeBase)
WIRELESS_TYPE: Tuple[Type[WlanNode], Type[EmaneNet]] = (WlanNode, EmaneNet)
class Session: class Session:
@ -119,7 +120,8 @@ class Session:
# dict of nodes: all nodes and nets # dict of nodes: all nodes and nets
self.nodes: Dict[int, NodeBase] = {} 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 # states and hooks handlers
self.state: EventTypes = EventTypes.DEFINITION_STATE self.state: EventTypes = EventTypes.DEFINITION_STATE
@ -187,43 +189,48 @@ class Session:
raise CoreError(f"invalid node class: {_class}") raise CoreError(f"invalid node class: {_class}")
return node_type 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: def use_ovs(self) -> bool:
return self.options.get_config("ovs") == "1" 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( def add_link(
self, self,
node1_id: int, node1_id: int,
@ -231,8 +238,7 @@ class Session:
iface1_data: InterfaceData = None, iface1_data: InterfaceData = None,
iface2_data: InterfaceData = None, iface2_data: InterfaceData = None,
options: LinkOptions = None, options: LinkOptions = None,
link_type: LinkTypes = LinkTypes.WIRED, ) -> Tuple[Optional[CoreInterface], Optional[CoreInterface]]:
) -> Tuple[CoreInterface, CoreInterface]:
""" """
Add a link between nodes. Add a link between nodes.
@ -244,89 +250,126 @@ class Session:
data, defaults to none data, defaults to none
:param options: data for creating link, :param options: data for creating link,
defaults to no options defaults to no options
:param link_type: type of link to add
:return: tuple of created core interfaces, depending on link :return: tuple of created core interfaces, depending on link
""" """
if not options: options = options if options else LinkOptions()
options = LinkOptions()
node1 = self.get_node(node1_id, NodeBase)
node2 = self.get_node(node2_id, NodeBase)
iface1 = None
iface2 = None
# set mtu # set mtu
mtu = self.options.get_config_int("mtu") or DEFAULT_MTU mtu = self.options.get_config_int("mtu") or DEFAULT_MTU
if iface1_data: if iface1_data:
iface1_data.mtu = mtu iface1_data.mtu = mtu
if iface2_data: if iface2_data:
iface2_data.mtu = mtu iface2_data.mtu = mtu
# wireless link node1 = self.get_node(node1_id, NodeBase)
if link_type == LinkTypes.WIRELESS: node2 = self.get_node(node2_id, NodeBase)
if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): # check for invalid linking
self._link_wireless(node1, node2, connect=True) if (
else: isinstance(node1, WIRELESS_TYPE)
raise CoreError( and isinstance(node2, WIRELESS_TYPE)
f"cannot wireless link node1({type(node1)}) node2({type(node2)})" or isinstance(node1, WIRELESS_TYPE)
) and not isinstance(node2, CoreNodeBase)
# wired link or not isinstance(node1, CoreNodeBase)
and isinstance(node2, WIRELESS_TYPE)
):
raise CoreError(f"cannot link node({type(node1)}) node({type(node2)})")
# custom links
iface1 = None
iface2 = None
if isinstance(node1, WlanNode):
iface2 = self._add_wlan_link(node2, iface2_data, node1)
elif isinstance(node2, WlanNode):
iface1 = self._add_wlan_link(node1, iface1_data, node2)
elif isinstance(node1, EmaneNet) and isinstance(node2, CoreNode):
iface2 = self._add_emane_link(node2, iface2_data, node1)
elif isinstance(node2, EmaneNet) and isinstance(node1, CoreNode):
iface1 = self._add_emane_link(node1, iface1_data, node2)
else: else:
# peer to peer link iface1, iface2 = self._add_wired_link(
if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): node1, node2, iface1_data, iface2_data, options
logger.info("linking ptp: %s - %s", node1.name, node2.name) )
start = self.state.should_start() # configure tunnel nodes
ptp = self.create_node(PtpNet, start) key = options.key
iface1 = node1.new_iface(ptp, iface1_data) if isinstance(node1, TunnelNode):
iface2 = node2.new_iface(ptp, iface2_data) logger.info("setting tunnel key for: %s", node1.name)
iface1.config(options) node1.setkey(key, iface1_data)
if not options.unidirectional: if isinstance(node2, TunnelNode):
iface2.config(options) logger.info("setting tunnel key for: %s", node2.name)
# link node to net node2.setkey(key, iface2_data)
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)
self.sdt.add_link(node1_id, node2_id) self.sdt.add_link(node1_id, node2_id)
return iface1, iface2 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, self,
node1_id: int, node1: NodeBase,
node2_id: int, node2: NodeBase,
iface1_id: int = None, iface1_data: InterfaceData = None,
iface2_id: int = None, iface2_data: InterfaceData = None,
link_type: LinkTypes = LinkTypes.WIRED, options: LinkOptions = None,
) -> Tuple[CoreInterface, CoreInterface]:
"""
Create a wired link between two nodes.
:param node1: first node to be linked
:param node2: second node to be linked
:param iface1_data: data to create interface for node1
:param iface2_data: data to create interface for node2
:param options: options to configure interfaces with
:return: interfaces created for both nodes
"""
# create interfaces
iface1 = node1.create_iface(iface1_data, options)
iface2 = node2.create_iface(iface2_data, options)
# join and attach to ptp bridge
ptp = self.create_node(PtpNet, self.state.should_start())
ptp.attach(iface1)
ptp.attach(iface2)
# track link
core_link = CoreLink(node1, iface1, node2, iface2, ptp)
self.link_manager.add(core_link)
# setup link for gre tunnels if needed
if ptp.up:
self.distributed.create_gre_tunnels(core_link)
return iface1, iface2
def delete_link(
self, node1_id: int, node2_id: int, iface1_id: int = None, iface2_id: int = None
) -> None: ) -> None:
""" """
Delete a link between nodes. Delete a link between nodes.
@ -335,63 +378,38 @@ class Session:
:param node2_id: node two id :param node2_id: node two id
:param iface1_id: interface id for node one :param iface1_id: interface id for node one
:param iface2_id: interface id for node two :param iface2_id: interface id for node two
:param link_type: link type to delete
:return: nothing :return: nothing
:raises core.CoreError: when no common network is found for link being deleted :raises core.CoreError: when no common network is found for link being deleted
""" """
node1 = self.get_node(node1_id, NodeBase) node1 = self.get_node(node1_id, NodeBase)
node2 = self.get_node(node2_id, NodeBase) node2 = self.get_node(node2_id, NodeBase)
logger.info( logger.info(
"deleting link(%s) node(%s):interface(%s) node(%s):interface(%s)", "deleting link node(%s):interface(%s) node(%s):interface(%s)",
link_type.name,
node1.name, node1.name,
iface1_id, iface1_id,
node2.name, node2.name,
iface2_id, iface2_id,
) )
iface1 = None
# wireless link iface2 = None
if link_type == LinkTypes.WIRELESS: if isinstance(node1, WlanNode):
if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): iface2 = node2.delete_iface(iface2_id)
self._link_wireless(node1, node2, connect=False) node1.detach(iface2)
else: elif isinstance(node2, WlanNode):
raise CoreError( iface1 = node1.delete_iface(iface1_id)
"cannot delete wireless link " node2.detach(iface1)
f"node1({type(node1)}) node2({type(node2)})" elif isinstance(node1, EmaneNet):
) iface2 = node2.delete_iface(iface2_id)
# wired link node1.detach(iface2)
elif isinstance(node2, EmaneNet):
iface1 = node1.delete_iface(iface1_id)
node2.detach(iface1)
else: else:
if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): iface1 = node1.delete_iface(iface1_id)
iface1 = node1.get_iface(iface1_id) iface2 = node2.delete_iface(iface2_id)
iface2 = node2.get_iface(iface2_id) core_link = self.link_manager.delete(node1, iface1, node2, iface2)
if iface1.net != iface2.net: if core_link.ptp:
raise CoreError( self.delete_node(core_link.ptp.id)
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"
)
self.sdt.delete_link(node1_id, node2_id) self.sdt.delete_link(node1_id, node2_id)
def update_link( def update_link(
@ -401,7 +419,6 @@ class Session:
iface1_id: int = None, iface1_id: int = None,
iface2_id: int = None, iface2_id: int = None,
options: LinkOptions = None, options: LinkOptions = None,
link_type: LinkTypes = LinkTypes.WIRED,
) -> None: ) -> None:
""" """
Update link information between nodes. Update link information between nodes.
@ -411,7 +428,6 @@ class Session:
:param iface1_id: interface id for node one :param iface1_id: interface id for node one
:param iface2_id: interface id for node two :param iface2_id: interface id for node two
:param options: data to update link with :param options: data to update link with
:param link_type: type of link to update
:return: nothing :return: nothing
:raises core.CoreError: when updating a wireless type link, when there is a :raises core.CoreError: when updating a wireless type link, when there is a
unknown link between networks unknown link between networks
@ -421,72 +437,26 @@ class Session:
node1 = self.get_node(node1_id, NodeBase) node1 = self.get_node(node1_id, NodeBase)
node2 = self.get_node(node2_id, NodeBase) node2 = self.get_node(node2_id, NodeBase)
logger.info( logger.info(
"update link(%s) node(%s):interface(%s) node(%s):interface(%s)", "update link node(%s):interface(%s) node(%s):interface(%s)",
link_type.name,
node1.name, node1.name,
iface1_id, iface1_id,
node2.name, node2.name,
iface2_id, iface2_id,
) )
iface1 = node1.get_iface(iface1_id) if iface1_id is not None else None
# wireless link iface2 = node2.get_iface(iface2_id) if iface2_id is not None else None
if link_type == LinkTypes.WIRELESS: core_link = self.link_manager.get_link(node1, iface1, node2, iface2)
raise CoreError("cannot update wireless link") if not core_link:
else: raise CoreError(
if isinstance(node1, CoreNodeBase) and isinstance(node2, CoreNodeBase): f"there is no link for node({node1.name}):interface({iface1_id}) "
iface1 = node1.ifaces.get(iface1_id) f"node({node2.name}):interface({iface2_id})"
if not iface1: )
raise CoreError( if iface1:
f"node({node1.name}) missing interface({iface1_id})" iface1.options.update(options)
) iface1.set_config()
iface2 = node2.ifaces.get(iface2_id) if iface2 and not options.unidirectional:
if not iface2: iface2.options.update(options)
raise CoreError( iface2.set_config()
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)})"
)
def next_node_id(self) -> int: def next_node_id(self) -> int:
""" """
@ -703,6 +673,7 @@ class Session:
""" """
self.emane.shutdown() self.emane.shutdown()
self.delete_nodes() self.delete_nodes()
self.link_manager.reset()
self.distributed.shutdown() self.distributed.shutdown()
self.hooks.clear() self.hooks.clear()
self.emane.reset() self.emane.reset()
@ -1426,7 +1397,8 @@ class Session:
ip4_mask=ip4_mask, ip4_mask=ip4_mask,
mtu=DEFAULT_MTU, mtu=DEFAULT_MTU,
) )
iface = node.new_iface(control_net, iface_data) iface = node.create_iface(iface_data)
control_net.attach(iface)
iface.control = True iface.control = True
except ValueError: except ValueError:
msg = f"Control interface not added to node {node.id}. " msg = f"Control interface not added to node {node.id}. "

View file

@ -275,7 +275,7 @@ class NodeConfigDialog(Dialog):
ifaces_scroll.listbox.bind("<<ListboxSelect>>", self.iface_select) ifaces_scroll.listbox.bind("<<ListboxSelect>>", self.iface_select)
# interfaces # interfaces
if self.canvas_node.ifaces: if nutils.is_container(self.node):
self.draw_ifaces() self.draw_ifaces()
self.draw_spacer() self.draw_spacer()

View file

@ -298,7 +298,10 @@ class CanvasNode:
other_iface = edge.other_iface(self) other_iface = edge.other_iface(self)
label = other_node.core_node.name label = other_node.core_node.name
if other_iface: 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) func_unlink = functools.partial(self.click_unlink, edge)
unlink_menu.add_command(label=label, command=func_unlink) unlink_menu.add_command(label=label, command=func_unlink)
themes.style_menu(unlink_menu) themes.style_menu(unlink_menu)

View file

@ -241,10 +241,10 @@ class InterfaceManager:
dst_node = edge.dst.core_node dst_node = edge.dst.core_node
self.determine_subnets(edge.src, edge.dst) self.determine_subnets(edge.src, edge.dst)
src_iface = None 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) src_iface = self.create_iface(edge.src, edge.linked_wireless)
dst_iface = None 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) dst_iface = self.create_iface(edge.dst, edge.linked_wireless)
link = Link( link = Link(
type=LinkType.WIRED, type=LinkType.WIRED,
@ -258,22 +258,26 @@ class InterfaceManager:
def create_iface(self, canvas_node: CanvasNode, wireless_link: bool) -> Interface: def create_iface(self, canvas_node: CanvasNode, wireless_link: bool) -> Interface:
node = canvas_node.core_node node = canvas_node.core_node
ip4, ip6 = self.get_ips(node) if nutils.is_bridge(node):
if wireless_link: iface_id = canvas_node.next_iface_id()
ip4_mask = WIRELESS_IP4_MASK iface = Interface(id=iface_id)
ip6_mask = WIRELESS_IP6_MASK
else: else:
ip4_mask = IP4_MASK ip4, ip6 = self.get_ips(node)
ip6_mask = IP6_MASK if wireless_link:
iface_id = canvas_node.next_iface_id() ip4_mask = WIRELESS_IP4_MASK
name = f"eth{iface_id}" ip6_mask = WIRELESS_IP6_MASK
iface = Interface( else:
id=iface_id, ip4_mask = IP4_MASK
name=name, ip6_mask = IP6_MASK
ip4=ip4, iface_id = canvas_node.next_iface_id()
ip4_mask=ip4_mask, name = f"eth{iface_id}"
ip6=ip6, iface = Interface(
ip6_mask=ip6_mask, 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) logger.info("create node(%s) interface(%s)", node.name, iface)
return iface return iface

View file

@ -97,6 +97,10 @@ def is_custom(node: Node) -> bool:
return is_model(node) and node.model not in NODE_MODELS 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]: def get_custom_services(gui_config: GuiConfig, name: str) -> List[str]:
for custom_node in gui_config.nodes: for custom_node in gui_config.nodes:
if custom_node.name == name: if custom_node.name == name:

View file

@ -320,7 +320,8 @@ class BasicRangeModel(WirelessModel):
loss=self.loss, loss=self.loss,
jitter=self.jitter, jitter=self.jitter,
) )
iface.config(options) iface.options.update(options)
iface.set_config()
def get_position(self, iface: CoreInterface) -> Tuple[float, float, float]: def get_position(self, iface: CoreInterface) -> Tuple[float, float, float]:
""" """

View file

@ -13,11 +13,11 @@ import netaddr
from core import utils from core import utils
from core.configservice.dependencies import ConfigServiceDependencies from core.configservice.dependencies import ConfigServiceDependencies
from core.emulator.data import InterfaceData, LinkData from core.emulator.data import InterfaceData, LinkOptions
from core.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes from core.emulator.enumerations import LinkTypes, NodeTypes
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.executables import BASH, MOUNT, TEST, VCMD, VNODED 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 from core.nodes.netclient import LinuxNetClient, get_net_client
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -74,6 +74,7 @@ class NodeBase(abc.ABC):
self.icon: Optional[str] = None self.icon: Optional[str] = None
self.position: Position = Position() self.position: Position = Position()
self.up: bool = False self.up: bool = False
self.lock: RLock = RLock()
self.net_client: LinuxNetClient = get_net_client( self.net_client: LinuxNetClient = get_net_client(
self.session.use_ovs(), self.host_cmd self.session.use_ovs(), self.host_cmd
) )
@ -96,6 +97,18 @@ class NodeBase(abc.ABC):
""" """
raise NotImplementedError 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( def host_cmd(
self, self,
args: str, args: str,
@ -120,6 +133,19 @@ class NodeBase(abc.ABC):
else: else:
return self.server.remote_cmd(args, env, cwd, wait) 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: def setposition(self, x: float = None, y: float = None, z: float = None) -> bool:
""" """
Set the (x,y,z) position of the object. Set the (x,y,z) position of the object.
@ -139,6 +165,70 @@ class NodeBase(abc.ABC):
""" """
return self.position.get() 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: def get_iface(self, iface_id: int) -> CoreInterface:
""" """
Retrieve interface based on id. Retrieve interface based on id.
@ -191,15 +281,6 @@ class NodeBase(abc.ABC):
self.iface_id += 1 self.iface_id += 1
return iface_id 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): class CoreNodeBase(NodeBase):
""" """
@ -227,14 +308,6 @@ class CoreNodeBase(NodeBase):
self.directory: Optional[Path] = None self.directory: Optional[Path] = None
self.tmpnodedir: bool = False self.tmpnodedir: bool = False
@abc.abstractmethod
def startup(self) -> None:
raise NotImplementedError
@abc.abstractmethod
def shutdown(self) -> None:
raise NotImplementedError
@abc.abstractmethod @abc.abstractmethod
def create_dir(self, dir_path: Path) -> None: def create_dir(self, dir_path: Path) -> None:
""" """
@ -270,19 +343,6 @@ class CoreNodeBase(NodeBase):
""" """
raise NotImplementedError 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 @abc.abstractmethod
def termcmdstring(self, sh: str) -> str: def termcmdstring(self, sh: str) -> str:
""" """
@ -293,19 +353,6 @@ class CoreNodeBase(NodeBase):
""" """
raise NotImplementedError 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 @abc.abstractmethod
def path_exists(self, path: str) -> bool: 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: 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 path: path to translate to host path
:param is_dir: True if path is a directory path, False otherwise :param is_dir: True if path is a directory path, False otherwise
@ -393,54 +440,6 @@ class CoreNodeBase(NodeBase):
if self.tmpnodedir: if self.tmpnodedir:
self.host_cmd(f"rm -rf {self.directory}") 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: def setposition(self, x: float = None, y: float = None, z: float = None) -> None:
""" """
Set position. Set position.
@ -455,25 +454,6 @@ class CoreNodeBase(NodeBase):
for iface in self.get_ifaces(): for iface in self.get_ifaces():
iface.setposition() 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): class CoreNode(CoreNodeBase):
""" """
@ -504,7 +484,6 @@ class CoreNode(CoreNodeBase):
self.directory: Optional[Path] = directory self.directory: Optional[Path] = directory
self.ctrlchnlname: Path = self.session.directory / self.name self.ctrlchnlname: Path = self.session.directory / self.name
self.pid: Optional[int] = None self.pid: Optional[int] = None
self.lock: RLock = RLock()
self._mounts: List[Tuple[Path, Path]] = [] self._mounts: List[Tuple[Path, Path]] = []
self.node_net_client: LinuxNetClient = self.create_node_net_client( self.node_net_client: LinuxNetClient = self.create_node_net_client(
self.session.use_ovs() self.session.use_ovs()
@ -585,6 +564,10 @@ class CoreNode(CoreNodeBase):
self._mounts = [] self._mounts = []
# shutdown all interfaces # shutdown all interfaces
for iface in self.get_ifaces(): for iface in self.get_ifaces():
try:
self.node_net_client.device_flush(iface.name)
except CoreCommandError:
pass
iface.shutdown() iface.shutdown()
# kill node process if present # kill node process if present
try: try:
@ -691,150 +674,6 @@ class CoreNode(CoreNodeBase):
self.cmd(f"{MOUNT} -n --bind {src_path} {target_path}") self.cmd(f"{MOUNT} -n --bind {src_path} {target_path}")
self._mounts.append((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]: def _find_parent_path(self, path: Path) -> Optional[Path]:
""" """
Check if there is a mounted parent directory created for this node. Check if there is a mounted parent directory created for this node.
@ -910,6 +749,48 @@ class CoreNode(CoreNodeBase):
if mode is not None: if mode is not None:
self.host_cmd(f"chmod {mode:o} {host_path}") self.host_cmd(f"chmod {mode:o} {host_path}")
def adopt_iface(self, iface: CoreInterface, name: str) -> None:
"""
Adopt interface to the network namespace of the node and setting
the proper name provided.
:param iface: interface to adopt
:param name: proper name for interface
:return: nothing
"""
# TODO: container, checksums off (container only?)
# TODO: container, get flow id (container only?)
# validate iface belongs to node and get id
iface_id = self.get_iface_id(iface)
if iface_id == -1:
raise CoreError(f"adopting unknown iface({iface.name})")
# add iface to container namespace
self.net_client.device_ns(iface.name, str(self.pid))
# update iface name to container name
name = name if name else f"eth{iface_id}"
self.node_net_client.device_name(iface.name, name)
iface.name = name
# turn checksums off
self.node_net_client.checksums_off(iface.name)
# retrieve flow id for container
iface.flow_id = self.node_net_client.get_ifindex(iface.name)
logger.debug("interface flow index: %s - %s", iface.name, iface.flow_id)
# set mac address
if iface.mac:
self.node_net_client.device_mac(iface.name, str(iface.mac))
logger.debug("interface mac: %s - %s", iface.name, iface.mac)
# set all addresses
for ip in iface.ips():
# ipv4 check
broadcast = None
if netaddr.valid_ipv4(ip):
broadcast = "+"
self.node_net_client.create_address(iface.name, str(ip), broadcast)
# configure iface options
iface.set_config()
# set iface up
self.node_net_client.device_up(iface.name)
class CoreNetworkBase(NodeBase): class CoreNetworkBase(NodeBase):
""" """
@ -917,7 +798,6 @@ class CoreNetworkBase(NodeBase):
""" """
linktype: LinkTypes = LinkTypes.WIRED linktype: LinkTypes = LinkTypes.WIRED
has_custom_iface: bool = False
def __init__( def __init__(
self, self,
@ -941,57 +821,6 @@ class CoreNetworkBase(NodeBase):
self.linked: Dict[CoreInterface, Dict[CoreInterface, bool]] = {} self.linked: Dict[CoreInterface, Dict[CoreInterface, bool]] = {}
self.linked_lock: threading.Lock = threading.Lock() 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: def attach(self, iface: CoreInterface) -> None:
""" """
Attach network interface. Attach network interface.
@ -999,9 +828,10 @@ class CoreNetworkBase(NodeBase):
:param iface: network interface to attach :param iface: network interface to attach
:return: nothing :return: nothing
""" """
i = self.next_iface_id() iface_id = self.next_iface_id()
self.ifaces[i] = iface self.ifaces[iface_id] = iface
iface.net_id = i iface.net = self
iface.net_id = iface_id
with self.linked_lock: with self.linked_lock:
self.linked[iface] = {} self.linked[iface] = {}
@ -1013,56 +843,11 @@ class CoreNetworkBase(NodeBase):
:return: nothing :return: nothing
""" """
del self.ifaces[iface.net_id] del self.ifaces[iface.net_id]
iface.net = None
iface.net_id = None iface.net_id = None
with self.linked_lock: with self.linked_lock:
del self.linked[iface] 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: class Position:
""" """

View file

@ -93,10 +93,9 @@ class DockerNode(CoreNode):
will run on, default is None for localhost will run on, default is None for localhost
:param image: image to start container with :param image: image to start container with
""" """
if image is None:
image = "ubuntu"
self.image: str = image
super().__init__(session, _id, name, directory, server) 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: 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 # nothing to do if node is not up
if not self.up: if not self.up:
return return
with self.lock: with self.lock:
self.ifaces.clear() self.ifaces.clear()
self.client.stop_container() self.client.stop_container()

View file

@ -4,7 +4,6 @@ virtual ethernet classes that implement the interfaces available under Linux.
import logging import logging
import math import math
import time
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Callable, Dict, List, Optional 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__) logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from core.emulator.distributed import DistributedServer
from core.emulator.session import Session 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 DEFAULT_MTU: int = 1500
IFACE_NAME_LENGTH: int = 15
def tc_clear_cmd(name: str) -> str: def tc_clear_cmd(name: str) -> str:
@ -78,35 +78,42 @@ class CoreInterface:
def __init__( def __init__(
self, self,
session: "Session", _id: int,
name: str, name: str,
localname: str, localname: str,
use_ovs: bool,
mtu: int = DEFAULT_MTU, mtu: int = DEFAULT_MTU,
node: "NodeBase" = None,
server: "DistributedServer" = None, server: "DistributedServer" = None,
node: "CoreNode" = None,
) -> None: ) -> None:
""" """
Creates a CoreInterface instance. Creates a CoreInterface instance.
:param session: core session instance :param _id: interface id for associated node
:param name: interface name :param name: interface name
:param localname: interface local name :param localname: interface local name
:param use_ovs: True to use ovs, False otherwise
:param mtu: mtu value :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 server: remote server node will run on, default is None for localhost
:param node: node for interface
""" """
if len(name) >= 16: if len(name) >= IFACE_NAME_LENGTH:
raise CoreError(f"interface name ({name}) too long, max 16") raise CoreError(
if len(localname) >= 16: f"interface name ({name}) too long, max {IFACE_NAME_LENGTH}"
raise CoreError(f"interface local name ({localname}) too long, max 16") )
self.session: "Session" = session if len(localname) >= IFACE_NAME_LENGTH:
self.node: Optional["CoreNode"] = node 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.name: str = name
self.localname: str = localname self.localname: str = localname
self.up: bool = False self.up: bool = False
self.mtu: int = mtu self.mtu: int = mtu
self.net: Optional[CoreNetworkBase] = None self.net: Optional[CoreNetworkBase] = None
self.othernet: Optional[CoreNetworkBase] = None
self.ip4s: List[netaddr.IPNetwork] = [] self.ip4s: List[netaddr.IPNetwork] = []
self.ip6s: List[netaddr.IPNetwork] = [] self.ip6s: List[netaddr.IPNetwork] = []
self.mac: Optional[netaddr.EUI] = None self.mac: Optional[netaddr.EUI] = None
@ -114,20 +121,12 @@ class CoreInterface:
self.poshook: Callable[[CoreInterface], None] = lambda x: None self.poshook: Callable[[CoreInterface], None] = lambda x: None
# used with EMANE # used with EMANE
self.transport_type: TransportType = TransportType.VIRTUAL 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 # id used to find flow data
self.flow_id: Optional[int] = None self.flow_id: Optional[int] = None
self.server: Optional["DistributedServer"] = server self.server: Optional["DistributedServer"] = server
self.net_client: LinuxNetClient = get_net_client( self.net_client: LinuxNetClient = get_net_client(use_ovs, self.host_cmd)
self.session.use_ovs(), self.host_cmd
)
self.control: bool = False self.control: bool = False
# configuration data # configuration data
self.has_local_netem: bool = False
self.local_options: LinkOptions = LinkOptions()
self.has_netem: bool = False self.has_netem: bool = False
self.options: LinkOptions = LinkOptions() self.options: LinkOptions = LinkOptions()
@ -161,7 +160,13 @@ class CoreInterface:
:return: nothing :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: def shutdown(self) -> None:
""" """
@ -169,29 +174,14 @@ class CoreInterface:
:return: nothing :return: nothing
""" """
pass if not self.up:
return
def attachnet(self, net: "CoreNetworkBase") -> None: if self.localname:
""" try:
Attach network. self.net_client.delete_device(self.localname)
except CoreCommandError:
:param net: network to attach pass
:return: nothing self.up = False
"""
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)
def add_ip(self, ip: str) -> None: def add_ip(self, ip: str) -> None:
""" """
@ -303,41 +293,24 @@ class CoreInterface:
""" """
return self.transport_type == TransportType.VIRTUAL return self.transport_type == TransportType.VIRTUAL
def config(self, options: LinkOptions, use_local: bool = True) -> None: def set_config(self) -> 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
# clear current settings # clear current settings
if current_options.is_clear(): if self.options.is_clear():
clear_local_netem = use_local and self.has_local_netem if self.has_netem:
clear_netem = not use_local and self.has_netem cmd = tc_clear_cmd(self.name)
if clear_local_netem or clear_netem: if self.node:
cmd = tc_clear_cmd(name) self.node.cmd(cmd)
self.host_cmd(cmd)
if use_local:
self.has_local_netem = False
else: else:
self.has_netem = False self.host_cmd(cmd)
self.has_netem = False
# set updated settings # set updated settings
else: else:
cmd = tc_cmd(name, current_options, self.mtu) cmd = tc_cmd(self.name, self.options, self.mtu)
self.host_cmd(cmd) if self.node:
if use_local: self.node.cmd(cmd)
self.has_local_netem = True
else: else:
self.has_netem = True self.host_cmd(cmd)
self.has_netem = True
def get_data(self) -> InterfaceData: def get_data(self) -> InterfaceData:
""" """
@ -345,231 +318,22 @@ class CoreInterface:
:return: interface data :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() ip4 = self.get_ip4()
if ip4: ip4_addr = str(ip4.ip) if ip4 else None
data.ip4 = str(ip4.ip) ip4_mask = ip4.prefixlen if ip4 else None
data.ip4_mask = ip4.prefixlen
ip6 = self.get_ip6() ip6 = self.get_ip6()
if ip6: ip6_addr = str(ip6.ip) if ip6 else None
data.ip6 = str(ip6.ip) ip6_mask = ip6.prefixlen if ip6 else None
data.ip6_mask = ip6.prefixlen mac = str(self.mac) if self.mac else None
return data return InterfaceData(
id=self.id,
name=self.name,
class Veth(CoreInterface): mac=mac,
""" ip4=ip4_addr,
Provides virtual ethernet functionality for core nodes. ip4_mask=ip4_mask,
""" ip6=ip6_addr,
ip6_mask=ip6_mask,
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))
class GreTap(CoreInterface): class GreTap(CoreInterface):
@ -594,7 +358,7 @@ class GreTap(CoreInterface):
""" """
Creates a GreTap instance. Creates a GreTap instance.
:param session: core session instance :param session: session for this gre tap
:param remoteip: remote address :param remoteip: remote address
:param key: gre tap key :param key: gre tap key
:param node: related core node :param node: related core node
@ -612,7 +376,7 @@ class GreTap(CoreInterface):
sessionid = session.short_session_id() sessionid = session.short_session_id()
localname = f"gt.{self.id}.{sessionid}" localname = f"gt.{self.id}.{sessionid}"
name = f"{localname}p" 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.transport_type: TransportType = TransportType.RAW
self.remote_ip: str = remoteip self.remote_ip: str = remoteip
self.ttl: int = ttl self.ttl: int = ttl

View file

@ -6,6 +6,7 @@ from tempfile import NamedTemporaryFile
from typing import TYPE_CHECKING, Callable, Dict, Optional from typing import TYPE_CHECKING, Callable, Dict, Optional
from core import utils from core import utils
from core.emulator.data import InterfaceData, LinkOptions
from core.emulator.distributed import DistributedServer from core.emulator.distributed import DistributedServer
from core.emulator.enumerations import NodeTypes from core.emulator.enumerations import NodeTypes
from core.errors import CoreCommandError from core.errors import CoreCommandError
@ -89,10 +90,9 @@ class LxcNode(CoreNode):
will run on, default is None for localhost will run on, default is None for localhost
:param image: image to start container with :param image: image to start container with
""" """
if image is None:
image = "ubuntu"
self.image: str = image
super().__init__(session, _id, name, directory, server) 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: def alive(self) -> bool:
""" """
@ -125,7 +125,6 @@ class LxcNode(CoreNode):
# nothing to do if node is not up # nothing to do if node is not up
if not self.up: if not self.up:
return return
with self.lock: with self.lock:
self.ifaces.clear() self.ifaces.clear()
self.client.stop_container() self.client.stop_container()
@ -212,7 +211,10 @@ class LxcNode(CoreNode):
if mode is not None: if mode is not None:
self.cmd(f"chmod {mode:o} {dst_path}") self.cmd(f"chmod {mode:o} {dst_path}")
def add_iface(self, iface: CoreInterface, iface_id: int) -> None: def create_iface(
super().add_iface(iface, iface_id) 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 # adding small delay to allow time for adding addresses to work correctly
time.sleep(0.5) time.sleep(0.5)
return iface

View file

@ -22,8 +22,8 @@ from core.emulator.enumerations import (
) )
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.executables import NFTABLES from core.executables import NFTABLES
from core.nodes.base import CoreNetworkBase, CoreNode from core.nodes.base import CoreNetworkBase
from core.nodes.interface import CoreInterface, GreTap, Veth from core.nodes.interface import CoreInterface, GreTap
from core.nodes.netclient import get_net_client from core.nodes.netclient import get_net_client
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -280,6 +280,17 @@ class CoreNetwork(CoreNetworkBase):
self.up = True self.up = True
nft_queue.start() 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: def shutdown(self) -> None:
""" """
Linux bridge shutdown logic. Linux bridge shutdown logic.
@ -309,9 +320,9 @@ class CoreNetwork(CoreNetworkBase):
:param iface: network interface to attach :param iface: network interface to attach
:return: nothing :return: nothing
""" """
super().attach(iface)
if self.up: if self.up:
iface.net_client.set_iface_master(self.brname, iface.localname) iface.net_client.set_iface_master(self.brname, iface.localname)
super().attach(iface)
def detach(self, iface: CoreInterface) -> None: def detach(self, iface: CoreInterface) -> None:
""" """
@ -320,9 +331,9 @@ class CoreNetwork(CoreNetworkBase):
:param iface: network interface to detach :param iface: network interface to detach
:return: nothing :return: nothing
""" """
super().detach(iface)
if self.up: if self.up:
iface.net_client.delete_iface(self.brname, iface.localname) iface.net_client.delete_iface(self.brname, iface.localname)
super().detach(iface)
def is_linked(self, iface1: CoreInterface, iface2: CoreInterface) -> bool: def is_linked(self, iface1: CoreInterface, iface2: CoreInterface) -> bool:
""" """
@ -378,67 +389,6 @@ class CoreNetwork(CoreNetworkBase):
self.linked[iface1][iface2] = True self.linked[iface1][iface2] = True
nft_queue.update(self) 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): class GreTapBridge(CoreNetwork):
""" """
@ -686,15 +636,6 @@ class CtrlNet(CoreNetwork):
super().shutdown() 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): class PtpNet(CoreNetwork):
""" """
@ -714,49 +655,6 @@ class PtpNet(CoreNetwork):
raise CoreError("ptp links support at most 2 network interfaces") raise CoreError("ptp links support at most 2 network interfaces")
super().attach(iface) super().attach(iface)
def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]:
"""
Get peer to peer link.
:param flags: message flags
:return: list of link data
"""
all_links = []
if len(self.ifaces) != 2:
return all_links
ifaces = self.get_ifaces()
iface1 = ifaces[0]
iface2 = ifaces[1]
unidirectional = 0 if iface1.local_options == iface2.local_options else 1
iface1_data = iface1.get_data()
iface2_data = iface2.get_data()
link_data = LinkData(
message_type=flags,
type=self.linktype,
node1_id=iface1.node.id,
node2_id=iface2.node.id,
iface1=iface1_data,
iface2=iface2_data,
options=iface1.local_options,
)
link_data.options.unidirectional = unidirectional
all_links.append(link_data)
# build a 2nd link message for the upstream link parameters
# (swap if1 and if2)
if unidirectional:
link_data = LinkData(
message_type=MessageFlags.NONE,
type=self.linktype,
node1_id=iface2.node.id,
node2_id=iface1.node.id,
iface1=InterfaceData(id=iface2_data.id),
iface2=InterfaceData(id=iface1_data.id),
options=iface2.local_options,
)
link_data.options.unidirectional = unidirectional
all_links.append(link_data)
return all_links
class SwitchNode(CoreNetwork): class SwitchNode(CoreNetwork):
""" """
@ -883,10 +781,10 @@ class WlanNode(CoreNetwork):
:param flags: message flags :param flags: message flags
:return: list of link data :return: list of link data
""" """
links = super().links(flags)
if self.model: if self.model:
links.extend(self.model.links(flags)) return self.model.links(flags)
return links else:
return []
class TunnelNode(GreTapBridge): class TunnelNode(GreTapBridge):

View file

@ -3,17 +3,18 @@ PhysicalNode class for including real systems in the emulated network.
""" """
import logging import logging
import threading
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, List, Optional, Tuple from typing import TYPE_CHECKING, List, Optional, Tuple
from core.emulator.data import InterfaceData import netaddr
from core.emulator.data import InterfaceData, LinkOptions
from core.emulator.distributed import DistributedServer from core.emulator.distributed import DistributedServer
from core.emulator.enumerations import NodeTypes, TransportType from core.emulator.enumerations import NodeTypes, TransportType
from core.errors import CoreCommandError, CoreError from core.errors import CoreCommandError, CoreError
from core.executables import MOUNT, TEST, UMOUNT from core.executables import BASH, TEST, UMOUNT
from core.nodes.base import CoreNetworkBase, CoreNodeBase from core.nodes.base import CoreNode, CoreNodeBase
from core.nodes.interface import DEFAULT_MTU, CoreInterface from core.nodes.interface import CoreInterface
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -21,185 +22,6 @@ if TYPE_CHECKING:
from core.emulator.session import Session 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): class Rj45Node(CoreNodeBase):
""" """
RJ45Node is a physical interface on the host linked to the emulated RJ45Node is a physical interface on the host linked to the emulated
@ -214,7 +36,6 @@ class Rj45Node(CoreNodeBase):
session: "Session", session: "Session",
_id: int = None, _id: int = None,
name: str = None, name: str = None,
mtu: int = DEFAULT_MTU,
server: DistributedServer = None, server: DistributedServer = None,
) -> None: ) -> None:
""" """
@ -223,17 +44,14 @@ class Rj45Node(CoreNodeBase):
:param session: core session instance :param session: core session instance
:param _id: node id :param _id: node id
:param name: node name :param name: node name
:param mtu: rj45 mtu
:param server: remote server node :param server: remote server node
will run on, default is None for localhost will run on, default is None for localhost
""" """
super().__init__(session, _id, name, server) super().__init__(session, _id, name, server)
self.iface: CoreInterface = CoreInterface( self.iface: CoreInterface = CoreInterface(
session, name, name, mtu, server, self self.iface_id, name, name, session.use_ovs(), node=self, server=server
) )
self.iface.transport_type = TransportType.RAW self.iface.transport_type = TransportType.RAW
self.lock: threading.RLock = threading.RLock()
self.iface_id: Optional[int] = None
self.old_up: bool = False self.old_up: bool = False
self.old_addrs: List[Tuple[str, Optional[str]]] = [] self.old_addrs: List[Tuple[str, Optional[str]]] = []
@ -245,7 +63,7 @@ class Rj45Node(CoreNodeBase):
:raises CoreCommandError: when there is a command exception :raises CoreCommandError: when there is a command exception
""" """
# interface will also be marked up during net.attach() # interface will also be marked up during net.attach()
self.savestate() self.save_state()
self.net_client.device_up(self.iface.localname) self.net_client.device_up(self.iface.localname)
self.up = True self.up = True
@ -266,7 +84,7 @@ class Rj45Node(CoreNodeBase):
except CoreCommandError: except CoreCommandError:
pass pass
self.up = False self.up = False
self.restorestate() self.restore_state()
def path_exists(self, path: str) -> bool: def path_exists(self, path: str) -> bool:
""" """
@ -281,33 +99,28 @@ class Rj45Node(CoreNodeBase):
except CoreCommandError: except CoreCommandError:
return False return False
def new_iface( def create_iface(
self, net: CoreNetworkBase, iface_data: InterfaceData self, iface_data: InterfaceData = None, options: LinkOptions = None
) -> CoreInterface: ) -> 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: with self.lock:
iface_id = iface_data.id if self.iface.id in self.ifaces:
if iface_id is None:
iface_id = 0
if self.iface.net is not None:
raise CoreError( 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 if iface_data and iface_data.mtu is not None:
self.iface_id = iface_id self.iface.mtu = iface_data.mtu
self.iface.attachnet(net) self.iface.ip4s.clear()
self.iface.ip6s.clear()
for ip in iface_data.get_ips(): for ip in iface_data.get_ips():
self.add_ip(ip) self.iface.add_ip(ip)
return self.iface self.ifaces[self.iface.id] = self.iface
if self.up:
for ip in self.iface.ips():
self.net_client.create_address(self.iface.name, str(ip))
return self.iface
def adopt_iface(self, iface: CoreInterface, name: str) -> None:
raise CoreError(f"rj45({self.name}) does not support adopt interface")
def delete_iface(self, iface_id: int) -> None: def delete_iface(self, iface_id: int) -> None:
""" """
@ -318,16 +131,10 @@ class Rj45Node(CoreNodeBase):
""" """
self.get_iface(iface_id) self.get_iface(iface_id)
self.ifaces.pop(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() self.shutdown()
def get_iface(self, iface_id: int) -> CoreInterface: 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") raise CoreError(f"node({self.name}) interface({iface_id}) does not exist")
return self.iface return self.iface
@ -341,42 +148,17 @@ class Rj45Node(CoreNodeBase):
""" """
if iface is not self.iface: if iface is not self.iface:
raise CoreError(f"node({self.name}) does not have interface({iface.name})") 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: def save_state(self) -> 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:
""" """
Save the addresses and other interface state before using the 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 :return: nothing
:raises CoreCommandError: when there is a command exception :raises CoreCommandError: when there is a command exception
""" """
# TODO: save/restore the PROMISC flag
self.old_up = False self.old_up = False
self.old_addrs: List[Tuple[str, Optional[str]]] = [] self.old_addrs: List[Tuple[str, Optional[str]]] = []
localname = self.iface.localname localname = self.iface.localname
@ -397,7 +179,7 @@ class Rj45Node(CoreNodeBase):
self.old_addrs.append((items[1], None)) self.old_addrs.append((items[1], None))
logger.info("saved rj45 state: addrs(%s) up(%s)", self.old_addrs, self.old_up) 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. Restore the addresses and other interface state after using it.
@ -437,3 +219,69 @@ class Rj45Node(CoreNodeBase):
def copy_file(self, src_path: Path, dst_path: Path, mode: int = None) -> None: def copy_file(self, src_path: Path, dst_path: Path, mode: int = None) -> None:
raise CoreError("rj45 does not support copying files") 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)

View file

@ -1,6 +1,6 @@
import logging import logging
from pathlib import Path 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 from lxml import etree
@ -8,11 +8,12 @@ import core.nodes.base
import core.nodes.physical import core.nodes.physical
from core import utils from core import utils
from core.emane.nodes import EmaneNet 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.emulator.enumerations import EventTypes, NodeTypes
from core.errors import CoreXmlError from core.errors import CoreXmlError
from core.nodes.base import CoreNodeBase, NodeBase from core.nodes.base import CoreNodeBase, NodeBase
from core.nodes.docker import DockerNode from core.nodes.docker import DockerNode
from core.nodes.interface import CoreInterface
from core.nodes.lxd import LxcNode from core.nodes.lxd import LxcNode
from core.nodes.network import CtrlNet, GreTapBridge, WlanNode from core.nodes.network import CtrlNet, GreTapBridge, WlanNode
from core.services.coreservices import CoreService from core.services.coreservices import CoreService
@ -269,8 +270,8 @@ class CoreXmlWriter:
def write_session(self) -> None: def write_session(self) -> None:
# generate xml content # generate xml content
links = self.write_nodes() self.write_nodes()
self.write_links(links) self.write_links()
self.write_mobility_configs() self.write_mobility_configs()
self.write_emane_configs() self.write_emane_configs()
self.write_service_configs() self.write_service_configs()
@ -449,8 +450,7 @@ class CoreXmlWriter:
if node_types.getchildren(): if node_types.getchildren():
self.scenario.append(node_types) self.scenario.append(node_types)
def write_nodes(self) -> List[LinkData]: def write_nodes(self) -> None:
links = []
for node_id in self.session.nodes: for node_id in self.session.nodes:
node = self.session.nodes[node_id] node = self.session.nodes[node_id]
# network node # network node
@ -464,10 +464,6 @@ class CoreXmlWriter:
elif isinstance(node, core.nodes.base.CoreNodeBase): elif isinstance(node, core.nodes.base.CoreNodeBase):
self.write_device(node) self.write_device(node)
# add known links
links.extend(node.links())
return links
def write_network(self, node: NodeBase) -> None: def write_network(self, node: NodeBase) -> None:
# ignore p2p and other nodes that are not part of the api # ignore p2p and other nodes that are not part of the api
if not node.apitype: if not node.apitype:
@ -476,15 +472,21 @@ class CoreXmlWriter:
network = NetworkElement(self.session, node) network = NetworkElement(self.session, node)
self.networks.append(network.element) self.networks.append(network.element)
def write_links(self, links: List[LinkData]) -> None: def write_links(self) -> None:
link_elements = etree.Element("links") link_elements = etree.Element("links")
# add link data for core_link in self.session.link_manager.links():
for link_data in links: node1, iface1 = core_link.node1, core_link.iface1
# skip basic range links node2, iface2 = core_link.node2, core_link.iface2
if link_data.iface1 is None and link_data.iface2 is None: unidirectional = core_link.is_unidirectional()
continue link_element = self.create_link_element(
link_element = self.create_link_element(link_data) node1, iface1, node2, iface2, core_link.options(), unidirectional
)
link_elements.append(link_element) 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(): if link_elements.getchildren():
self.scenario.append(link_elements) self.scenario.append(link_elements)
@ -493,67 +495,71 @@ class CoreXmlWriter:
self.devices.append(device.element) self.devices.append(device.element)
def create_iface_element( def create_iface_element(
self, element_name: str, node_id: int, iface_data: InterfaceData self, element_name: str, iface: CoreInterface
) -> etree.Element: ) -> etree.Element:
iface_element = etree.Element(element_name) iface_element = etree.Element(element_name)
node = self.session.get_node(node_id, NodeBase) # check if interface if connected to emane
if isinstance(node, CoreNodeBase): if isinstance(iface.node, CoreNodeBase) and isinstance(iface.net, EmaneNet):
iface = node.get_iface(iface_data.id) nem_id = self.session.emane.get_nem_id(iface)
# check if emane interface add_attribute(iface_element, "nem", nem_id)
if isinstance(iface.net, EmaneNet): ip4 = iface.get_ip4()
nem_id = self.session.emane.get_nem_id(iface) ip4_mask = None
add_attribute(iface_element, "nem", nem_id) if ip4:
add_attribute(iface_element, "id", iface_data.id) ip4_mask = ip4.prefixlen
add_attribute(iface_element, "name", iface_data.name) ip4 = str(ip4.ip)
add_attribute(iface_element, "mac", iface_data.mac) ip6 = iface.get_ip6()
add_attribute(iface_element, "ip4", iface_data.ip4) ip6_mask = None
add_attribute(iface_element, "ip4_mask", iface_data.ip4_mask) if ip6:
add_attribute(iface_element, "ip6", iface_data.ip6) ip6_mask = ip6.prefixlen
add_attribute(iface_element, "ip6_mask", iface_data.ip6_mask) 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 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") link_element = etree.Element("link")
add_attribute(link_element, "node1", link_data.node1_id) add_attribute(link_element, "node1", node1.id)
add_attribute(link_element, "node2", link_data.node2_id) add_attribute(link_element, "node2", node2.id)
# check for interface one # check for interface one
if link_data.iface1 is not None: if iface1 is not None:
iface1 = self.create_iface_element( iface1 = self.create_iface_element("iface1", iface1)
"iface1", link_data.node1_id, link_data.iface1
)
link_element.append(iface1) link_element.append(iface1)
# check for interface two # check for interface two
if link_data.iface2 is not None: if iface2 is not None:
iface2 = self.create_iface_element( iface2 = self.create_iface_element("iface2", iface2)
"iface2", link_data.node2_id, link_data.iface2
)
link_element.append(iface2) link_element.append(iface2)
# check for options, don't write for emane/wlan links # 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_node1_wireless = isinstance(node1, (WlanNode, EmaneNet))
is_node2_wireless = isinstance(node2, (WlanNode, EmaneNet)) is_node2_wireless = isinstance(node2, (WlanNode, EmaneNet))
if not any([is_node1_wireless, is_node2_wireless]): if not (is_node1_wireless or is_node2_wireless):
options_data = link_data.options unidirectional = 1 if unidirectional else 0
options = etree.Element("options") options_element = etree.Element("options")
add_attribute(options, "delay", options_data.delay) add_attribute(options_element, "delay", options.delay)
add_attribute(options, "bandwidth", options_data.bandwidth) add_attribute(options_element, "bandwidth", options.bandwidth)
add_attribute(options, "loss", options_data.loss) add_attribute(options_element, "loss", options.loss)
add_attribute(options, "dup", options_data.dup) add_attribute(options_element, "dup", options.dup)
add_attribute(options, "jitter", options_data.jitter) add_attribute(options_element, "jitter", options.jitter)
add_attribute(options, "mer", options_data.mer) add_attribute(options_element, "mer", options.mer)
add_attribute(options, "burst", options_data.burst) add_attribute(options_element, "burst", options.burst)
add_attribute(options, "mburst", options_data.mburst) add_attribute(options_element, "mburst", options.mburst)
add_attribute(options, "unidirectional", options_data.unidirectional) add_attribute(options_element, "unidirectional", unidirectional)
add_attribute(options, "network_id", link_data.network_id) add_attribute(options_element, "key", options.key)
add_attribute(options, "key", options_data.key) add_attribute(options_element, "buffer", options.buffer)
add_attribute(options, "buffer", options_data.buffer) if options_element.items():
if options.items(): link_element.append(options_element)
link_element.append(options)
return link_element return link_element

View file

@ -61,6 +61,8 @@ service CoreApi {
} }
rpc DeleteLink (DeleteLinkRequest) returns (DeleteLinkResponse) { rpc DeleteLink (DeleteLinkRequest) returns (DeleteLinkResponse) {
} }
rpc Linked (LinkedRequest) returns (LinkedResponse) {
}
// mobility rpc // mobility rpc
rpc GetMobilityConfig (mobility.GetMobilityConfigRequest) returns (mobility.GetMobilityConfigResponse) { rpc GetMobilityConfig (mobility.GetMobilityConfigRequest) returns (mobility.GetMobilityConfigResponse) {
@ -683,3 +685,15 @@ message Server {
string name = 1; string name = 1;
string host = 2; 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 {
}

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "core" name = "core"
version = "8.1.0" version = "8.2.0"
description = "CORE Common Open Research Emulator" description = "CORE Common Open Research Emulator"
authors = ["Boeing Research and Technology"] authors = ["Boeing Research and Technology"]
license = "BSD-2-Clause" license = "BSD-2-Clause"

View file

@ -1,22 +1,24 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import json
import sys import sys
from argparse import ( from argparse import (
ArgumentDefaultsHelpFormatter, ArgumentDefaultsHelpFormatter,
ArgumentParser, ArgumentParser,
ArgumentTypeError, ArgumentTypeError,
Namespace, Namespace,
_SubParsersAction,
) )
from functools import wraps from functools import wraps
from pathlib import Path from pathlib import Path
from typing import Optional, Tuple from typing import Any, Dict, Optional, Tuple
import grpc import grpc
import netaddr import netaddr
from google.protobuf.json_format import MessageToDict
from netaddr import EUI, AddrFormatError, IPNetwork from netaddr import EUI, AddrFormatError, IPNetwork
from core.api.grpc.client import CoreGrpcClient from core.api.grpc.client import CoreGrpcClient
from core.api.grpc.wrappers import ( from core.api.grpc.wrappers import (
ConfigOption,
Geo, Geo,
Interface, Interface,
Link, Link,
@ -29,6 +31,15 @@ from core.api.grpc.wrappers import (
NODE_TYPES = [x for x in NodeType if x != NodeType.PEER_TO_PEER] 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): def coreclient(func):
@wraps(func) @wraps(func)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
@ -94,11 +105,11 @@ def geo_type(value: str) -> Tuple[float, float, float]:
return lon, lat, alt return lon, lat, alt
def file_type(value: str) -> str: def file_type(value: str) -> Path:
path = Path(value) path = Path(value)
if not path.is_file(): if not path.is_file():
raise ArgumentTypeError(f"invalid file: {value}") raise ArgumentTypeError(f"invalid file: {value}")
return str(path.absolute()) return path
def get_current_session(core: CoreGrpcClient, session_id: Optional[int]) -> int: def get_current_session(core: CoreGrpcClient, session_id: Optional[int]) -> int:
@ -140,12 +151,15 @@ def print_iface(iface: Interface) -> None:
def get_wlan_config(core: CoreGrpcClient, args: Namespace) -> None: def get_wlan_config(core: CoreGrpcClient, args: Namespace) -> None:
session_id = get_current_session(core, args.session) session_id = get_current_session(core, args.session)
config = core.get_wlan_config(session_id, args.node) config = core.get_wlan_config(session_id, args.node)
size = 0 if args.json:
for option in config.values(): print_json(ConfigOption.to_dict(config))
size = max(size, len(option.name)) else:
print(f"{'Name':<{size}.{size}} | Value") size = 0
for option in config.values(): for option in config.values():
print(f"{option.name:<{size}.{size}} | {option.value}") 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 @coreclient
@ -163,80 +177,102 @@ def set_wlan_config(core: CoreGrpcClient, args: Namespace) -> None:
if args.range: if args.range:
config["range"] = str(args.range) config["range"] = str(args.range)
result = core.set_wlan_config(session_id, args.node, config) 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 @coreclient
def open_xml(core: CoreGrpcClient, args: Namespace) -> None: def open_xml(core: CoreGrpcClient, args: Namespace) -> None:
result, session_id = core.open_xml(args.file, args.start) 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 @coreclient
def query_sessions(core: CoreGrpcClient, args: Namespace) -> None: def query_sessions(core: CoreGrpcClient, args: Namespace) -> None:
sessions = core.get_sessions() sessions = core.get_sessions()
print("Session ID | Session State | Nodes") if args.json:
for session in sessions: sessions = [protobuf_to_json(x.to_proto()) for x in sessions]
print(f"{session.id:<10} | {session.state.name:<13} | {session.nodes}") 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 @coreclient
def query_session(core: CoreGrpcClient, args: Namespace) -> None: def query_session(core: CoreGrpcClient, args: Namespace) -> None:
session = core.get_session(args.id) session = core.get_session(args.id)
print("Nodes") if args.json:
print("Node ID | Node Name | Node Type") session = protobuf_to_json(session.to_proto())
for node in session.nodes.values(): print_json(session)
print(f"{node.id:<7} | {node.name:<9} | {node.type.name}") else:
print("\nLinks") print("Nodes")
for link in session.links: print("ID | Name | Type | XY | Geo")
n1 = session.nodes[link.node1_id].name for node in session.nodes.values():
n2 = session.nodes[link.node2_id].name xy_pos = f"{int(node.position.x)},{int(node.position.y)}"
print(f"Node | ", end="") geo_pos = f"{node.geo.lon:.7f},{node.geo.lat:.7f},{node.geo.alt:f}"
print_iface_header() print(f"{node.id:<7} | {node.name[:7]:<7} | {node.type.name[:7]:<7} | {xy_pos:<9} | {geo_pos}")
print(f"{n1:<6} | ", end="") print("\nLinks")
if link.iface1: for link in session.links:
print_iface(link.iface1) n1 = session.nodes[link.node1_id].name
else: 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()
print(f"{n2:<6} | ", end="")
if link.iface2:
print_iface(link.iface2)
else:
print()
print()
@coreclient @coreclient
def query_node(core: CoreGrpcClient, args: Namespace) -> None: def query_node(core: CoreGrpcClient, args: Namespace) -> None:
session = core.get_session(args.id) session = core.get_session(args.id)
node, ifaces, _ = core.get_node(args.id, args.node) node, ifaces, _ = core.get_node(args.id, args.node)
print("ID | Name | Type | XY") if args.json:
xy_pos = f"{int(node.position.x)},{int(node.position.y)}" node = protobuf_to_json(node.to_proto())
print(f"{node.id:<4} | {node.name[:7]:<7} | {node.type.name[:7]:<7} | {xy_pos}") ifaces = [protobuf_to_json(x.to_proto()) for x in ifaces]
if node.geo: print_json(dict(node=node, ifaces=ifaces))
print("Geo") else:
print(f"{node.geo.lon:.7f},{node.geo.lat:.7f},{node.geo.alt:f}") print("ID | Name | Type | XY | Geo")
if ifaces: xy_pos = f"{int(node.position.x)},{int(node.position.y)}"
print("Interfaces") geo_pos = f"{node.geo.lon:.7f},{node.geo.lat:.7f},{node.geo.alt:f}"
print("Connected To | ", end="") print(f"{node.id:<7} | {node.name[:7]:<7} | {node.type.name[:7]:<7} | {xy_pos:<9} | {geo_pos}")
print_iface_header() if ifaces:
for iface in ifaces: print("Interfaces")
if iface.net_id == node.id: print("Connected To | ", end="")
if iface.node_id: print_iface_header()
name = session.nodes[iface.node_id].name 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: else:
name = session.nodes[iface.net2_id].name net_node = session.nodes.get(iface.net_id)
else: name = net_node.name if net_node else ""
net_node = session.nodes.get(iface.net_id) print(f"{name:<12} | ", end="")
name = net_node.name if net_node else "" print_iface(iface)
print(f"{name:<12} | ", end="")
print_iface(iface)
@coreclient @coreclient
def delete_session(core: CoreGrpcClient, args: Namespace) -> None: def delete_session(core: CoreGrpcClient, args: Namespace) -> None:
result = core.delete_session(args.id) 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 @coreclient
@ -263,14 +299,20 @@ def add_node(core: CoreGrpcClient, args: Namespace) -> None:
geo=geo, geo=geo,
) )
node_id = core.add_node(session_id, node) 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 @coreclient
def edit_node(core: CoreGrpcClient, args: Namespace) -> None: def edit_node(core: CoreGrpcClient, args: Namespace) -> None:
session_id = get_current_session(core, args.session) session_id = get_current_session(core, args.session)
result = core.edit_node(session_id, args.id, args.icon) 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 @coreclient
@ -285,14 +327,20 @@ def move_node(core: CoreGrpcClient, args: Namespace) -> None:
lon, lat, alt = args.geo lon, lat, alt = args.geo
geo = Geo(lon=lon, lat=lat, alt=alt) geo = Geo(lon=lon, lat=lat, alt=alt)
result = core.move_node(session_id, args.id, pos, geo) 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 @coreclient
def delete_node(core: CoreGrpcClient, args: Namespace) -> None: def delete_node(core: CoreGrpcClient, args: Namespace) -> None:
session_id = get_current_session(core, args.session) session_id = get_current_session(core, args.session)
result = core.delete_node(session_id, args.id) 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 @coreclient
@ -313,8 +361,13 @@ def add_link(core: CoreGrpcClient, args: Namespace) -> None:
unidirectional=args.uni, unidirectional=args.uni,
) )
link = Link(args.node1, args.node2, iface1=iface1, iface2=iface2, options=options) link = Link(args.node1, args.node2, iface1=iface1, iface2=iface2, options=options)
result, _, _ = core.add_link(session_id, link) result, iface1, iface2 = core.add_link(session_id, link)
print(f"add link: {result}") 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 @coreclient
@ -332,7 +385,10 @@ def edit_link(core: CoreGrpcClient, args: Namespace) -> None:
iface2 = Interface(args.iface2) iface2 = Interface(args.iface2)
link = Link(args.node1, args.node2, iface1=iface1, iface2=iface2, options=options) link = Link(args.node1, args.node2, iface1=iface1, iface2=iface2, options=options)
result = core.edit_link(session_id, link) 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 @coreclient
@ -342,10 +398,13 @@ def delete_link(core: CoreGrpcClient, args: Namespace) -> None:
iface2 = Interface(args.iface2) iface2 = Interface(args.iface2)
link = Link(args.node1, args.node2, iface1=iface1, iface2=iface2) link = Link(args.node1, args.node2, iface1=iface1, iface2=iface2)
result = core.delete_link(session_id, link) 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 = parent.add_parser("session", help="session interactions")
parser.formatter_class = ArgumentDefaultsHelpFormatter parser.formatter_class = ArgumentDefaultsHelpFormatter
parser.add_argument("-i", "--id", type=int, help="session id to use", required=True) 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) 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 = parent.add_parser("node", help="node interactions")
parser.formatter_class = ArgumentDefaultsHelpFormatter parser.formatter_class = ArgumentDefaultsHelpFormatter
parser.add_argument("-s", "--session", type=int, help="session to interact with") 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) 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 = parent.add_parser("link", help="link interactions")
parser.formatter_class = ArgumentDefaultsHelpFormatter parser.formatter_class = ArgumentDefaultsHelpFormatter
parser.add_argument("-s", "--session", type=int, help="session to interact with") 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) 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") parser = parent.add_parser("query", help="query interactions")
subparsers = parser.add_subparsers(help="query commands") subparsers = parser.add_subparsers(help="query commands")
subparsers.required = True subparsers.required = True
@ -477,7 +536,7 @@ def setup_query_parser(parent: _SubParsersAction) -> None:
node_parser.set_defaults(func=query_node) 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 = parent.add_parser("xml", help="open session xml")
parser.formatter_class = ArgumentDefaultsHelpFormatter parser.formatter_class = ArgumentDefaultsHelpFormatter
parser.add_argument("-f", "--file", type=file_type, help="xml file to open", required=True) 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) 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 = parent.add_parser("wlan", help="wlan specific interactions")
parser.formatter_class = ArgumentDefaultsHelpFormatter parser.formatter_class = ArgumentDefaultsHelpFormatter
parser.add_argument("-s", "--session", type=int, help="session to interact with") 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: def main() -> None:
parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) 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 = parser.add_subparsers(help="supported commands")
subparsers.required = True subparsers.required = True
subparsers.dest = "command" subparsers.dest = "command"

View file

@ -9,7 +9,6 @@ from typing import List, Type
import pytest import pytest
from core.emulator.data import IpPrefixes, NodeOptions from core.emulator.data import IpPrefixes, NodeOptions
from core.emulator.enumerations import MessageFlags
from core.emulator.session import Session from core.emulator.session import Session
from core.errors import CoreCommandError from core.errors import CoreCommandError
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
@ -63,44 +62,6 @@ class TestCore:
status = ping(node1, node2, ip_prefixes) status = ping(node1, node2, ip_prefixes)
assert not status 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): def test_wlan_ping(self, session: Session, ip_prefixes: IpPrefixes):
""" """
Test basic wlan network. Test basic wlan network.

View file

@ -29,12 +29,14 @@ class TestDistributed:
# when # when
session.distributed.add_server(server_name, host) session.distributed.add_server(server_name, host)
node1 = session.add_node(HubNode)
options = NodeOptions(server=server_name) 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() session.instantiate()
# then # then
assert node.server is not None assert node2.server is not None
assert node.server.name == server_name assert node2.server.name == server_name
assert node.server.host == host assert node2.server.host == host
assert len(session.distributed.tunnels) > 0 assert len(session.distributed.tunnels) == 1

View file

@ -34,7 +34,7 @@ from core.api.grpc.wrappers import (
from core.emane.models.ieee80211abg import EmaneIeee80211abgModel from core.emane.models.ieee80211abg import EmaneIeee80211abgModel
from core.emane.nodes import EmaneNet from core.emane.nodes import EmaneNet
from core.emulator.data import EventData, IpPrefixes, NodeData, NodeOptions 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.errors import CoreError
from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility from core.location.mobility import BasicRangeModel, Ns2ScriptedMobility
from core.nodes.base import CoreNode from core.nodes.base import CoreNode
@ -413,7 +413,7 @@ class TestGrpc:
session = grpc_server.coreemu.create_session() session = grpc_server.coreemu.create_session()
switch = session.add_node(SwitchNode) switch = session.add_node(SwitchNode)
node = session.add_node(CoreNode) 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) iface = InterfaceHelper("10.0.0.0/24").create_iface(node.id, 0)
link = Link(node.id, switch.id, iface1=iface) link = Link(node.id, switch.id, iface1=iface)
@ -423,7 +423,7 @@ class TestGrpc:
# then # then
assert result is True assert result is True
assert len(switch.links()) == 1 assert len(session.link_manager.links()) == 1
assert iface1.id == iface.id assert iface1.id == iface.id
assert iface1.ip4 == iface.ip4 assert iface1.ip4 == iface.ip4
@ -445,11 +445,10 @@ class TestGrpc:
session = grpc_server.coreemu.create_session() session = grpc_server.coreemu.create_session()
switch = session.add_node(SwitchNode) switch = session.add_node(SwitchNode)
node = session.add_node(CoreNode) node = session.add_node(CoreNode)
iface = ip_prefixes.create_iface(node) iface_data = ip_prefixes.create_iface(node)
session.add_link(node.id, switch.id, iface) iface, _ = session.add_link(node.id, switch.id, iface_data)
options = LinkOptions(bandwidth=30000) options = LinkOptions(bandwidth=30000)
link = switch.links()[0] assert iface.options.bandwidth != options.bandwidth
assert options.bandwidth != link.options.bandwidth
link = Link(node.id, switch.id, iface1=Interface(id=iface.id), options=options) link = Link(node.id, switch.id, iface1=Interface(id=iface.id), options=options)
# then # then
@ -458,8 +457,7 @@ class TestGrpc:
# then # then
assert result is True assert result is True
link = switch.links()[0] assert options.bandwidth == iface.options.bandwidth
assert options.bandwidth == link.options.bandwidth
def test_delete_link(self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes): def test_delete_link(self, grpc_server: CoreGrpcServer, ip_prefixes: IpPrefixes):
# given # given
@ -470,13 +468,7 @@ class TestGrpc:
node2 = session.add_node(CoreNode) node2 = session.add_node(CoreNode)
iface2 = ip_prefixes.create_iface(node2) iface2 = ip_prefixes.create_iface(node2)
session.add_link(node1.id, node2.id, iface1, iface2) session.add_link(node1.id, node2.id, iface1, iface2)
link_node = None assert len(session.link_manager.links()) == 1
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
link = Link( link = Link(
node1.id, node1.id,
node2.id, node2.id,
@ -490,7 +482,7 @@ class TestGrpc:
# then # then
assert result is True 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): def test_get_wlan_config(self, grpc_server: CoreGrpcServer):
# given # given
@ -755,9 +747,11 @@ class TestGrpc:
session = grpc_server.coreemu.create_session() session = grpc_server.coreemu.create_session()
wlan = session.add_node(WlanNode) wlan = session.add_node(WlanNode)
node = session.add_node(CoreNode) node = session.add_node(CoreNode)
iface = ip_prefixes.create_iface(node) iface_data = ip_prefixes.create_iface(node)
session.add_link(node.id, wlan.id, iface) session.add_link(node.id, wlan.id, iface_data)
link_data = wlan.links()[0] core_link = list(session.link_manager.links())[0]
link_data = core_link.get_data(MessageFlags.ADD)
queue = Queue() queue = Queue()
def handle_event(event: Event) -> None: def handle_event(event: Event) -> None:
@ -932,3 +926,26 @@ class TestGrpc:
with pytest.raises(grpc.RpcError): with pytest.raises(grpc.RpcError):
with client.context_connect(): with client.context_connect():
client.move_nodes(streamer) 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

View file

@ -46,14 +46,17 @@ class TestLinks:
) )
# then # then
assert len(session.link_manager.links()) == 1
assert node1.get_iface(iface1_data.id) assert node1.get_iface(iface1_data.id)
assert node2.get_iface(iface2_data.id) assert node2.get_iface(iface2_data.id)
assert iface1 is not None 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 iface2 is not None
assert iface1.local_options == LINK_OPTIONS assert iface2.options == LINK_OPTIONS
assert iface1.has_local_netem assert iface2.has_netem
assert iface2.local_options == LINK_OPTIONS assert node1.get_iface(iface1_data.id)
assert iface2.has_local_netem
def test_add_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): def test_add_node_to_net(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
@ -62,16 +65,20 @@ class TestLinks:
iface1_data = ip_prefixes.create_iface(node1) iface1_data = ip_prefixes.create_iface(node1)
# when # when
iface, _ = session.add_link( iface1, iface2 = session.add_link(
node1.id, node2.id, iface1_data=iface1_data, options=LINK_OPTIONS node1.id, node2.id, iface1_data=iface1_data, options=LINK_OPTIONS
) )
# then # 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 node1.get_iface(iface1_data.id)
assert iface is not None assert iface2 is not None
assert iface.local_options == LINK_OPTIONS assert iface2.options == LINK_OPTIONS
assert iface.has_local_netem assert iface2.has_netem
assert node2.get_iface(iface1_data.id)
def test_add_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): def test_add_net_to_node(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
@ -80,32 +87,37 @@ class TestLinks:
iface2_data = ip_prefixes.create_iface(node2) iface2_data = ip_prefixes.create_iface(node2)
# when # when
_, iface = session.add_link( iface1, iface2 = session.add_link(
node1.id, node2.id, iface2_data=iface2_data, options=LINK_OPTIONS node1.id, node2.id, iface2_data=iface2_data, options=LINK_OPTIONS
) )
# then # then
assert node1.links() assert len(session.link_manager.links()) == 1
assert node2.get_iface(iface2_data.id) assert iface1 is not None
assert iface is not None assert iface1.options == LINK_OPTIONS
assert iface.local_options == LINK_OPTIONS assert iface1.has_netem
assert iface.has_local_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 # given
node1 = session.add_node(SwitchNode) node1 = session.add_node(SwitchNode)
node2 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode)
# when # 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 # then
assert node1.links() assert len(session.link_manager.links()) == 1
assert iface is not None assert iface1 is not None
assert iface.local_options == LINK_OPTIONS assert iface1.options == LINK_OPTIONS
assert iface.options == LINK_OPTIONS assert iface1.has_netem
assert iface.has_local_netem assert iface2 is not None
assert iface.has_netem assert iface2.options == LINK_OPTIONS
assert iface2.has_netem
def test_add_node_to_node_uni(self, session: Session, ip_prefixes: IpPrefixes): def test_add_node_to_node_uni(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
@ -141,48 +153,52 @@ class TestLinks:
) )
# then # then
assert len(session.link_manager.links()) == 1
assert node1.get_iface(iface1_data.id) assert node1.get_iface(iface1_data.id)
assert node2.get_iface(iface2_data.id) assert node2.get_iface(iface2_data.id)
assert iface1 is not None assert iface1 is not None
assert iface1.options == link_options1
assert iface1.has_netem
assert iface2 is not None assert iface2 is not None
assert iface1.local_options == link_options1 assert iface2.options == link_options2
assert iface1.has_local_netem assert iface2.has_netem
assert iface2.local_options == link_options2
assert iface2.has_local_netem
def test_update_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): def test_update_node_to_net(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
node1 = session.add_node(CoreNode) node1 = session.add_node(CoreNode)
node2 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode)
iface1_data = ip_prefixes.create_iface(node1) iface1_data = ip_prefixes.create_iface(node1)
iface1, _ = session.add_link(node1.id, node2.id, iface1_data) iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data)
assert iface1.local_options != LINK_OPTIONS assert len(session.link_manager.links()) == 1
assert iface1.options != LINK_OPTIONS
assert iface2.options != LINK_OPTIONS
# when # when
session.update_link( session.update_link(node1.id, node2.id, iface1.id, iface2.id, LINK_OPTIONS)
node1.id, node2.id, iface1_id=iface1_data.id, options=LINK_OPTIONS
)
# then # then
assert iface1.local_options == LINK_OPTIONS assert iface1.options == LINK_OPTIONS
assert iface1.has_local_netem 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): def test_update_net_to_node(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
node1 = session.add_node(SwitchNode) node1 = session.add_node(SwitchNode)
node2 = session.add_node(CoreNode) node2 = session.add_node(CoreNode)
iface2_data = ip_prefixes.create_iface(node2) iface2_data = ip_prefixes.create_iface(node2)
_, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data) iface1, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data)
assert iface2.local_options != LINK_OPTIONS assert iface1.options != LINK_OPTIONS
assert iface2.options != LINK_OPTIONS
# when # when
session.update_link( session.update_link(node1.id, node2.id, iface1.id, iface2.id, LINK_OPTIONS)
node1.id, node2.id, iface2_id=iface2_data.id, options=LINK_OPTIONS
)
# then # then
assert iface2.local_options == LINK_OPTIONS assert iface1.options == LINK_OPTIONS
assert iface2.has_local_netem assert iface1.has_netem
assert iface2.options == LINK_OPTIONS
assert iface2.has_netem
def test_update_ptp(self, session: Session, ip_prefixes: IpPrefixes): def test_update_ptp(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
@ -191,55 +207,68 @@ class TestLinks:
iface1_data = ip_prefixes.create_iface(node1) iface1_data = ip_prefixes.create_iface(node1)
iface2_data = ip_prefixes.create_iface(node2) iface2_data = ip_prefixes.create_iface(node2)
iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data) iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data)
assert iface1.local_options != LINK_OPTIONS assert iface1.options != LINK_OPTIONS
assert iface2.local_options != LINK_OPTIONS assert iface2.options != LINK_OPTIONS
# when # when
session.update_link( session.update_link(node1.id, node2.id, iface1.id, iface2.id, LINK_OPTIONS)
node1.id, node2.id, iface1_data.id, iface2_data.id, LINK_OPTIONS
)
# then # then
assert iface1.local_options == LINK_OPTIONS assert iface1.options == LINK_OPTIONS
assert iface1.has_local_netem assert iface1.has_netem
assert iface2.local_options == LINK_OPTIONS assert iface2.options == LINK_OPTIONS
assert iface2.has_local_netem assert iface2.has_netem
def test_update_net_to_net(self, session: Session, ip_prefixes: IpPrefixes): def test_update_net_to_net(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
node1 = session.add_node(SwitchNode) node1 = session.add_node(SwitchNode)
node2 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode)
iface1, _ = session.add_link(node1.id, node2.id) iface1, iface2 = session.add_link(node1.id, node2.id)
assert iface1.local_options != LINK_OPTIONS assert iface1.options != LINK_OPTIONS
assert iface2.options != LINK_OPTIONS
# when # 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 # then
assert iface1.local_options == LINK_OPTIONS
assert iface1.has_local_netem
assert iface1.options == LINK_OPTIONS assert iface1.options == LINK_OPTIONS
assert iface1.has_netem 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): def test_clear_net_to_net(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
node1 = session.add_node(SwitchNode) node1 = session.add_node(SwitchNode)
node2 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode)
iface1, _ = session.add_link(node1.id, node2.id, options=LINK_OPTIONS) iface1, iface2 = session.add_link(node1.id, node2.id, options=LINK_OPTIONS)
assert iface1.local_options == LINK_OPTIONS
assert iface1.has_local_netem
assert iface1.options == LINK_OPTIONS assert iface1.options == LINK_OPTIONS
assert iface1.has_netem assert iface1.has_netem
assert iface2.options == LINK_OPTIONS
assert iface2.has_netem
# when # when
options = LinkOptions(delay=0, bandwidth=0, loss=0.0, dup=0, jitter=0, buffer=0) 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 # then
assert iface1.local_options.is_clear()
assert not iface1.has_local_netem
assert iface1.options.is_clear() assert iface1.options.is_clear()
assert not iface1.has_netem 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): def test_delete_node_to_node(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
@ -247,82 +276,100 @@ class TestLinks:
node2 = session.add_node(CoreNode) node2 = session.add_node(CoreNode)
iface1_data = ip_prefixes.create_iface(node1) iface1_data = ip_prefixes.create_iface(node1)
iface2_data = ip_prefixes.create_iface(node2) iface2_data = ip_prefixes.create_iface(node2)
session.add_link(node1.id, node2.id, iface1_data, iface2_data) iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data)
assert node1.get_iface(iface1_data.id) assert len(session.link_manager.links()) == 1
assert node2.get_iface(iface2_data.id) assert node1.get_iface(iface1.id)
assert node2.get_iface(iface2.id)
# when # 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 # then
assert iface1_data.id not in node1.ifaces assert len(session.link_manager.links()) == 0
assert iface2_data.id not in node2.ifaces 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): def test_delete_node_to_net(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
node1 = session.add_node(CoreNode) node1 = session.add_node(CoreNode)
node2 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode)
iface1_data = ip_prefixes.create_iface(node1) iface1_data = ip_prefixes.create_iface(node1)
session.add_link(node1.id, node2.id, iface1_data) iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data)
assert node1.get_iface(iface1_data.id) assert len(session.link_manager.links()) == 1
assert node1.get_iface(iface1.id)
assert node2.get_iface(iface2.id)
# when # 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 # 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): def test_delete_net_to_node(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
node1 = session.add_node(SwitchNode) node1 = session.add_node(SwitchNode)
node2 = session.add_node(CoreNode) node2 = session.add_node(CoreNode)
iface2_data = ip_prefixes.create_iface(node2) iface2_data = ip_prefixes.create_iface(node2)
session.add_link(node1.id, node2.id, iface2_data=iface2_data) iface1, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data)
assert node2.get_iface(iface2_data.id) assert len(session.link_manager.links()) == 1
assert node1.get_iface(iface1.id)
assert node2.get_iface(iface2.id)
# when # 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 # 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): def test_delete_net_to_net(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
node1 = session.add_node(SwitchNode) node1 = session.add_node(SwitchNode)
node2 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode)
session.add_link(node1.id, node2.id) iface1, iface2 = session.add_link(node1.id, node2.id)
assert node1.get_linked_iface(node2) assert len(session.link_manager.links()) == 1
assert node1.get_iface(iface1.id)
assert node2.get_iface(iface2.id)
# when # when
session.delete_link(node1.id, node2.id) session.delete_link(node1.id, node2.id, iface1.id, iface2.id)
# then # 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): def test_delete_node_error(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
node1 = session.add_node(SwitchNode) node1 = session.add_node(SwitchNode)
node2 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode)
session.add_link(node1.id, node2.id) iface1, iface2 = session.add_link(node1.id, node2.id)
assert node1.get_linked_iface(node2) assert len(session.link_manager.links()) == 1
assert node1.get_iface(iface1.id)
assert node2.get_iface(iface2.id)
# when # when
with pytest.raises(CoreError): 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): 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): def test_delete_net_to_net_error(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
node1 = session.add_node(SwitchNode) node1 = session.add_node(SwitchNode)
node2 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode)
node3 = session.add_node(SwitchNode) node3 = session.add_node(SwitchNode)
session.add_link(node1.id, node2.id) iface1, iface2 = session.add_link(node1.id, node2.id)
assert node1.get_linked_iface(node2) assert len(session.link_manager.links()) == 1
assert node1.get_iface(iface1.id)
assert node2.get_iface(iface2.id)
# when # when
with pytest.raises(CoreError): 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): def test_delete_node_to_net_error(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
@ -330,12 +377,14 @@ class TestLinks:
node2 = session.add_node(SwitchNode) node2 = session.add_node(SwitchNode)
node3 = session.add_node(SwitchNode) node3 = session.add_node(SwitchNode)
iface1_data = ip_prefixes.create_iface(node1) iface1_data = ip_prefixes.create_iface(node1)
iface1, _ = session.add_link(node1.id, node2.id, iface1_data) iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data)
assert iface1 assert len(session.link_manager.links()) == 1
assert node1.get_iface(iface1.id)
assert node2.get_iface(iface2.id)
# when # when
with pytest.raises(CoreError): 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): def test_delete_net_to_node_error(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
@ -343,12 +392,14 @@ class TestLinks:
node2 = session.add_node(CoreNode) node2 = session.add_node(CoreNode)
node3 = session.add_node(SwitchNode) node3 = session.add_node(SwitchNode)
iface2_data = ip_prefixes.create_iface(node2) iface2_data = ip_prefixes.create_iface(node2)
_, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data) iface1, iface2 = session.add_link(node1.id, node2.id, iface2_data=iface2_data)
assert iface2 assert len(session.link_manager.links()) == 1
assert node1.get_iface(iface1.id)
assert node2.get_iface(iface2.id)
# when # when
with pytest.raises(CoreError): 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): def test_delete_node_to_node_error(self, session: Session, ip_prefixes: IpPrefixes):
# given # given
@ -358,9 +409,10 @@ class TestLinks:
iface1_data = ip_prefixes.create_iface(node1) iface1_data = ip_prefixes.create_iface(node1)
iface2_data = ip_prefixes.create_iface(node2) iface2_data = ip_prefixes.create_iface(node2)
iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data) iface1, iface2 = session.add_link(node1.id, node2.id, iface1_data, iface2_data)
assert iface1 assert len(session.link_manager.links()) == 1
assert iface2 assert node1.get_iface(iface1.id)
assert node2.get_iface(iface2.id)
# when # when
with pytest.raises(CoreError): with pytest.raises(CoreError):
session.delete_link(node1.id, node3.id) session.delete_link(node1.id, node3.id, iface1.id, iface2.id)

View file

@ -60,6 +60,40 @@ class TestNodes:
with pytest.raises(CoreError): with pytest.raises(CoreError):
session.get_node(node.id, CoreNode) 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( @pytest.mark.parametrize(
"mac,expected", "mac,expected",
[ [
@ -70,12 +104,11 @@ class TestNodes:
def test_node_set_mac(self, session: Session, mac: str, expected: str): def test_node_set_mac(self, session: Session, mac: str, expected: str):
# given # given
node = session.add_node(CoreNode) node = session.add_node(CoreNode)
switch = session.add_node(SwitchNode)
iface_data = InterfaceData() iface_data = InterfaceData()
iface = node.new_iface(switch, iface_data) iface = node.create_iface(iface_data)
# when # when
node.set_mac(iface.node_id, mac) iface.set_mac(mac)
# then # then
assert str(iface.mac) == expected assert str(iface.mac) == expected
@ -86,13 +119,12 @@ class TestNodes:
def test_node_set_mac_exception(self, session: Session, mac: str): def test_node_set_mac_exception(self, session: Session, mac: str):
# given # given
node = session.add_node(CoreNode) node = session.add_node(CoreNode)
switch = session.add_node(SwitchNode)
iface_data = InterfaceData() iface_data = InterfaceData()
iface = node.new_iface(switch, iface_data) iface = node.create_iface(iface_data)
# when # when
with pytest.raises(CoreError): with pytest.raises(CoreError):
node.set_mac(iface.node_id, mac) iface.set_mac(mac)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"ip,expected,is_ip6", "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): def test_node_add_ip(self, session: Session, ip: str, expected: str, is_ip6: bool):
# given # given
node = session.add_node(CoreNode) node = session.add_node(CoreNode)
switch = session.add_node(SwitchNode)
iface_data = InterfaceData() iface_data = InterfaceData()
iface = node.new_iface(switch, iface_data) iface = node.create_iface(iface_data)
# when # when
node.add_ip(iface.node_id, ip) iface.add_ip(ip)
# then # then
if is_ip6: if is_ip6:
@ -122,14 +153,13 @@ class TestNodes:
def test_node_add_ip_exception(self, session): def test_node_add_ip_exception(self, session):
# given # given
node = session.add_node(CoreNode) node = session.add_node(CoreNode)
switch = session.add_node(SwitchNode)
iface_data = InterfaceData() iface_data = InterfaceData()
iface = node.new_iface(switch, iface_data) iface = node.create_iface(iface_data)
ip = "256.168.0.1/24" ip = "256.168.0.1/24"
# when # when
with pytest.raises(CoreError): with pytest.raises(CoreError):
node.add_ip(iface.node_id, ip) iface.add_ip(ip)
@pytest.mark.parametrize("net_type", NET_TYPES) @pytest.mark.parametrize("net_type", NET_TYPES)
def test_net(self, session, net_type): def test_net(self, session, net_type):

View file

@ -10,7 +10,7 @@ from core.emulator.session import Session
from core.errors import CoreError from core.errors import CoreError
from core.location.mobility import BasicRangeModel from core.location.mobility import BasicRangeModel
from core.nodes.base import CoreNode 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 from core.services.utility import SshService
@ -65,25 +65,18 @@ class TestXml:
:param tmpdir: tmpdir to create data in :param tmpdir: tmpdir to create data in
:param ip_prefixes: generates ip addresses for nodes :param ip_prefixes: generates ip addresses for nodes
""" """
# create ptp
ptp_node = session.add_node(PtpNet)
# create nodes # create nodes
node1 = session.add_node(CoreNode) node1 = session.add_node(CoreNode)
node2 = session.add_node(CoreNode) node2 = session.add_node(CoreNode)
# link nodes to ptp net # link nodes
for node in [node1, node2]: iface1_data = ip_prefixes.create_iface(node1)
iface_data = ip_prefixes.create_iface(node) iface2_data = ip_prefixes.create_iface(node2)
session.add_link(node.id, ptp_node.id, iface1_data=iface_data) session.add_link(node1.id, node2.id, iface1_data, iface2_data)
# instantiate session # instantiate session
session.instantiate() session.instantiate()
# get ids for nodes
node1_id = node1.id
node2_id = node2.id
# save xml # save xml
xml_file = tmpdir.join("session.xml") xml_file = tmpdir.join("session.xml")
file_path = Path(xml_file.strpath) file_path = Path(xml_file.strpath)
@ -98,16 +91,19 @@ class TestXml:
# verify nodes have been removed from session # verify nodes have been removed from session
with pytest.raises(CoreError): with pytest.raises(CoreError):
assert not session.get_node(node1_id, CoreNode) assert not session.get_node(node1.id, CoreNode)
with pytest.raises(CoreError): 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 # load saved xml
session.open_xml(file_path, start=True) session.open_xml(file_path, start=True)
# verify nodes have been recreated # verify nodes have been recreated
assert session.get_node(node1_id, CoreNode) assert session.get_node(node1.id, CoreNode)
assert session.get_node(node2_id, CoreNode) assert session.get_node(node2.id, CoreNode)
assert len(session.link_manager.links()) == 1
def test_xml_ptp_services( def test_xml_ptp_services(
self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
@ -119,18 +115,15 @@ class TestXml:
:param tmpdir: tmpdir to create data in :param tmpdir: tmpdir to create data in
:param ip_prefixes: generates ip addresses for nodes :param ip_prefixes: generates ip addresses for nodes
""" """
# create ptp
ptp_node = session.add_node(PtpNet)
# create nodes # create nodes
options = NodeOptions(model="host") options = NodeOptions(model="host")
node1 = session.add_node(CoreNode, options=options) node1 = session.add_node(CoreNode, options=options)
node2 = session.add_node(CoreNode) node2 = session.add_node(CoreNode)
# link nodes to ptp net # link nodes to ptp net
for node in [node1, node2]: iface1_data = ip_prefixes.create_iface(node1)
iface_data = ip_prefixes.create_iface(node) iface2_data = ip_prefixes.create_iface(node2)
session.add_link(node.id, ptp_node.id, iface1_data=iface_data) session.add_link(node1.id, node2.id, iface1_data, iface2_data)
# set custom values for node service # set custom values for node service
session.services.set_service(node1.id, SshService.name) session.services.set_service(node1.id, SshService.name)
@ -143,10 +136,6 @@ class TestXml:
# instantiate session # instantiate session
session.instantiate() session.instantiate()
# get ids for nodes
node1_id = node1.id
node2_id = node2.id
# save xml # save xml
xml_file = tmpdir.join("session.xml") xml_file = tmpdir.join("session.xml")
file_path = Path(xml_file.strpath) file_path = Path(xml_file.strpath)
@ -161,9 +150,9 @@ class TestXml:
# verify nodes have been removed from session # verify nodes have been removed from session
with pytest.raises(CoreError): with pytest.raises(CoreError):
assert not session.get_node(node1_id, CoreNode) assert not session.get_node(node1.id, CoreNode)
with pytest.raises(CoreError): with pytest.raises(CoreError):
assert not session.get_node(node2_id, CoreNode) assert not session.get_node(node2.id, CoreNode)
# load saved xml # load saved xml
session.open_xml(file_path, start=True) session.open_xml(file_path, start=True)
@ -172,8 +161,8 @@ class TestXml:
service = session.services.get_service(node1.id, SshService.name) service = session.services.get_service(node1.id, SshService.name)
# verify nodes have been recreated # verify nodes have been recreated
assert session.get_node(node1_id, CoreNode) assert session.get_node(node1.id, CoreNode)
assert session.get_node(node2_id, CoreNode) assert session.get_node(node2.id, CoreNode)
assert service.config_data.get(service_file) == file_data assert service.config_data.get(service_file) == file_data
def test_xml_mobility( def test_xml_mobility(
@ -187,8 +176,8 @@ class TestXml:
:param ip_prefixes: generates ip addresses for nodes :param ip_prefixes: generates ip addresses for nodes
""" """
# create wlan # create wlan
wlan_node = session.add_node(WlanNode) wlan = session.add_node(WlanNode)
session.mobility.set_model(wlan_node, BasicRangeModel, {"test": "1"}) session.mobility.set_model(wlan, BasicRangeModel, {"test": "1"})
# create nodes # create nodes
options = NodeOptions(model="mdr") options = NodeOptions(model="mdr")
@ -199,16 +188,11 @@ class TestXml:
# link nodes # link nodes
for node in [node1, node2]: for node in [node1, node2]:
iface_data = ip_prefixes.create_iface(node) 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 # instantiate session
session.instantiate() session.instantiate()
# get ids for nodes
wlan_id = wlan_node.id
node1_id = node1.id
node2_id = node2.id
# save xml # save xml
xml_file = tmpdir.join("session.xml") xml_file = tmpdir.join("session.xml")
file_path = Path(xml_file.strpath) file_path = Path(xml_file.strpath)
@ -223,20 +207,20 @@ class TestXml:
# verify nodes have been removed from session # verify nodes have been removed from session
with pytest.raises(CoreError): with pytest.raises(CoreError):
assert not session.get_node(node1_id, CoreNode) assert not session.get_node(node1.id, CoreNode)
with pytest.raises(CoreError): with pytest.raises(CoreError):
assert not session.get_node(node2_id, CoreNode) assert not session.get_node(node2.id, CoreNode)
# load saved xml # load saved xml
session.open_xml(file_path, start=True) session.open_xml(file_path, start=True)
# retrieve configuration we set originally # 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 # verify nodes and configuration were restored
assert session.get_node(node1_id, CoreNode) assert session.get_node(node1.id, CoreNode)
assert session.get_node(node2_id, CoreNode) assert session.get_node(node2.id, CoreNode)
assert session.get_node(wlan_id, WlanNode) assert session.get_node(wlan.id, WlanNode)
assert value == "1" assert value == "1"
def test_network_to_network(self, session: Session, tmpdir: TemporaryFile): def test_network_to_network(self, session: Session, tmpdir: TemporaryFile):
@ -256,10 +240,6 @@ class TestXml:
# instantiate session # instantiate session
session.instantiate() session.instantiate()
# get ids for nodes
node1_id = switch1.id
node2_id = switch2.id
# save xml # save xml
xml_file = tmpdir.join("session.xml") xml_file = tmpdir.join("session.xml")
file_path = Path(xml_file.strpath) file_path = Path(xml_file.strpath)
@ -274,19 +254,19 @@ class TestXml:
# verify nodes have been removed from session # verify nodes have been removed from session
with pytest.raises(CoreError): with pytest.raises(CoreError):
assert not session.get_node(node1_id, SwitchNode) assert not session.get_node(switch1.id, SwitchNode)
with pytest.raises(CoreError): with pytest.raises(CoreError):
assert not session.get_node(node2_id, SwitchNode) assert not session.get_node(switch2.id, SwitchNode)
# load saved xml # load saved xml
session.open_xml(file_path, start=True) session.open_xml(file_path, start=True)
# verify nodes have been recreated # verify nodes have been recreated
switch1 = session.get_node(node1_id, SwitchNode) switch1 = session.get_node(switch1.id, SwitchNode)
switch2 = session.get_node(node2_id, SwitchNode) switch2 = session.get_node(switch2.id, SwitchNode)
assert switch1 assert switch1
assert switch2 assert switch2
assert len(switch1.links() + switch2.links()) == 1 assert len(session.link_manager.links()) == 1
def test_link_options( def test_link_options(
self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
@ -316,10 +296,6 @@ class TestXml:
# instantiate session # instantiate session
session.instantiate() session.instantiate()
# get ids for nodes
node1_id = node1.id
node2_id = switch.id
# save xml # save xml
xml_file = tmpdir.join("session.xml") xml_file = tmpdir.join("session.xml")
file_path = Path(xml_file.strpath) file_path = Path(xml_file.strpath)
@ -334,27 +310,25 @@ class TestXml:
# verify nodes have been removed from session # verify nodes have been removed from session
with pytest.raises(CoreError): with pytest.raises(CoreError):
assert not session.get_node(node1_id, CoreNode) assert not session.get_node(node1.id, CoreNode)
with pytest.raises(CoreError): with pytest.raises(CoreError):
assert not session.get_node(node2_id, SwitchNode) assert not session.get_node(switch.id, SwitchNode)
# load saved xml # load saved xml
session.open_xml(file_path, start=True) session.open_xml(file_path, start=True)
# verify nodes have been recreated # verify nodes have been recreated
assert session.get_node(node1_id, CoreNode) assert session.get_node(node1.id, CoreNode)
assert session.get_node(node2_id, SwitchNode) assert session.get_node(switch.id, SwitchNode)
links = [] assert len(session.link_manager.links()) == 1
for node_id in session.nodes: link = list(session.link_manager.links())[0]
node = session.nodes[node_id] link_options = link.options()
links += node.links() assert options.loss == link_options.loss
link = links[0] assert options.bandwidth == link_options.bandwidth
assert options.loss == link.options.loss assert options.jitter == link_options.jitter
assert options.bandwidth == link.options.bandwidth assert options.delay == link_options.delay
assert options.jitter == link.options.jitter assert options.dup == link_options.dup
assert options.delay == link.options.delay assert options.buffer == link_options.buffer
assert options.dup == link.options.dup
assert options.buffer == link.options.buffer
def test_link_options_ptp( def test_link_options_ptp(
self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
@ -385,10 +359,6 @@ class TestXml:
# instantiate session # instantiate session
session.instantiate() session.instantiate()
# get ids for nodes
node1_id = node1.id
node2_id = node2.id
# save xml # save xml
xml_file = tmpdir.join("session.xml") xml_file = tmpdir.join("session.xml")
file_path = Path(xml_file.strpath) file_path = Path(xml_file.strpath)
@ -403,27 +373,25 @@ class TestXml:
# verify nodes have been removed from session # verify nodes have been removed from session
with pytest.raises(CoreError): with pytest.raises(CoreError):
assert not session.get_node(node1_id, CoreNode) assert not session.get_node(node1.id, CoreNode)
with pytest.raises(CoreError): with pytest.raises(CoreError):
assert not session.get_node(node2_id, CoreNode) assert not session.get_node(node2.id, CoreNode)
# load saved xml # load saved xml
session.open_xml(file_path, start=True) session.open_xml(file_path, start=True)
# verify nodes have been recreated # verify nodes have been recreated
assert session.get_node(node1_id, CoreNode) assert session.get_node(node1.id, CoreNode)
assert session.get_node(node2_id, CoreNode) assert session.get_node(node2.id, CoreNode)
links = [] assert len(session.link_manager.links()) == 1
for node_id in session.nodes: link = list(session.link_manager.links())[0]
node = session.nodes[node_id] link_options = link.options()
links += node.links() assert options.loss == link_options.loss
link = links[0] assert options.bandwidth == link_options.bandwidth
assert options.loss == link.options.loss assert options.jitter == link_options.jitter
assert options.bandwidth == link.options.bandwidth assert options.delay == link_options.delay
assert options.jitter == link.options.jitter assert options.dup == link_options.dup
assert options.delay == link.options.delay assert options.buffer == link_options.buffer
assert options.dup == link.options.dup
assert options.buffer == link.options.buffer
def test_link_options_bidirectional( def test_link_options_bidirectional(
self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes
@ -450,7 +418,9 @@ class TestXml:
options1.dup = 5 options1.dup = 5
options1.jitter = 5 options1.jitter = 5
options1.buffer = 50 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 = LinkOptions()
options2.unidirectional = 1 options2.unidirectional = 1
options2.bandwidth = 10000 options2.bandwidth = 10000
@ -459,17 +429,11 @@ class TestXml:
options2.dup = 10 options2.dup = 10
options2.jitter = 10 options2.jitter = 10
options2.buffer = 100 options2.buffer = 100
session.update_link( session.update_link(node2.id, node1.id, iface2.id, iface1.id, options2)
node2.id, node1.id, iface2_data.id, iface1_data.id, options2
)
# instantiate session # instantiate session
session.instantiate() session.instantiate()
# get ids for nodes
node1_id = node1.id
node2_id = node2.id
# save xml # save xml
xml_file = tmpdir.join("session.xml") xml_file = tmpdir.join("session.xml")
file_path = Path(xml_file.strpath) file_path = Path(xml_file.strpath)
@ -484,32 +448,26 @@ class TestXml:
# verify nodes have been removed from session # verify nodes have been removed from session
with pytest.raises(CoreError): with pytest.raises(CoreError):
assert not session.get_node(node1_id, CoreNode) assert not session.get_node(node1.id, CoreNode)
with pytest.raises(CoreError): with pytest.raises(CoreError):
assert not session.get_node(node2_id, CoreNode) assert not session.get_node(node2.id, CoreNode)
# load saved xml # load saved xml
session.open_xml(file_path, start=True) session.open_xml(file_path, start=True)
# verify nodes have been recreated # verify nodes have been recreated
assert session.get_node(node1_id, CoreNode) assert session.get_node(node1.id, CoreNode)
assert session.get_node(node2_id, CoreNode) assert session.get_node(node2.id, CoreNode)
links = [] assert len(session.link_manager.links()) == 1
for node_id in session.nodes: assert options1.bandwidth == iface1.options.bandwidth
node = session.nodes[node_id] assert options1.delay == iface1.options.delay
links += node.links() assert options1.loss == iface1.options.loss
assert len(links) == 2 assert options1.dup == iface1.options.dup
link1 = links[0] assert options1.jitter == iface1.options.jitter
link2 = links[1] assert options1.buffer == iface1.options.buffer
assert options1.bandwidth == link1.options.bandwidth assert options2.bandwidth == iface2.options.bandwidth
assert options1.delay == link1.options.delay assert options2.delay == iface2.options.delay
assert options1.loss == link1.options.loss assert options2.loss == iface2.options.loss
assert options1.dup == link1.options.dup assert options2.dup == iface2.options.dup
assert options1.jitter == link1.options.jitter assert options2.jitter == iface2.options.jitter
assert options1.buffer == link1.options.buffer assert options2.buffer == iface2.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

View file

@ -341,19 +341,11 @@ EMANE Model Configuration:
```python ```python
from core import utils from core import utils
# emane network specific config # standardized way to retrieve an appropriate config id
session.emane.set_model_config(emane.id, EmaneIeee80211abgModel.name, { # iface id can be omitted, to allow a general configuration for a model, per node
"unicastrate": "3",
})
# node specific config
session.emane.set_model_config(node.id, EmaneIeee80211abgModel.name, {
"unicastrate": "3",
})
# node interface specific config
config_id = utils.iface_config_id(node.id, iface_id) 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", "unicastrate": "3",
}) })
``` ```