daemon: update how emane is started on nodes, fixing issue with multiple interfaces running emane, added test case to check on this in the future

This commit is contained in:
Blake Harnden 2020-09-09 10:27:06 -07:00
parent 82d87445b6
commit d981d88a6f
8 changed files with 227 additions and 127 deletions

View file

@ -80,7 +80,7 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
nem_name = emanexml.nem_file_name(iface)
shim_name = emanexml.shim_file_name(iface)
etree.SubElement(nem_element, "shim", definition=shim_name)
emanexml.create_iface_file(iface, nem_element, "nem", nem_name)
emanexml.create_node_file(iface.node, nem_element, "nem", nem_name)
# create and write shim document
shim_element = etree.Element(
@ -99,7 +99,7 @@ class EmaneCommEffectModel(emanemodel.EmaneModel):
ff = config["filterfile"]
if ff.strip() != "":
emanexml.add_param(shim_element, "filterfile", ff)
emanexml.create_iface_file(iface, shim_element, "shim", shim_name)
emanexml.create_node_file(iface.node, shim_element, "shim", shim_name)
# create transport xml
emanexml.create_transport_xml(iface, config)

View file

@ -6,6 +6,7 @@ import logging
import os
import threading
from collections import OrderedDict
from dataclasses import dataclass, field
from enum import Enum
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type
@ -28,7 +29,7 @@ from core.emulator.enumerations import (
RegisterTlvs,
)
from core.errors import CoreCommandError, CoreError
from core.nodes.base import CoreNode, NodeBase
from core.nodes.base import CoreNetworkBase, CoreNode, CoreNodeBase, NodeBase
from core.nodes.interface import CoreInterface, TunTap
from core.xml import emanexml
@ -68,6 +69,13 @@ class EmaneState(Enum):
NOT_READY = 2
@dataclass
class StartData:
emane_net: EmaneNet
node: CoreNodeBase
ifaces: List[CoreInterface] = field(default_factory=list)
class EmaneManager(ModelManager):
"""
EMANE controller object. Lives in a Session instance and is used for
@ -126,20 +134,15 @@ class EmaneManager(ModelManager):
self, emane_net: EmaneNet, iface: CoreInterface
) -> Dict[str, str]:
"""
Retrieve configuration for a given interface.
Retrieve configuration for a given interface, first checking for interface
specific config, node specific config, network specific config, and finally
falling back to the default configuration settings.
:param emane_net: emane network the interface is connected to
:param iface: interface running emane
:return: net, node, or interface model configuration
"""
model_name = emane_net.model.name
# don"t use default values when interface config is the same as net
# note here that using iface.node.id as key allows for only one type
# of each model per node;
# TODO: use both node and interface as key
# Adamson change: first check for iface config keyed by "node:iface.name"
# (so that nodes w/ multiple interfaces of same conftype can have
# different configs for each separate interface)
config = None
# try to retrieve interface specific configuration
if iface.node_id is not None:
@ -345,36 +348,45 @@ class EmaneManager(ModelManager):
self.buildeventservicexml()
with self._emane_node_lock:
logging.info("emane building xmls...")
for node_id in sorted(self._emane_nets):
emane_net = self._emane_nets[node_id]
if not emane_net.model:
logging.error("emane net(%s) has no model", emane_net.name)
continue
for iface in emane_net.get_ifaces():
self.start_iface(emane_net, iface)
start_data = self.get_start_data()
for data in start_data:
self.start_node(data)
if self.links_enabled():
self.link_monitor.start()
return EmaneState.SUCCESS
def start_iface(self, emane_net: EmaneNet, iface: CoreInterface) -> None:
if not iface.node:
logging.error(
"emane net(%s) connected interface(%s) missing node",
emane_net.name,
iface.name,
)
return
def get_start_data(self) -> List[StartData]:
node_map = {}
for node_id in sorted(self._emane_nets):
emane_net = self._emane_nets[node_id]
if not emane_net.model:
logging.error("emane net(%s) has no model", emane_net.name)
continue
for iface in emane_net.get_ifaces():
if not iface.node:
logging.error(
"emane net(%s) connected interface(%s) missing node",
emane_net.name,
iface.name,
)
continue
start_node = node_map.setdefault(
iface.node, StartData(emane_net, iface.node)
)
start_node.ifaces.append(iface)
start_nodes = sorted(node_map.values(), key=lambda x: x.node.id)
for start_node in start_nodes:
start_node.ifaces = sorted(start_node.ifaces, key=lambda x: x.node_id)
return start_nodes
def start_node(self, data: StartData) -> None:
control_net = self.session.add_remove_control_net(
0, remove=False, conf_required=False
)
nem_id = self.next_nem_id()
self.set_nem(nem_id, iface)
self.write_nem(iface, nem_id)
emanexml.build_platform_xml(self, control_net, emane_net, iface, nem_id)
config = self.get_iface_config(emane_net, iface)
emane_net.model.build_xml_files(config, iface)
self.start_daemon(iface)
self.install_iface(emane_net, iface)
emanexml.build_platform_xml(self, control_net, data)
self.start_daemon(data.node)
for iface in data.ifaces:
self.install_iface(data.emane_net, iface)
def set_nem(self, nem_id: int, iface: CoreInterface) -> None:
if nem_id in self.nems_to_ifaces:
@ -435,8 +447,21 @@ class EmaneManager(ModelManager):
logging.info("stopping EMANE daemons")
if self.links_enabled():
self.link_monitor.stop()
self.deinstall_ifaces()
self.stopdaemons()
# shutdown interfaces and stop daemons
kill_emaned = "killall -q emane"
start_data = self.get_start_data()
for data in start_data:
node = data.node
if not node.up:
continue
for iface in data.ifaces:
if isinstance(node, CoreNode):
iface.shutdown()
iface.poshook = None
if isinstance(node, CoreNode):
node.cmd(kill_emaned, wait=False)
else:
node.host_cmd(kill_emaned, wait=False)
self.stopeventmonitor()
def check_node_models(self) -> None:
@ -524,7 +549,7 @@ class EmaneManager(ModelManager):
)
)
def start_daemon(self, iface: CoreInterface) -> None:
def start_daemon(self, node: CoreNodeBase) -> None:
"""
Start one EMANE daemon per node having a radio.
Add a control network even if the user has not configured one.
@ -539,8 +564,7 @@ class EmaneManager(ModelManager):
emanecmd = f"emane -d -l {loglevel}"
if realtime:
emanecmd += " -r"
node = iface.node
if iface.is_virtual():
if isinstance(node, CoreNode):
otagroup, _otaport = self.get_config("otamanagergroup").split(":")
otadev = self.get_config("otamanagerdevice")
otanetidx = self.session.get_control_net_index(otadev)
@ -569,35 +593,19 @@ class EmaneManager(ModelManager):
if eventservicenetidx >= 0 and eventgroup != otagroup:
node.node_net_client.create_route(eventgroup, eventdev)
# start emane
log_file = os.path.join(node.nodedir, f"{iface.name}-emane.log")
platform_xml = os.path.join(node.nodedir, f"{iface.name}-platform.xml")
log_file = os.path.join(node.nodedir, f"{node.name}-emane.log")
platform_xml = os.path.join(node.nodedir, f"{node.name}-platform.xml")
args = f"{emanecmd} -f {log_file} {platform_xml}"
node.cmd(args)
logging.info("node(%s) emane daemon running: %s", node.name, args)
else:
path = self.session.session_dir
log_file = os.path.join(path, f"{iface.name}-emane.log")
platform_xml = os.path.join(path, f"{iface.name}-platform.xml")
log_file = os.path.join(path, f"{node.name}-emane.log")
platform_xml = os.path.join(path, f"{node.name}-platform.xml")
emanecmd += f" -f {log_file} {platform_xml}"
node.host_cmd(emanecmd, cwd=path)
logging.info("node(%s) host emane daemon running: %s", node.name, emanecmd)
def stopdaemons(self) -> None:
"""
Kill the appropriate EMANE daemons.
"""
kill_emaned = "killall -q emane"
for node_id in sorted(self._emane_nets):
emane_net = self._emane_nets[node_id]
for iface in emane_net.get_ifaces():
node = iface.node
if not node.up:
continue
if iface.is_raw():
node.host_cmd(kill_emaned, wait=False)
else:
node.cmd(kill_emaned, wait=False)
def install_iface(self, emane_net: EmaneNet, iface: CoreInterface) -> None:
config = self.get_iface_config(emane_net, iface)
external = config.get("external", "0")
@ -609,17 +617,6 @@ class EmaneManager(ModelManager):
iface.poshook = emane_net.setnemposition
iface.setposition()
def deinstall_ifaces(self) -> None:
"""
Uninstall TUN/TAP virtual interfaces.
"""
for key in sorted(self._emane_nets):
emane_net = self._emane_nets[key]
for iface in emane_net.get_ifaces():
if iface.is_virtual():
iface.shutdown()
iface.poshook = None
def doeventmonitor(self) -> bool:
"""
Returns boolean whether or not EMANE events will be monitored.
@ -783,6 +780,9 @@ class EmaneManager(ModelManager):
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,

View file

@ -11,6 +11,7 @@ EBTABLES: str = "ebtables"
MOUNT: str = "mount"
UMOUNT: str = "umount"
OVS_VSCTL: str = "ovs-vsctl"
TEST: str = "test"
COMMON_REQUIREMENTS: List[str] = [
BASH,
@ -21,6 +22,7 @@ COMMON_REQUIREMENTS: List[str] = [
SYSCTL,
TC,
UMOUNT,
TEST,
]
VCMD_REQUIREMENTS: List[str] = [VNODED, VCMD]
OVS_REQUIREMENTS: List[str] = [OVS_VSCTL]

View file

@ -16,7 +16,7 @@ from core.configservice.dependencies import ConfigServiceDependencies
from core.emulator.data import InterfaceData, LinkData, LinkOptions
from core.emulator.enumerations import LinkTypes, MessageFlags, NodeTypes
from core.errors import CoreCommandError, CoreError
from core.executables import MOUNT, VNODED
from core.executables import MOUNT, TEST, VNODED
from core.nodes.client import VnodeClient
from core.nodes.interface import CoreInterface, TunTap, Veth
from core.nodes.netclient import LinuxNetClient, get_net_client
@ -294,6 +294,16 @@ class CoreNodeBase(NodeBase):
"""
raise NotImplementedError
@abc.abstractmethod
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
"""
raise NotImplementedError
def add_config_service(self, service_class: "ConfigServiceType") -> None:
"""
Adds a configuration service to the node.
@ -602,6 +612,19 @@ class CoreNode(CoreNodeBase):
args = self.client.create_cmd(args, shell)
return self.server.remote_cmd(args, wait=wait)
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.cmd(f"{TEST} -e {path}")
return True
except CoreCommandError:
return False
def termcmdstring(self, sh: str = "/bin/sh") -> str:
"""
Create a terminal command string.

View file

@ -528,8 +528,9 @@ class TunTap(CoreInterface):
# check if this is an EMANE interface; if so, continue
# waiting if EMANE is still running
should_retry = count < 5
is_emane_running = self.node.session.emane.emanerunning(self.node)
if all([should_retry, self.net.is_emane, is_emane_running]):
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")

View file

@ -11,7 +11,7 @@ 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, UMOUNT
from core.executables import MOUNT, TEST, UMOUNT
from core.nodes.base import CoreNetworkBase, CoreNodeBase
from core.nodes.interface import CoreInterface
from core.nodes.network import CoreNetwork, GreTap
@ -55,6 +55,19 @@ class PhysicalNode(CoreNodeBase):
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.
@ -291,6 +304,19 @@ class Rj45Node(CoreNodeBase):
self.up = False
self.restorestate()
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 new_iface(
self, net: CoreNetworkBase, iface_data: InterfaceData
) -> CoreInterface:

View file

@ -7,15 +7,15 @@ from lxml import etree
from core import utils
from core.config import Configuration
from core.emane.nodes import EmaneNet
from core.emulator.distributed import DistributedServer
from core.errors import CoreError
from core.nodes.base import CoreNode, CoreNodeBase
from core.nodes.interface import CoreInterface
from core.nodes.network import CtrlNet
from core.xml import corexml
if TYPE_CHECKING:
from core.emane.emanemanager import EmaneManager
from core.emane.emanemanager import EmaneManager, StartData
from core.emane.emanemodel import EmaneModel
_MAC_PREFIX = "02:02"
@ -78,23 +78,22 @@ def create_file(
corexml.write_xml_file(xml_element, file_path, doctype=doctype)
def create_iface_file(
iface: CoreInterface, xml_element: etree.Element, doc_name: str, file_name: str
def create_node_file(
node: CoreNodeBase, xml_element: etree.Element, doc_name: str, file_name: str
) -> None:
"""
Create emane xml for an interface.
:param iface: interface running emane
:param node: node running emane
:param xml_element: root element to write to file
:param doc_name: name to use in the emane doctype
:param file_name: name of xml file
:return:
"""
node = iface.node
if iface.is_raw():
file_path = os.path.join(node.session.session_dir, file_name)
else:
if isinstance(node, CoreNode):
file_path = os.path.join(node.nodedir, file_name)
else:
file_path = os.path.join(node.session.session_dir, file_name)
create_file(xml_element, doc_name, file_path, node.server)
@ -143,11 +142,7 @@ def add_configurations(
def build_platform_xml(
emane_manager: "EmaneManager",
control_net: CtrlNet,
emane_net: EmaneNet,
iface: CoreInterface,
nem_id: int,
emane_manager: "EmaneManager", control_net: CtrlNet, data: "StartData"
) -> None:
"""
Create platform xml for a specific node.
@ -156,50 +151,62 @@ def build_platform_xml(
configurations
:param control_net: control net node for this emane
network
:param emane_net: emane network associated with interface
:param iface: interface running emane
:param nem_id: nem id to use for this interface
:param data: start data for a node connected to emane and associated interfaces
:return: the next nem id that can be used for creating platform xml files
"""
# build nem xml
nem_definition = nem_file_name(iface)
nem_element = etree.Element(
"nem", id=str(nem_id), name=iface.localname, definition=nem_definition
)
# check if this is an external transport, get default config if an interface
# specific one does not exist
config = emane_manager.get_iface_config(emane_net, iface)
if is_external(config):
nem_element.set("transport", "external")
platform_endpoint = "platformendpoint"
add_param(nem_element, platform_endpoint, config[platform_endpoint])
transport_endpoint = "transportendpoint"
add_param(nem_element, transport_endpoint, config[transport_endpoint])
else:
transport_name = transport_file_name(iface)
transport_element = etree.SubElement(
nem_element, "transport", definition=transport_name
)
add_param(transport_element, "device", iface.name)
# create top level platform element
transport_configs = {"otamanagerdevice", "eventservicedevice"}
platform_element = etree.Element("platform")
for configuration in emane_manager.emane_config.emulator_config:
name = configuration.id
if iface.is_raw() and name in transport_configs:
if not isinstance(data.node, CoreNode) and name in transport_configs:
value = control_net.brname
else:
value = emane_manager.get_config(name)
add_param(platform_element, name, value)
platform_element.append(nem_element)
mac = _MAC_PREFIX + ":00:00:"
mac += f"{(nem_id >> 8) & 0xFF:02X}:{nem_id & 0xFF:02X}"
iface.set_mac(mac)
# create nem xml entries for all interfaces
emane_net = data.emane_net
for iface in data.ifaces:
nem_id = emane_manager.next_nem_id()
emane_manager.set_nem(nem_id, iface)
emane_manager.write_nem(iface, nem_id)
config = emane_manager.get_iface_config(emane_net, iface)
emane_net.model.build_xml_files(config, iface)
# build nem xml
nem_definition = nem_file_name(iface)
nem_element = etree.Element(
"nem", id=str(nem_id), name=iface.localname, definition=nem_definition
)
# check if this is an external transport, get default config if an interface
# specific one does not exist
config = emane_manager.get_iface_config(emane_net, iface)
if is_external(config):
nem_element.set("transport", "external")
platform_endpoint = "platformendpoint"
add_param(nem_element, platform_endpoint, config[platform_endpoint])
transport_endpoint = "transportendpoint"
add_param(nem_element, transport_endpoint, config[transport_endpoint])
else:
transport_name = transport_file_name(iface)
transport_element = etree.SubElement(
nem_element, "transport", definition=transport_name
)
add_param(transport_element, "device", iface.name)
# add nem element to platform element
platform_element.append(nem_element)
# generate and assign interface mac address based on nem id
mac = _MAC_PREFIX + ":00:00:"
mac += f"{(nem_id >> 8) & 0xFF:02X}:{nem_id & 0xFF:02X}"
iface.set_mac(mac)
doc_name = "platform"
file_name = f"{iface.name}-platform.xml"
create_iface_file(iface, platform_element, doc_name, file_name)
file_name = f"{data.node.name}-platform.xml"
create_node_file(data.node, platform_element, doc_name, file_name)
def create_transport_xml(iface: CoreInterface, config: Dict[str, str]) -> None:
@ -220,16 +227,16 @@ def create_transport_xml(iface: CoreInterface, config: Dict[str, str]) -> None:
# get emane model cnfiguration
flowcontrol = config.get("flowcontrolenable", "0") == "1"
if iface.is_virtual():
if isinstance(iface.node, CoreNode):
device_path = "/dev/net/tun_flowctl"
if not os.path.exists(device_path):
if not iface.node.path_exists(device_path):
device_path = "/dev/net/tun"
add_param(transport_element, "devicepath", device_path)
if flowcontrol:
add_param(transport_element, "flowcontrolenable", "on")
doc_name = "transport"
transport_name = transport_file_name(iface)
create_iface_file(iface, transport_element, doc_name, transport_name)
create_node_file(iface.node, transport_element, doc_name, transport_name)
def create_phy_xml(
@ -250,7 +257,7 @@ def create_phy_xml(
phy_element, emane_model.phy_config, config, emane_model.config_ignore
)
file_name = phy_file_name(iface)
create_iface_file(iface, phy_element, "phy", file_name)
create_node_file(iface.node, phy_element, "phy", file_name)
def create_mac_xml(
@ -273,7 +280,7 @@ def create_mac_xml(
mac_element, emane_model.mac_config, config, emane_model.config_ignore
)
file_name = mac_file_name(iface)
create_iface_file(iface, mac_element, "mac", file_name)
create_node_file(iface.node, mac_element, "mac", file_name)
def create_nem_xml(
@ -298,7 +305,7 @@ def create_nem_xml(
phy_name = phy_file_name(iface)
etree.SubElement(nem_element, "phy", definition=phy_name)
nem_name = nem_file_name(iface)
create_iface_file(iface, nem_element, "nem", nem_name)
create_node_file(iface.node, nem_element, "nem", nem_name)
def create_event_service_xml(
@ -351,7 +358,7 @@ def nem_file_name(iface: CoreInterface) -> str:
:param iface: interface running emane
:return: nem xm file name
"""
append = "-raw" if iface.is_raw() else ""
append = "-raw" if not isinstance(iface.node, CoreNode) else ""
return f"{iface.name}-nem{append}.xml"

View file

@ -44,6 +44,47 @@ def ping(
class TestEmane:
def test_two_emane_interfaces(self, session: Session):
"""
Test nodes running multiple emane interfaces.
:param core.emulator.coreemu.EmuSession session: session for test
"""
# create emane node for networking the core nodes
session.set_location(47.57917, -122.13232, 2.00000, 1.0)
options = NodeOptions()
options.set_position(80, 50)
options.emane = EmaneIeee80211abgModel.name
emane_net1 = session.add_node(EmaneNet, options=options)
options.emane = EmaneRfPipeModel.name
emane_net2 = session.add_node(EmaneNet, options=options)
# create nodes
options = NodeOptions(model="mdr")
options.set_position(150, 150)
node1 = session.add_node(CoreNode, options=options)
options.set_position(300, 150)
node2 = session.add_node(CoreNode, options=options)
# create interfaces
ip_prefix1 = IpPrefixes("10.0.0.0/24")
ip_prefix2 = IpPrefixes("10.0.1.0/24")
for i, node in enumerate([node1, node2]):
node.setposition(x=150 * (i + 1), y=150)
iface_data = ip_prefix1.create_iface(node)
session.add_link(node.id, emane_net1.id, iface1_data=iface_data)
iface_data = ip_prefix2.create_iface(node)
session.add_link(node.id, emane_net2.id, iface1_data=iface_data)
# instantiate session
session.instantiate()
# ping node2 from node1 on both interfaces and check success
status = ping(node1, node2, ip_prefix1, count=5)
assert not status
status = ping(node1, node2, ip_prefix2, count=5)
assert not status
@pytest.mark.parametrize("model", _EMANE_MODELS)
def test_models(
self, session: Session, model: Type[EmaneModel], ip_prefixes: IpPrefixes