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