diff --git a/CHANGELOG.md b/CHANGELOG.md index cdc4bf440..30b5c711c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +## 2020-12-02 CORE 7.3.0 + +* core-daemon + * fixed issue where emane global configuration was not being sent to core-gui + * updated controlnet names on host to be prefixed with ctrl + * fixed RJ45 link shutdown from core-gui causing an error + * fixed emane external transport xml generation + * \#517 - update to account for radvd required directory + * \#514 - support added for session specific environment files + * \#529 - updated to configure netem limit based on delay or user specified, requires kernel 3.3+ +* core-pygui + * fixed issue drawing wlan/emane link options when it should not have + * edge labels are now placed a set distance from nodes like original gui + * link color/width are now saved to xml files + * added support to configure buffer size for links + * \#525 - added support for multiple wired links between the same nodes + * \#526 - added option to hide/show links with 100% loss +* Documentation + * \#527 - typo in service documentation + * \#515 - added examples to docs for using EMANE features within a CORE context + ## 2020-09-29 CORE 7.2.1 * core-daemon diff --git a/configure.ac b/configure.ac index 180558173..7b91b304c 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ # Process this file with autoconf to produce a configure script. # this defines the CORE version number, must be static for AC_INIT -AC_INIT(core, 7.2.1) +AC_INIT(core, 7.3.0) # autoconf and automake initialization AC_CONFIG_SRCDIR([netns/version.h.in]) diff --git a/daemon/core/api/grpc/grpcutils.py b/daemon/core/api/grpc/grpcutils.py index 7f25f1c16..145f7029c 100644 --- a/daemon/core/api/grpc/grpcutils.py +++ b/daemon/core/api/grpc/grpcutils.py @@ -128,6 +128,7 @@ def add_link_data( options.mer = options_proto.mer options.burst = options_proto.burst options.mburst = options_proto.mburst + options.buffer = options_proto.buffer options.unidirectional = options_proto.unidirectional options.key = options_proto.key return iface1_data, iface2_data, options, link_type @@ -329,6 +330,7 @@ def convert_link_options(options_data: LinkOptions) -> core_pb2.LinkOptions: burst=options_data.burst, delay=options_data.delay, dup=options_data.dup, + buffer=options_data.buffer, unidirectional=options_data.unidirectional, ) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 622124857..3159776a6 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -972,6 +972,7 @@ def EditLink( mburst=options_proto.mburst, unidirectional=options_proto.unidirectional, key=options_proto.key, + buffer=options_proto.buffer, ) session.update_link(node1_id, node2_id, iface1_id, iface2_id, options) iface1 = InterfaceData(id=iface1_id) diff --git a/daemon/core/api/grpc/wrappers.py b/daemon/core/api/grpc/wrappers.py index 2146198ad..8cc554464 100644 --- a/daemon/core/api/grpc/wrappers.py +++ b/daemon/core/api/grpc/wrappers.py @@ -457,6 +457,7 @@ class LinkOptions: delay: int = 0 dup: int = 0 unidirectional: bool = False + buffer: int = 0 @classmethod def from_proto(cls, proto: core_pb2.LinkOptions) -> "LinkOptions": @@ -471,6 +472,7 @@ def from_proto(cls, proto: core_pb2.LinkOptions) -> "LinkOptions": delay=proto.delay, dup=proto.dup, unidirectional=proto.unidirectional, + buffer=proto.buffer, ) def to_proto(self) -> core_pb2.LinkOptions: @@ -485,6 +487,7 @@ def to_proto(self) -> core_pb2.LinkOptions: delay=self.delay, dup=self.dup, unidirectional=self.unidirectional, + buffer=self.buffer, ) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index da37e3be1..b99b86308 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -52,6 +52,7 @@ from core.location.mobility import BasicRangeModel from core.nodes.base import CoreNode, CoreNodeBase, NodeBase from core.nodes.network import WlanNode +from core.nodes.physical import Rj45Node from core.services.coreservices import ServiceManager, ServiceShim @@ -787,20 +788,30 @@ def handle_link_message(self, message): options = LinkOptions() options.delay = message.get_tlv(LinkTlvs.DELAY.value) options.bandwidth = message.get_tlv(LinkTlvs.BANDWIDTH.value) - options.loss = message.get_tlv(LinkTlvs.LOSS.value) - options.dup = message.get_tlv(LinkTlvs.DUP.value) options.jitter = message.get_tlv(LinkTlvs.JITTER.value) options.mer = message.get_tlv(LinkTlvs.MER.value) options.burst = message.get_tlv(LinkTlvs.BURST.value) options.mburst = message.get_tlv(LinkTlvs.MBURST.value) options.unidirectional = message.get_tlv(LinkTlvs.UNIDIRECTIONAL.value) options.key = message.get_tlv(LinkTlvs.KEY.value) + loss = message.get_tlv(LinkTlvs.LOSS.value) + dup = message.get_tlv(LinkTlvs.DUP.value) + if loss is not None: + options.loss = float(loss) + if dup is not None: + options.dup = int(dup) if message.flags & MessageFlags.ADD.value: self.session.add_link( node1_id, node2_id, iface1_data, iface2_data, options, link_type ) elif message.flags & MessageFlags.DELETE.value: + node1 = self.session.get_node(node1_id, NodeBase) + node2 = self.session.get_node(node2_id, NodeBase) + if isinstance(node1, Rj45Node): + iface1_data.id = node1.iface_id + if isinstance(node2, Rj45Node): + iface2_data.id = node2.iface_id self.session.delete_link( node1_id, node2_id, iface1_data.id, iface2_data.id, link_type ) @@ -1851,7 +1862,15 @@ def send_objects(self): ) self.session.broadcast_config(config_data) - # send emane model info + # send global emane config + config = self.session.emane.get_configs() + logging.debug("global emane config: values(%s)", config) + config_data = ConfigShim.config_data( + 0, None, ConfigFlags.UPDATE.value, self.session.emane.emane_config, config + ) + self.session.broadcast_config(config_data) + + # send emane model configs for node_id in self.session.emane.nodes(): emane_configs = self.session.emane.get_all_configs(node_id) for model_name in emane_configs: diff --git a/daemon/core/configservices/frrservices/services.py b/daemon/core/configservices/frrservices/services.py index fa6f599a3..d6ebacf3c 100644 --- a/daemon/core/configservices/frrservices/services.py +++ b/daemon/core/configservices/frrservices/services.py @@ -5,7 +5,7 @@ from core.configservice.base import ConfigService, ConfigServiceMode from core.emane.nodes import EmaneNet from core.nodes.base import CoreNodeBase -from core.nodes.interface import CoreInterface +from core.nodes.interface import DEFAULT_MTU, CoreInterface from core.nodes.network import WlanNode GROUP: str = "FRR" @@ -18,7 +18,7 @@ def has_mtu_mismatch(iface: CoreInterface) -> bool: mtu-ignore command. This is needed when e.g. a node is linked via a GreTap device. """ - if iface.mtu != 1500: + if iface.mtu != DEFAULT_MTU: return True if not iface.net: return False diff --git a/daemon/core/configservices/quaggaservices/services.py b/daemon/core/configservices/quaggaservices/services.py index bf23e00c0..d3083ab64 100644 --- a/daemon/core/configservices/quaggaservices/services.py +++ b/daemon/core/configservices/quaggaservices/services.py @@ -6,7 +6,7 @@ from core.configservice.base import ConfigService, ConfigServiceMode from core.emane.nodes import EmaneNet from core.nodes.base import CoreNodeBase -from core.nodes.interface import CoreInterface +from core.nodes.interface import DEFAULT_MTU, CoreInterface from core.nodes.network import WlanNode GROUP: str = "Quagga" @@ -19,7 +19,7 @@ def has_mtu_mismatch(iface: CoreInterface) -> bool: mtu-ignore command. This is needed when e.g. a node is linked via a GreTap device. """ - if iface.mtu != 1500: + if iface.mtu != DEFAULT_MTU: return True if not iface.net: return False diff --git a/daemon/core/configservices/utilservices/services.py b/daemon/core/configservices/utilservices/services.py index 9b3369dbe..633da3336 100644 --- a/daemon/core/configservices/utilservices/services.py +++ b/daemon/core/configservices/utilservices/services.py @@ -217,7 +217,7 @@ def data(self) -> Dict[str, Any]: class RadvdService(ConfigService): name: str = "radvd" group: str = GROUP_NAME - directories: List[str] = ["/etc/radvd"] + directories: List[str] = ["/etc/radvd", "/var/run/radvd"] files: List[str] = ["/etc/radvd/radvd.conf"] executables: List[str] = ["radvd"] dependencies: List[str] = [] diff --git a/daemon/core/emulator/data.py b/daemon/core/emulator/data.py index 15d922a9e..68a92eea7 100644 --- a/daemon/core/emulator/data.py +++ b/daemon/core/emulator/data.py @@ -172,6 +172,7 @@ class LinkOptions: mburst: int = None unidirectional: int = None key: int = None + buffer: int = None @dataclass diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 64276dccf..6d583860b 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -12,6 +12,7 @@ import tempfile import threading import time +from pathlib import Path from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, TypeVar from core import constants, utils @@ -997,28 +998,25 @@ def get_environment(self, state: bool = True) -> Dict[str, str]: env["SESSION_USER"] = str(self.user) if state: env["SESSION_STATE"] = str(self.state) - # attempt to read and add environment config file - environment_config_file = os.path.join(constants.CORE_CONF_DIR, "environment") - try: - if os.path.isfile(environment_config_file): - utils.load_config(environment_config_file, env) - except IOError: - logging.warning( - "environment configuration file does not exist: %s", - environment_config_file, - ) - # attempt to read and add user environment file + # try reading and merging optional environments from: + # /etc/core/environment + # /home/user/.core/environment + # /tmp/pycore./environment + core_env_path = Path(constants.CORE_CONF_DIR) / "environment" + session_env_path = Path(self.session_dir) / "environment" if self.user: - environment_user_file = os.path.join( - "/home", self.user, ".core", "environment" - ) - try: - utils.load_config(environment_user_file, env) - except IOError: - logging.debug( - "user core environment settings file not present: %s", - environment_user_file, - ) + user_home_path = Path(f"~{self.user}").expanduser() + user_env1 = user_home_path / ".core" / "environment" + user_env2 = user_home_path / ".coregui" / "environment" + paths = [core_env_path, user_env1, user_env2, session_env_path] + else: + paths = [core_env_path, session_env_path] + for path in paths: + if path.is_file(): + try: + utils.load_config(path, env) + except IOError: + logging.exception("error reading environment file: %s", path) return env def set_thumbnail(self, thumb_file: str) -> None: @@ -1447,12 +1445,14 @@ def add_remove_control_net( ) control_net = self.create_node( CtrlNet, - True, - prefix, + start=False, + prefix=prefix, _id=_id, updown_script=updown_script, serverintf=server_iface, ) + control_net.brname = f"ctrl{net_index}.{self.short_session_id()}" + control_net.startup() return control_net def add_remove_control_iface( diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index f4f8b79a3..114aa5b29 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -87,14 +87,14 @@ def __init__(self, app: "Application", proxy: bool) -> None: self.read_config() # helpers - self.iface_to_edge: Dict[Tuple[int, ...], Tuple[int, ...]] = {} + self.iface_to_edge: Dict[Tuple[int, ...], CanvasEdge] = {} self.ifaces_manager: InterfaceManager = InterfaceManager(self.app) self.observer: Optional[str] = None # session data self.mobility_players: Dict[int, MobilityPlayer] = {} self.canvas_nodes: Dict[int, CanvasNode] = {} - self.links: Dict[Tuple[int, int], CanvasEdge] = {} + self.links: Dict[str, CanvasEdge] = {} self.handling_throughputs: Optional[grpc.Future] = None self.handling_cpu_usage: Optional[grpc.Future] = None self.handling_events: Optional[grpc.Future] = None @@ -225,11 +225,9 @@ def handle_link_event(self, event: LinkEvent) -> None: self.app.canvas.add_wired_edge(canvas_node1, canvas_node2, event.link) self.app.canvas.organize() elif event.message_type == MessageType.DELETE: - self.app.canvas.delete_wired_edge(canvas_node1, canvas_node2) + self.app.canvas.delete_wired_edge(event.link) elif event.message_type == MessageType.NONE: - self.app.canvas.update_wired_edge( - canvas_node1, canvas_node2, event.link - ) + self.app.canvas.update_wired_edge(event.link) else: logging.warning("unknown link event: %s", event) @@ -383,6 +381,17 @@ def parse_metadata(self) -> None: except ValueError: logging.exception("unknown shape: %s", shape_type) + # load edges config + edges_config = config.get("edges") + if edges_config: + edges_config = json.loads(edges_config) + logging.info("edges config: %s", edges_config) + for edge_config in edges_config: + edge = self.links[edge_config["token"]] + edge.width = edge_config["width"] + edge.color = edge_config["color"] + edge.redraw() + def create_new_session(self) -> None: """ Create a new session @@ -570,7 +579,15 @@ def set_metadata(self) -> None: shapes.append(shape.metadata()) shapes = json.dumps(shapes) - metadata = {"canvas": canvas_config, "shapes": shapes} + # create edges config + edges_config = [] + for edge in self.links.values(): + edge_config = dict(token=edge.token, width=edge.width, color=edge.color) + edges_config.append(edge_config) + edges_config = json.dumps(edges_config) + + # save metadata + metadata = dict(canvas=canvas_config, shapes=shapes, edges=edges_config) response = self.client.set_session_metadata(self.session.id, metadata) logging.debug("set session metadata %s, result: %s", metadata, response) @@ -877,7 +894,7 @@ def create_iface(self, canvas_node: CanvasNode) -> Interface: def create_link( self, edge: CanvasEdge, canvas_src_node: CanvasNode, canvas_dst_node: CanvasNode - ) -> None: + ) -> Link: """ Create core link for a pair of canvas nodes, with token referencing the canvas edge. @@ -888,15 +905,9 @@ def create_link( src_iface = None if NodeUtils.is_container_node(src_node.type): src_iface = self.create_iface(canvas_src_node) - self.iface_to_edge[(src_node.id, src_iface.id)] = edge.token - edge.src_iface = src_iface - canvas_src_node.ifaces[src_iface.id] = src_iface dst_iface = None if NodeUtils.is_container_node(dst_node.type): dst_iface = self.create_iface(canvas_dst_node) - self.iface_to_edge[(dst_node.id, dst_iface.id)] = edge.token - edge.dst_iface = dst_iface - canvas_dst_node.ifaces[dst_iface.id] = dst_iface link = Link( type=LinkType.WIRED, node1_id=src_node.id, @@ -904,9 +915,21 @@ def create_link( iface1=src_iface, iface2=dst_iface, ) - edge.set_link(link) - self.links[edge.token] = edge logging.info("added link between %s and %s", src_node.name, dst_node.name) + return link + + def save_edge( + self, edge: CanvasEdge, canvas_src_node: CanvasNode, canvas_dst_node: CanvasNode + ) -> None: + self.links[edge.token] = edge + src_node = canvas_src_node.core_node + dst_node = canvas_dst_node.core_node + if NodeUtils.is_container_node(src_node.type): + src_iface_id = edge.link.iface1.id + self.iface_to_edge[(src_node.id, src_iface_id)] = edge + if NodeUtils.is_container_node(dst_node.type): + dst_iface_id = edge.link.iface2.id + self.iface_to_edge[(dst_node.id, dst_iface_id)] = edge def get_wlan_configs_proto(self) -> List[wlan_pb2.WlanConfig]: configs = [] diff --git a/daemon/core/gui/data/xmls/emane-demo-antenna.xml b/daemon/core/gui/data/xmls/emane-demo-antenna.xml new file mode 100644 index 000000000..935fd97a8 --- /dev/null +++ b/daemon/core/gui/data/xmls/emane-demo-antenna.xml @@ -0,0 +1,340 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/daemon/core/gui/data/xmls/emane-demo-eel.xml b/daemon/core/gui/data/xmls/emane-demo-eel.xml new file mode 100644 index 000000000..4162458c7 --- /dev/null +++ b/daemon/core/gui/data/xmls/emane-demo-eel.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/daemon/core/gui/data/xmls/emane-demo-files.xml b/daemon/core/gui/data/xmls/emane-demo-files.xml new file mode 100644 index 000000000..da6f9c705 --- /dev/null +++ b/daemon/core/gui/data/xmls/emane-demo-files.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/daemon/core/gui/data/xmls/emane-demo-gpsd.xml b/daemon/core/gui/data/xmls/emane-demo-gpsd.xml new file mode 100644 index 000000000..06bc54dca --- /dev/null +++ b/daemon/core/gui/data/xmls/emane-demo-gpsd.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/daemon/core/gui/data/xmls/emane-demo-precomputed.xml b/daemon/core/gui/data/xmls/emane-demo-precomputed.xml new file mode 100644 index 000000000..a19acba67 --- /dev/null +++ b/daemon/core/gui/data/xmls/emane-demo-precomputed.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/daemon/core/gui/dialogs/linkconfig.py b/daemon/core/gui/dialogs/linkconfig.py index 82bf92e19..6cb228628 100644 --- a/daemon/core/gui/dialogs/linkconfig.py +++ b/daemon/core/gui/dialogs/linkconfig.py @@ -49,16 +49,18 @@ def __init__(self, app: "Application", edge: "CanvasEdge") -> None: self.jitter: tk.StringVar = tk.StringVar() self.loss: tk.StringVar = tk.StringVar() self.duplicate: tk.StringVar = tk.StringVar() + self.buffer: tk.StringVar = tk.StringVar() self.down_bandwidth: tk.StringVar = tk.StringVar() self.down_delay: tk.StringVar = tk.StringVar() self.down_jitter: tk.StringVar = tk.StringVar() self.down_loss: tk.StringVar = tk.StringVar() self.down_duplicate: tk.StringVar = tk.StringVar() + self.down_buffer: tk.StringVar = tk.StringVar() - self.color: tk.StringVar = tk.StringVar(value="#000000") + self.color: tk.StringVar = tk.StringVar(value=self.edge.color) self.color_button: Optional[tk.Button] = None - self.width: tk.DoubleVar = tk.DoubleVar() + self.width: tk.DoubleVar = tk.DoubleVar(value=self.edge.width) self.load_link_config() self.symmetric_frame: Optional[ttk.Frame] = None @@ -68,10 +70,14 @@ def __init__(self, app: "Application", edge: "CanvasEdge") -> None: def draw(self) -> None: self.top.columnconfigure(0, weight=1) - source_name = self.app.canvas.nodes[self.edge.src].core_node.name - dest_name = self.app.canvas.nodes[self.edge.dst].core_node.name + src_label = self.app.canvas.nodes[self.edge.src].core_node.name + if self.edge.link.iface1: + src_label += f":{self.edge.link.iface1.name}" + dst_label = self.app.canvas.nodes[self.edge.dst].core_node.name + if self.edge.link.iface2: + dst_label += f":{self.edge.link.iface2.name}" label = ttk.Label( - self.top, text=f"Link from {source_name} to {dest_name}", anchor=tk.CENTER + self.top, text=f"{src_label} to {dst_label}", anchor=tk.CENTER ) label.grid(row=0, column=0, sticky=tk.EW, pady=PADY) @@ -183,6 +189,19 @@ def get_frame(self) -> ttk.Frame: entry.grid(row=row, column=2, sticky=tk.EW, pady=PADY) row = row + 1 + label = ttk.Label(frame, text="Buffer (Packets)") + label.grid(row=row, column=0, sticky=tk.EW) + entry = validation.PositiveIntEntry( + frame, empty_enabled=False, textvariable=self.buffer + ) + entry.grid(row=row, column=1, sticky=tk.EW, pady=PADY) + if not self.is_symmetric: + entry = validation.PositiveIntEntry( + frame, empty_enabled=False, textvariable=self.down_buffer + ) + entry.grid(row=row, column=2, sticky=tk.EW, pady=PADY) + row = row + 1 + label = ttk.Label(frame, text="Color") label.grid(row=row, column=0, sticky=tk.EW) self.color_button = tk.Button( @@ -213,16 +232,22 @@ def click_color(self) -> None: self.color_button.config(background=color) def click_apply(self) -> None: - self.app.canvas.itemconfigure(self.edge.id, width=self.width.get()) - self.app.canvas.itemconfigure(self.edge.id, fill=self.color.get()) + self.edge.width = self.width.get() + self.edge.color = self.color.get() link = self.edge.link bandwidth = get_int(self.bandwidth) jitter = get_int(self.jitter) delay = get_int(self.delay) duplicate = get_int(self.duplicate) + buffer = get_int(self.buffer) loss = get_float(self.loss) options = LinkOptions( - bandwidth=bandwidth, jitter=jitter, delay=delay, dup=duplicate, loss=loss + bandwidth=bandwidth, + jitter=jitter, + delay=delay, + dup=duplicate, + loss=loss, + buffer=buffer, ) link.options = options iface1_id = link.iface1.id if link.iface1 else None @@ -239,6 +264,7 @@ def click_apply(self) -> None: down_jitter = get_int(self.down_jitter) down_delay = get_int(self.down_delay) down_duplicate = get_int(self.down_duplicate) + down_buffer = get_int(self.down_buffer) down_loss = get_float(self.down_loss) options = LinkOptions( bandwidth=down_bandwidth, @@ -246,6 +272,7 @@ def click_apply(self) -> None: delay=down_delay, dup=down_duplicate, loss=down_loss, + buffer=down_buffer, unidirectional=True, ) self.edge.asymmetric_link = Link( @@ -265,7 +292,8 @@ def click_apply(self) -> None: self.app.core.edit_link(self.edge.asymmetric_link) # update edge label - self.edge.draw_link_options() + self.edge.redraw() + self.edge.check_options() self.destroy() def change_symmetry(self) -> None: @@ -299,6 +327,7 @@ def load_link_config(self) -> None: self.duplicate.set(str(link.options.dup)) self.loss.set(str(link.options.loss)) self.delay.set(str(link.options.delay)) + self.buffer.set(str(link.options.buffer)) if not self.is_symmetric: asym_link = self.edge.asymmetric_link self.down_bandwidth.set(str(asym_link.options.bandwidth)) @@ -306,3 +335,4 @@ def load_link_config(self) -> None: self.down_duplicate.set(str(asym_link.options.dup)) self.down_loss.set(str(asym_link.options.loss)) self.down_delay.set(str(asym_link.options.delay)) + self.down_buffer.set(str(asym_link.options.buffer)) diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index f9d018244..9829cbd49 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -14,19 +14,23 @@ if TYPE_CHECKING: from core.gui.graph.graph import CanvasGraph -TEXT_DISTANCE: float = 0.30 +TEXT_DISTANCE: int = 60 EDGE_WIDTH: int = 3 EDGE_COLOR: str = "#ff0000" +EDGE_LOSS: float = 100.0 WIRELESS_WIDTH: float = 3 WIRELESS_COLOR: str = "#009933" ARC_DISTANCE: int = 50 -def create_edge_token(src: int, dst: int, network: int = None) -> Tuple[int, ...]: - values = [src, dst] - if network is not None: - values.append(network) - return tuple(sorted(values)) +def create_wireless_token(src: int, dst: int, network: int) -> str: + return f"{src}-{dst}-{network}" + + +def create_edge_token(link: Link) -> str: + iface1_id = link.iface1.id if link.iface1 else None + iface2_id = link.iface2.id if link.iface2 else None + return f"{link.node1_id}-{iface1_id}-{link.node2_id}-{iface2_id}" def arc_edges(edges) -> None: @@ -67,17 +71,13 @@ def __init__(self, canvas: "CanvasGraph", src: int, dst: int = None) -> None: self.src: int = src self.dst: int = dst self.arc: int = 0 - self.token: Optional[Tuple[int, ...]] = None + self.token: Optional[str] = None self.src_label: Optional[int] = None self.middle_label: Optional[int] = None self.dst_label: Optional[int] = None self.color: str = EDGE_COLOR self.width: int = EDGE_WIDTH - @classmethod - def create_token(cls, src: int, dst: int) -> Tuple[int, ...]: - return tuple(sorted([src, dst])) - def scaled_width(self) -> float: return self.width * self.canvas.app.app_scale @@ -156,15 +156,17 @@ def clear_middle_label(self) -> None: def node_label_positions(self) -> Tuple[Tuple[float, float], Tuple[float, float]]: src_x, src_y, _, _, dst_x, dst_y = self.canvas.coords(self.id) - v1 = dst_x - src_x - v2 = dst_y - src_y - ux = TEXT_DISTANCE * v1 - uy = TEXT_DISTANCE * v2 - src_x = src_x + ux - src_y = src_y + uy - dst_x = dst_x - ux - dst_y = dst_y - uy - return (src_x, src_y), (dst_x, dst_y) + v_x, v_y = dst_x - src_x, dst_y - src_y + v_len = math.sqrt(v_x ** 2 + v_y ** 2) + if v_len == 0: + u_x, u_y = 0.0, 0.0 + else: + u_x, u_y = v_x / v_len, v_y / v_len + offset_x, offset_y = TEXT_DISTANCE * u_x, TEXT_DISTANCE * u_y + return ( + (src_x + offset_x, src_y + offset_y), + (dst_x - offset_x, dst_y - offset_y), + ) def src_label_text(self, text: str) -> None: if self.src_label is None: @@ -240,15 +242,17 @@ def __init__( canvas: "CanvasGraph", src: int, dst: int, + network_id: int, + token: str, src_pos: Tuple[float, float], dst_pos: Tuple[float, float], - token: Tuple[int, ...], link: Link, ) -> None: logging.debug("drawing wireless link from node %s to node %s", src, dst) super().__init__(canvas, src, dst) + self.network_id: int = network_id self.link: Link = link - self.token: Tuple[int, ...] = token + self.token: str = token self.width: float = WIRELESS_WIDTH color = link.color if link.color else WIRELESS_COLOR self.color: str = color @@ -282,11 +286,10 @@ def __init__( Create an instance of canvas edge object """ super().__init__(canvas, src) - self.src_iface: Optional[Interface] = None - self.dst_iface: Optional[Interface] = None self.text_src: Optional[int] = None self.text_dst: Optional[int] = None self.link: Optional[Link] = None + self.linked_wireless: bool = False self.asymmetric_link: Optional[Link] = None self.throughput: Optional[float] = None self.draw(src_pos, dst_pos, tk.NORMAL) @@ -303,10 +306,6 @@ def set_binding(self) -> None: self.canvas.tag_bind(self.id, "", self.show_context) self.canvas.tag_bind(self.id, "", self.show_info) - def set_link(self, link: Link) -> None: - self.link = link - self.draw_labels() - def iface_label(self, iface: Interface) -> str: label = "" if iface.name and self.canvas.show_iface_names.get(): @@ -332,12 +331,25 @@ def draw_labels(self) -> None: src_text, dst_text = self.create_node_labels() self.src_label_text(src_text) self.dst_label_text(dst_text) - self.draw_link_options() + if not self.linked_wireless: + self.draw_link_options() def redraw(self) -> None: super().redraw() self.draw_labels() + def check_options(self) -> None: + if not self.link.options: + return + if self.link.options.loss == EDGE_LOSS: + state = tk.HIDDEN + self.canvas.addtag_withtag(tags.LOSS_EDGES, self.id) + else: + state = tk.NORMAL + self.canvas.dtag(self.id, tags.LOSS_EDGES) + if self.canvas.show_loss_links.state() == tk.HIDDEN: + self.canvas.itemconfigure(self.id, state=state) + def set_throughput(self, throughput: float) -> None: throughput = 0.001 * throughput text = f"{throughput:.3f} kbps" @@ -350,36 +362,21 @@ def set_throughput(self, throughput: float) -> None: width = self.scaled_width() self.canvas.itemconfig(self.id, fill=color, width=width) - def complete(self, dst: int) -> None: + def clear_throughput(self) -> None: + self.clear_middle_label() + if not self.linked_wireless: + self.draw_link_options() + + def complete(self, dst: int, linked_wireless: bool) -> None: self.dst = dst - self.token = create_edge_token(self.src, self.dst) + self.linked_wireless = linked_wireless dst_pos = self.canvas.coords(self.dst) self.move_dst(dst_pos) self.check_wireless() - logging.debug("Draw wired link from node %s to node %s", self.src, dst) - - def is_wireless(self) -> bool: - src_node = self.canvas.nodes[self.src] - dst_node = self.canvas.nodes[self.dst] - src_node_type = src_node.core_node.type - dst_node_type = dst_node.core_node.type - is_src_wireless = NodeUtils.is_wireless_node(src_node_type) - is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type) - - # update the wlan/EMANE network - wlan_network = self.canvas.wireless_network - if is_src_wireless and not is_dst_wireless: - if self.src not in wlan_network: - wlan_network[self.src] = set() - wlan_network[self.src].add(self.dst) - elif not is_src_wireless and is_dst_wireless: - if self.dst not in wlan_network: - wlan_network[self.dst] = set() - wlan_network[self.dst].add(self.src) - return is_src_wireless or is_dst_wireless + logging.debug("draw wired link from node %s to node %s", self.src, dst) def check_wireless(self) -> None: - if self.is_wireless(): + if self.linked_wireless: self.canvas.itemconfig(self.id, state=tk.HIDDEN) self.canvas.dtag(self.id, tags.EDGE) self._check_antenna() diff --git a/daemon/core/gui/graph/graph.py b/daemon/core/gui/graph/graph.py index 1021e305d..9862d2c88 100644 --- a/daemon/core/gui/graph/graph.py +++ b/daemon/core/gui/graph/graph.py @@ -21,8 +21,10 @@ EDGE_WIDTH, CanvasEdge, CanvasWirelessEdge, + Edge, arc_edges, create_edge_token, + create_wireless_token, ) from core.gui.graph.enums import GraphMode, ScaleOption from core.gui.graph.node import CanvasNode @@ -69,9 +71,9 @@ def __init__( self.selected: Optional[int] = None self.node_draw: Optional[NodeDraw] = None self.nodes: Dict[int, CanvasNode] = {} - self.edges: Dict[int, CanvasEdge] = {} + self.edges: Dict[str, CanvasEdge] = {} self.shapes: Dict[int, Shape] = {} - self.wireless_edges: Dict[Tuple[int, ...], CanvasWirelessEdge] = {} + self.wireless_edges: Dict[str, CanvasWirelessEdge] = {} # map wireless/EMANE node to the set of MDRs connected to that node self.wireless_network: Dict[int, Set[int]] = {} @@ -108,6 +110,7 @@ def __init__( self.show_wireless: ShowVar = ShowVar(self, tags.WIRELESS_EDGE, value=True) self.show_grid: ShowVar = ShowVar(self, tags.GRIDLINE, value=True) self.show_annotations: ShowVar = ShowVar(self, tags.ANNOTATION, value=True) + self.show_loss_links: ShowVar = ShowVar(self, tags.LOSS_EDGES, value=True) self.show_iface_names: BooleanVar = BooleanVar(value=False) self.show_ip4s: BooleanVar = BooleanVar(value=True) self.show_ip6s: BooleanVar = BooleanVar(value=True) @@ -145,6 +148,7 @@ def reset_and_redraw(self, session: Session) -> None: self.show_iface_names.set(False) self.show_ip4s.set(True) self.show_ip6s.set(True) + self.show_loss_links.set(True) # delete any existing drawn items for tag in tags.RESET_TAGS: @@ -206,14 +210,9 @@ def set_throughputs(self, throughputs_event: ThroughputsEvent) -> None: iface_id = iface_throughput.iface_id throughput = iface_throughput.throughput iface_to_edge_id = (node_id, iface_id) - token = self.core.iface_to_edge.get(iface_to_edge_id) - if not token: - continue - edge = self.edges.get(token) + edge = self.core.iface_to_edge.get(iface_to_edge_id) if edge: edge.set_throughput(throughput) - else: - del self.core.iface_to_edge[iface_to_edge_id] def draw_grid(self) -> None: """ @@ -230,7 +229,7 @@ def draw_grid(self) -> None: self.tag_lower(self.rect) def add_wired_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None: - token = create_edge_token(src.id, dst.id) + token = create_edge_token(link) if token in self.edges and link.options.unidirectional: edge = self.edges[token] edge.asymmetric_link = link @@ -240,71 +239,52 @@ def add_wired_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None: src_pos = (node1.position.x, node1.position.y) dst_pos = (node2.position.x, node2.position.y) edge = CanvasEdge(self, src.id, src_pos, dst_pos) - edge.token = token - edge.dst = dst.id - edge.set_link(link) - edge.check_wireless() - src.edges.add(edge) - dst.edges.add(edge) - self.edges[edge.token] = edge - self.core.links[edge.token] = edge - if link.iface1: - iface1 = link.iface1 - self.core.iface_to_edge[(node1.id, iface1.id)] = token - src.ifaces[iface1.id] = iface1 - edge.src_iface = iface1 - if link.iface2: - iface2 = link.iface2 - self.core.iface_to_edge[(node2.id, iface2.id)] = edge.token - dst.ifaces[iface2.id] = iface2 - edge.dst_iface = iface2 - - def delete_wired_edge(self, src: CanvasNode, dst: CanvasNode) -> None: - token = create_edge_token(src.id, dst.id) + self.complete_edge(src, dst, edge, link) + + def delete_wired_edge(self, link: Link) -> None: + token = create_edge_token(link) edge = self.edges.get(token) - if not edge: - return - self.delete_edge(edge) + if edge: + self.delete_edge(edge) - def update_wired_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None: - token = create_edge_token(src.id, dst.id) + def update_wired_edge(self, link: Link) -> None: + token = create_edge_token(link) edge = self.edges.get(token) - if not edge: - return - edge.link.options = deepcopy(link.options) + if edge: + edge.link.options = deepcopy(link.options) + edge.draw_link_options() + edge.check_options() def add_wireless_edge(self, src: CanvasNode, dst: CanvasNode, link: Link) -> None: network_id = link.network_id if link.network_id else None - token = create_edge_token(src.id, dst.id, network_id) + token = create_wireless_token(src.id, dst.id, network_id) if token in self.wireless_edges: logging.warning("ignoring link that already exists: %s", link) return src_pos = self.coords(src.id) dst_pos = self.coords(dst.id) - edge = CanvasWirelessEdge(self, src.id, dst.id, src_pos, dst_pos, token, link) + edge = CanvasWirelessEdge( + self, src.id, dst.id, network_id, token, src_pos, dst_pos, link + ) self.wireless_edges[token] = edge src.wireless_edges.add(edge) dst.wireless_edges.add(edge) self.tag_raise(src.id) self.tag_raise(dst.id) - # update arcs when there are multiple links - common_edges = list(src.wireless_edges & dst.wireless_edges) - arc_edges(common_edges) + self.arc_common_edges(edge) def delete_wireless_edge( self, src: CanvasNode, dst: CanvasNode, link: Link ) -> None: network_id = link.network_id if link.network_id else None - token = create_edge_token(src.id, dst.id, network_id) + token = create_wireless_token(src.id, dst.id, network_id) if token not in self.wireless_edges: return edge = self.wireless_edges.pop(token) edge.delete() src.wireless_edges.remove(edge) dst.wireless_edges.remove(edge) - # update arcs when there are multiple links - common_edges = list(src.wireless_edges & dst.wireless_edges) - arc_edges(common_edges) + self.arc_common_edges(edge) def update_wireless_edge( self, src: CanvasNode, dst: CanvasNode, link: Link @@ -312,7 +292,7 @@ def update_wireless_edge( if not link.label: return network_id = link.network_id if link.network_id else None - token = create_edge_token(src.id, dst.id, network_id) + token = create_wireless_token(src.id, dst.id, network_id) if token not in self.wireless_edges: self.add_wireless_edge(src, dst, link) else: @@ -453,12 +433,6 @@ def handle_edge_release(self, _event: tk.Event) -> None: edge.delete() return - # ignore repeated edges - token = create_edge_token(edge.src, self.selected) - if token in self.edges: - edge.delete() - return - # rj45 nodes can only support one link if NodeUtils.is_rj45_node(src_node.core_node.type) and src_node.edges: edge.delete() @@ -467,13 +441,23 @@ def handle_edge_release(self, _event: tk.Event) -> None: edge.delete() return - # set dst node and snap edge to center - edge.complete(self.selected) + # only 1 link between bridge based nodes + is_src_bridge = NodeUtils.is_bridge_node(src_node.core_node) + is_dst_bridge = NodeUtils.is_bridge_node(dst_node.core_node) + common_links = src_node.edges & dst_node.edges + if all([is_src_bridge, is_dst_bridge, common_links]): + edge.delete() + return - self.edges[edge.token] = edge - src_node.edges.add(edge) - dst_node.edges.add(edge) - self.core.create_link(edge, src_node, dst_node) + # finalize edge creation + self.complete_edge(src_node, dst_node, edge) + + def arc_common_edges(self, edge: Edge) -> None: + src_node = self.nodes[edge.src] + dst_node = self.nodes[edge.dst] + common_edges = list(src_node.edges & dst_node.edges) + common_edges += list(src_node.wireless_edges & dst_node.wireless_edges) + arc_edges(common_edges) def select_object(self, object_id: int, choose_multiple: bool = False) -> None: """ @@ -532,10 +516,10 @@ def delete_selected_objects(self) -> None: edge.delete() # update node connected to edge being deleted other_id = edge.src - other_iface = edge.src_iface + other_iface = edge.link.iface1 if edge.src == object_id: other_id = edge.dst - other_iface = edge.dst_iface + other_iface = edge.link.iface2 other_node = self.nodes[other_id] other_node.edges.remove(edge) if other_iface: @@ -557,12 +541,12 @@ def delete_edge(self, edge: CanvasEdge) -> None: del self.edges[edge.token] src_node = self.nodes[edge.src] src_node.edges.discard(edge) - if edge.src_iface: - del src_node.ifaces[edge.src_iface.id] + if edge.link.iface1: + del src_node.ifaces[edge.link.iface1.id] dst_node = self.nodes[edge.dst] dst_node.edges.discard(edge) - if edge.dst_iface: - del dst_node.ifaces[edge.dst_iface.id] + if edge.link.iface2: + del dst_node.ifaces[edge.link.iface2.id] src_wireless = NodeUtils.is_wireless_node(src_node.core_node.type) if src_wireless: dst_node.delete_antenna() @@ -570,6 +554,7 @@ def delete_edge(self, edge: CanvasEdge) -> None: if dst_wireless: src_node.delete_antenna() self.core.deleted_canvas_edges([edge]) + self.arc_common_edges(edge) def zoom(self, event: tk.Event, factor: float = None) -> None: if not factor: @@ -901,19 +886,41 @@ def set_wallpaper(self, filename: Optional[str]) -> None: def is_selection_mode(self) -> bool: return self.mode == GraphMode.SELECT - def create_edge(self, source: CanvasNode, dest: CanvasNode) -> None: + def create_edge(self, src: CanvasNode, dst: CanvasNode) -> CanvasEdge: """ create an edge between source node and destination node """ - token = create_edge_token(source.id, dest.id) - if token not in self.edges: - pos = (source.core_node.position.x, source.core_node.position.y) - edge = CanvasEdge(self, source.id, pos, pos) - edge.complete(dest.id) - self.edges[edge.token] = edge - self.nodes[source.id].edges.add(edge) - self.nodes[dest.id].edges.add(edge) - self.core.create_link(edge, source, dest) + pos = (src.core_node.position.x, src.core_node.position.y) + edge = CanvasEdge(self, src.id, pos, pos) + self.complete_edge(src, dst, edge) + return edge + + def complete_edge( + self, + src: CanvasNode, + dst: CanvasNode, + edge: CanvasEdge, + link: Optional[Link] = None, + ) -> None: + linked_wireless = self.is_linked_wireless(src.id, dst.id) + edge.complete(dst.id, linked_wireless) + if link is None: + link = self.core.create_link(edge, src, dst) + edge.link = link + if link.iface1: + iface1 = link.iface1 + src.ifaces[iface1.id] = iface1 + if link.iface2: + iface2 = link.iface2 + dst.ifaces[iface2.id] = iface2 + src.edges.add(edge) + dst.edges.add(edge) + edge.token = create_edge_token(edge.link) + self.arc_common_edges(edge) + edge.draw_labels() + edge.check_options() + self.edges[edge.token] = edge + self.core.save_edge(edge, src, dst) def copy(self) -> None: if self.core.is_runtime(): @@ -967,13 +974,12 @@ def paste(self) -> None: if edge.src not in to_copy_ids or edge.dst not in to_copy_ids: if canvas_node.id == edge.src: dst_node = self.nodes[edge.dst] - self.create_edge(node, dst_node) - token = create_edge_token(node.id, dst_node.id) + copy_edge = self.create_edge(node, dst_node) elif canvas_node.id == edge.dst: src_node = self.nodes[edge.src] - self.create_edge(src_node, node) - token = create_edge_token(src_node.id, node.id) - copy_edge = self.edges[token] + copy_edge = self.create_edge(src_node, node) + else: + continue copy_link = copy_edge.link iface1_id = copy_link.iface1.id if copy_link.iface1 else None iface2_id = copy_link.iface2.id if copy_link.iface2 else None @@ -1000,13 +1006,11 @@ def paste(self) -> None: # copy link and link config for edge in to_copy_edges: - src_node_id = copy_map[edge.token[0]] - dst_node_id = copy_map[edge.token[1]] + src_node_id = copy_map[edge.src] + dst_node_id = copy_map[edge.dst] src_node_copy = self.nodes[src_node_id] dst_node_copy = self.nodes[dst_node_id] - self.create_edge(src_node_copy, dst_node_copy) - token = create_edge_token(src_node_copy.id, dst_node_copy.id) - copy_edge = self.edges[token] + copy_edge = self.create_edge(src_node_copy, dst_node_copy) copy_link = copy_edge.link iface1_id = copy_link.iface1.id if copy_link.iface1 else None iface2_id = copy_link.iface2.id if copy_link.iface2 else None @@ -1035,10 +1039,29 @@ def paste(self) -> None: ) self.tag_raise(tags.NODE) + def is_linked_wireless(self, src: int, dst: int) -> bool: + src_node = self.nodes[src] + dst_node = self.nodes[dst] + src_node_type = src_node.core_node.type + dst_node_type = dst_node.core_node.type + is_src_wireless = NodeUtils.is_wireless_node(src_node_type) + is_dst_wireless = NodeUtils.is_wireless_node(dst_node_type) + + # update the wlan/EMANE network + wlan_network = self.wireless_network + if is_src_wireless and not is_dst_wireless: + if src not in wlan_network: + wlan_network[src] = set() + wlan_network[src].add(dst) + elif not is_src_wireless and is_dst_wireless: + if dst not in wlan_network: + wlan_network[dst] = set() + wlan_network[dst].add(src) + return is_src_wireless or is_dst_wireless + def clear_throughputs(self) -> None: for edge in self.edges.values(): - edge.clear_middle_label() - edge.draw_link_options() + edge.clear_throughput() def scale_graph(self) -> None: for nid, canvas_node in self.nodes.items(): diff --git a/daemon/core/gui/graph/node.py b/daemon/core/gui/graph/node.py index b8172e1dd..2ac4219ee 100644 --- a/daemon/core/gui/graph/node.py +++ b/daemon/core/gui/graph/node.py @@ -247,14 +247,18 @@ def show_context(self, event: tk.Event) -> None: ) unlink_menu = tk.Menu(self.context) for edge in self.edges: - other_id = edge.src - if self.id == other_id: + link = edge.link + if self.id == edge.src: other_id = edge.dst + other_iface = link.iface2.name if link.iface2 else None + else: + other_id = edge.src + other_iface = link.iface1.name if link.iface1 else None other_node = self.canvas.nodes[other_id] + other_name = other_node.core_node.name + label = f"{other_name}:{other_iface}" if other_iface else other_name func_unlink = functools.partial(self.click_unlink, edge) - unlink_menu.add_command( - label=other_node.core_node.name, command=func_unlink - ) + unlink_menu.add_command(label=label, command=func_unlink) themes.style_menu(unlink_menu) self.context.add_cascade(label="Unlink", menu=unlink_menu) edit_menu = tk.Menu(self.context) @@ -318,10 +322,10 @@ def has_emane_link(self, iface_id: int) -> Node: for edge in self.edges: if self.id == edge.src: other_id = edge.dst - edge_iface_id = edge.src_iface.id + edge_iface_id = edge.link.iface1.id else: other_id = edge.src - edge_iface_id = edge.dst_iface.id + edge_iface_id = edge.link.iface2.id if edge_iface_id != iface_id: continue other_node = self.canvas.nodes[other_id] diff --git a/daemon/core/gui/graph/tags.py b/daemon/core/gui/graph/tags.py index b7b355172..3d3c3611e 100644 --- a/daemon/core/gui/graph/tags.py +++ b/daemon/core/gui/graph/tags.py @@ -5,6 +5,7 @@ SHAPE: str = "shape" SHAPE_TEXT: str = "shapetext" EDGE: str = "edge" +LOSS_EDGES: str = "loss-edge" LINK_LABEL: str = "linklabel" WIRELESS_EDGE: str = "wireless" ANTENNA: str = "antenna" diff --git a/daemon/core/gui/interface.py b/daemon/core/gui/interface.py index c075b95ae..4c5f59786 100644 --- a/daemon/core/gui/interface.py +++ b/daemon/core/gui/interface.py @@ -196,10 +196,10 @@ def find_subnets( for edge in canvas_node.edges: src_node = canvas.nodes[edge.src] dst_node = canvas.nodes[edge.dst] - iface = edge.src_iface + iface = edge.link.iface1 check_node = src_node if src_node == canvas_node: - iface = edge.dst_iface + iface = edge.link.iface2 check_node = dst_node if check_node.core_node.id in visited: continue diff --git a/daemon/core/gui/menubar.py b/daemon/core/gui/menubar.py index ebbac677a..fa7853adf 100644 --- a/daemon/core/gui/menubar.py +++ b/daemon/core/gui/menubar.py @@ -172,6 +172,11 @@ def draw_view_menu(self) -> None: command=self.canvas.show_links.click_handler, variable=self.canvas.show_links, ) + menu.add_checkbutton( + label="Loss Links", + command=self.canvas.show_loss_links.click_handler, + variable=self.canvas.show_loss_links, + ) menu.add_checkbutton( label="Wireless Links", command=self.canvas.show_wireless.click_handler, diff --git a/daemon/core/gui/nodeutils.py b/daemon/core/gui/nodeutils.py index ded1ac89a..5ee3469ee 100644 --- a/daemon/core/gui/nodeutils.py +++ b/daemon/core/gui/nodeutils.py @@ -62,12 +62,17 @@ class NodeUtils: IMAGE_NODES: Set[NodeType] = {NodeType.DOCKER, NodeType.LXC} WIRELESS_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE} RJ45_NODES: Set[NodeType] = {NodeType.RJ45} + BRIDGE_NODES: Set[NodeType] = {NodeType.HUB, NodeType.SWITCH} IGNORE_NODES: Set[NodeType] = {NodeType.CONTROL_NET} MOBILITY_NODES: Set[NodeType] = {NodeType.WIRELESS_LAN, NodeType.EMANE} NODE_MODELS: Set[str] = {"router", "host", "PC", "mdr", "prouter"} ROUTER_NODES: Set[str] = {"router", "mdr"} ANTENNA_ICON: PhotoImage = None + @classmethod + def is_bridge_node(cls, node: Node) -> bool: + return node.type in cls.BRIDGE_NODES + @classmethod def is_mobility(cls, node: Node) -> bool: return node.type in cls.MOBILITY_NODES diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index cf0c87388..9069b14c3 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -30,8 +30,6 @@ CoreServices = List[Union[CoreService, Type[CoreService]]] ConfigServiceType = Type[ConfigService] -_DEFAULT_MTU = 1500 - class NodeBase(abc.ABC): """ diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index bc242eacb..28f7f925c 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -19,6 +19,8 @@ from core.emulator.session import Session from core.nodes.base import CoreNetworkBase, CoreNode +DEFAULT_MTU: int = 1500 + class CoreInterface: """ @@ -338,7 +340,7 @@ def __init__( node: "CoreNode", name: str, localname: str, - mtu: int = 1500, + mtu: int = DEFAULT_MTU, server: "DistributedServer" = None, start: bool = True, ) -> None: @@ -403,7 +405,7 @@ def __init__( node: "CoreNode", name: str, localname: str, - mtu: int = 1500, + mtu: int = DEFAULT_MTU, server: "DistributedServer" = None, start: bool = True, ) -> None: diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index 58c1e195b..aef4b04b7 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -3,6 +3,7 @@ """ import logging +import math import threading import time from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Type @@ -447,77 +448,63 @@ def linkconfig( :param iface2: interface two :return: nothing """ - devname = iface.localname - tc = f"{TC} qdisc replace dev {devname}" - parent = "root" - changed = False - bw = options.bandwidth - if iface.setparam("bw", bw): - # from tc-tbf(8): minimum value for burst is rate / kernel_hz - burst = max(2 * iface.mtu, int(bw / 1000)) - # max IP payload - limit = 0xFFFF - tbf = f"tbf rate {bw} burst {burst} limit {limit}" - if bw > 0: - if self.up: - cmd = f"{tc} {parent} handle 1: {tbf}" - iface.host_cmd(cmd) - iface.setparam("has_tbf", True) - changed = True - elif iface.getparam("has_tbf") and bw <= 0: - if self.up: - cmd = f"{TC} qdisc delete dev {devname} {parent}" - iface.host_cmd(cmd) - iface.setparam("has_tbf", False) - # removing the parent removes the child - iface.setparam("has_netem", False) - changed = True - if iface.getparam("has_tbf"): - parent = "parent 1:1" - netem = "netem" - delay = options.delay - changed = max(changed, iface.setparam("delay", delay)) - loss = options.loss - if loss is not None: - loss = float(loss) - changed = max(changed, iface.setparam("loss", loss)) - duplicate = options.dup - if duplicate is not None: - duplicate = int(duplicate) - changed = max(changed, iface.setparam("duplicate", duplicate)) - jitter = options.jitter - changed = max(changed, iface.setparam("jitter", jitter)) + # determine if any settings have changed + changed = any( + [ + iface.setparam("bw", options.bandwidth), + iface.setparam("delay", options.delay), + iface.setparam("loss", options.loss), + iface.setparam("duplicate", options.dup), + iface.setparam("jitter", options.jitter), + iface.setparam("buffer", options.buffer), + ] + ) if not changed: return - # jitter and delay use the same delay statement - if delay is not None: - netem += f" delay {delay}us" - if jitter is not None: - if delay is None: - netem += f" delay 0us {jitter}us 25%" - else: - netem += f" {jitter}us 25%" - - if loss is not None and loss > 0: - netem += f" loss {min(loss, 100)}%" - if duplicate is not None and duplicate > 0: - netem += f" duplicate {min(duplicate, 100)}%" - - delay_check = delay is None or delay <= 0 - jitter_check = jitter is None or jitter <= 0 - loss_check = loss is None or loss <= 0 - duplicate_check = duplicate is None or duplicate <= 0 - if all([delay_check, jitter_check, loss_check, duplicate_check]): - # possibly remove netem if it exists and parent queue wasn't removed + + # delete tc configuration or create and add it + devname = iface.localname + if all( + [ + options.delay is None or options.delay <= 0, + options.jitter is None or options.jitter <= 0, + options.loss is None or options.loss <= 0, + options.dup is None or options.dup <= 0, + options.bandwidth is None or options.bandwidth <= 0, + options.buffer is None or options.buffer <= 0, + ] + ): if not iface.getparam("has_netem"): return if self.up: - cmd = f"{TC} qdisc delete dev {devname} {parent} handle 10:" + cmd = f"{TC} qdisc delete dev {devname} root handle 10:" iface.host_cmd(cmd) iface.setparam("has_netem", False) - elif len(netem) > 1: + else: + netem = "" + if options.bandwidth is not None: + limit = 1000 + bw = options.bandwidth / 1000 + if options.buffer is not None and options.buffer > 0: + limit = options.buffer + elif options.delay and options.bandwidth: + delay = options.delay / 1000 + limit = max(2, math.ceil((2 * bw * delay) / (8 * iface.mtu))) + netem += f" rate {bw}kbit" + netem += f" limit {limit}" + if options.delay is not None: + netem += f" delay {options.delay}us" + if options.jitter is not None: + if options.delay is None: + netem += f" delay 0us {options.jitter}us 25%" + else: + netem += f" {options.jitter}us 25%" + if options.loss is not None and options.loss > 0: + netem += f" loss {min(options.loss, 100)}%" + if options.dup is not None and options.dup > 0: + netem += f" duplicate {min(options.dup, 100)}%" if self.up: - cmd = f"{TC} qdisc replace dev {devname} {parent} handle 10: {netem}" + cmd = f"{TC} qdisc replace dev {devname} root handle 10: netem {netem}" iface.host_cmd(cmd) iface.setparam("has_netem", True) diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index 22819f6db..e69985ef3 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -13,7 +13,7 @@ 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 CoreInterface +from core.nodes.interface import DEFAULT_MTU, CoreInterface from core.nodes.network import CoreNetwork, GreTap if TYPE_CHECKING: @@ -252,7 +252,7 @@ def __init__( session: "Session", _id: int = None, name: str = None, - mtu: int = 1500, + mtu: int = DEFAULT_MTU, server: DistributedServer = None, ) -> None: """ diff --git a/daemon/core/services/frr.py b/daemon/core/services/frr.py index cec9d860f..0677a1d83 100644 --- a/daemon/core/services/frr.py +++ b/daemon/core/services/frr.py @@ -8,7 +8,7 @@ from core.emane.nodes import EmaneNet from core.nodes.base import CoreNode -from core.nodes.interface import CoreInterface +from core.nodes.interface import DEFAULT_MTU, CoreInterface from core.nodes.network import PtpNet, WlanNode from core.nodes.physical import Rj45Node from core.services.coreservices import CoreService @@ -384,7 +384,7 @@ def mtu_check(iface: CoreInterface) -> str: mtu-ignore command. This is needed when e.g. a node is linked via a GreTap device. """ - if iface.mtu != 1500: + if iface.mtu != DEFAULT_MTU: # a workaround for PhysicalNode GreTap, which has no knowledge of # the other nodes/nets return " ip ospf mtu-ignore\n" diff --git a/daemon/core/services/quagga.py b/daemon/core/services/quagga.py index 8c474fd83..f47da1d09 100644 --- a/daemon/core/services/quagga.py +++ b/daemon/core/services/quagga.py @@ -8,7 +8,7 @@ from core.emane.nodes import EmaneNet from core.emulator.enumerations import LinkTypes from core.nodes.base import CoreNode -from core.nodes.interface import CoreInterface +from core.nodes.interface import DEFAULT_MTU, CoreInterface from core.nodes.network import PtpNet, WlanNode from core.nodes.physical import Rj45Node from core.services.coreservices import CoreService @@ -301,7 +301,7 @@ def mtu_check(iface: CoreInterface) -> str: mtu-ignore command. This is needed when e.g. a node is linked via a GreTap device. """ - if iface.mtu != 1500: + if iface.mtu != DEFAULT_MTU: # a workaround for PhysicalNode GreTap, which has no knowledge of # the other nodes/nets return " ip ospf mtu-ignore\n" diff --git a/daemon/core/services/utility.py b/daemon/core/services/utility.py index a30d1f621..d7bd2edb3 100644 --- a/daemon/core/services/utility.py +++ b/daemon/core/services/utility.py @@ -603,7 +603,7 @@ def generate_config(cls, node: CoreNode, filename: str) -> str: class RadvdService(UtilService): name: str = "radvd" configs: Tuple[str, ...] = ("/etc/radvd/radvd.conf",) - dirs: Tuple[str, ...] = ("/etc/radvd",) + dirs: Tuple[str, ...] = ("/etc/radvd", "/var/run/radvd") startup: Tuple[str, ...] = ( "radvd -C /etc/radvd/radvd.conf -m logfile -l /var/log/radvd.log", ) diff --git a/daemon/core/utils.py b/daemon/core/utils.py index 40001fe1a..4a9d6ca65 100644 --- a/daemon/core/utils.py +++ b/daemon/core/utils.py @@ -15,6 +15,7 @@ import shlex import shutil import sys +from pathlib import Path from subprocess import PIPE, STDOUT, Popen from typing import ( TYPE_CHECKING, @@ -315,27 +316,25 @@ def sysctl_devname(devname: str) -> Optional[str]: return devname.replace(".", "/") -def load_config(filename: str, d: Dict[str, str]) -> None: +def load_config(file_path: Path, d: Dict[str, str]) -> None: """ Read key=value pairs from a file, into a dict. Skip comments; strip newline characters and spacing. - :param filename: file to read into a dictionary - :param d: dictionary to read file into + :param file_path: file path to read data from + :param d: dictionary to config into :return: nothing """ - with open(filename, "r") as f: + with file_path.open("r") as f: lines = f.readlines() - for line in lines: if line[:1] == "#": continue - try: key, value = line.split("=", 1) d[key] = value.strip() except ValueError: - logging.exception("error reading file to dict: %s", filename) + logging.exception("error reading file to dict: %s", file_path) def load_classes(path: str, clazz: Generic[T]) -> T: diff --git a/daemon/core/xml/emanexml.py b/daemon/core/xml/emanexml.py index 68f3fd69c..c0d5462b1 100644 --- a/daemon/core/xml/emanexml.py +++ b/daemon/core/xml/emanexml.py @@ -192,12 +192,13 @@ def build_platform_xml( 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) + + # define transport element + 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) diff --git a/daemon/examples/myemane/examplemodel.py b/daemon/examples/myemane/examplemodel.py index 82f1e97d7..b9e6e148a 100644 --- a/daemon/examples/myemane/examplemodel.py +++ b/daemon/examples/myemane/examplemodel.py @@ -1,7 +1,9 @@ """ Example custom emane model. """ +from typing import Dict, List, Optional, Set +from core.config import Configuration from core.emane import emanemanifest, emanemodel @@ -9,41 +11,45 @@ class ExampleModel(emanemodel.EmaneModel): """ Custom emane model. - :var str name: defines the emane model name that will show up in the GUI + :cvar name: defines the emane model name that will show up in the GUI Mac Definition: - :var str mac_library: defines that mac library that the model will reference - :var str mac_xml: defines the mac manifest file that will be parsed to obtain configuration options, + :cvar mac_library: defines that mac library that the model will reference + :cvar mac_xml: defines the mac manifest file that will be parsed to obtain configuration options, that will be displayed within the GUI - :var dict mac_mac_defaults: allows you to override options that are maintained within the manifest file above - :var list mac_mac_config: parses the manifest file and converts configurations into core supported formats + :cvar mac_defaults: allows you to override options that are maintained within the manifest file above + :cvar mac_config: parses the manifest file and converts configurations into core supported formats Phy Definition: NOTE: phy configuration will default to the universal model as seen below and the below section does not have to be included - :var str phy_library: defines that phy library that the model will reference, used if you need to + :cvar phy_library: defines that phy library that the model will reference, used if you need to provide a custom phy - :var str phy_xml: defines the phy manifest file that will be parsed to obtain configuration options, + :cvar phy_xml: defines the phy manifest file that will be parsed to obtain configuration options, that will be displayed within the GUI - :var dict phy_defaults: allows you to override options that are maintained within the manifest file above + :cvar phy_defaults: allows you to override options that are maintained within the manifest file above or for the default universal model - :var list phy_config: parses the manifest file and converts configurations into core supported formats + :cvar phy_config: parses the manifest file and converts configurations into core supported formats Custom Override Options: NOTE: these options default to what's seen below and do not have to be included - :var set config_ignore: allows you to ignore options within phy/mac, used typically if you needed to add + :cvar config_ignore: allows you to ignore options within phy/mac, used typically if you needed to add a custom option for display within the gui """ - name = "emane_example" - mac_library = "rfpipemaclayer" - mac_xml = "/usr/share/emane/manifest/rfpipemaclayer.xml" - mac_defaults = { + name: str = "emane_example" + mac_library: str = "rfpipemaclayer" + mac_xml: str = "/usr/share/emane/manifest/rfpipemaclayer.xml" + mac_defaults: Dict[str, str] = { "pcrcurveuri": "/usr/share/emane/xml/models/mac/rfpipe/rfpipepcr.xml" } - mac_config = emanemanifest.parse(mac_xml, mac_defaults) - phy_library = None - phy_xml = "/usr/share/emane/manifest/emanephy.xml" - phy_defaults = {"subid": "1", "propagationmodel": "2ray", "noisemode": "none"} - phy_config = emanemanifest.parse(phy_xml, phy_defaults) - config_ignore = set() + mac_config: List[Configuration] = emanemanifest.parse(mac_xml, mac_defaults) + phy_library: Optional[str] = None + phy_xml: str = "/usr/share/emane/manifest/emanephy.xml" + phy_defaults: Dict[str, str] = { + "subid": "1", + "propagationmodel": "2ray", + "noisemode": "none", + } + phy_config: List[Configuration] = emanemanifest.parse(phy_xml, phy_defaults) + config_ignore: Set[str] = set() diff --git a/daemon/examples/myservices/exampleservice.py b/daemon/examples/myservices/exampleservice.py new file mode 100644 index 000000000..b6b2bed02 --- /dev/null +++ b/daemon/examples/myservices/exampleservice.py @@ -0,0 +1,115 @@ +""" +Simple example custom service, used to drive shell commands on a node. +""" +from typing import Tuple + +from core.nodes.base import CoreNode +from core.services.coreservices import CoreService, ServiceMode + + +class ExampleService(CoreService): + """ + Example Custom CORE Service + + :cvar name: name used as a unique ID for this service and is required, no spaces + :cvar group: allows you to group services within the GUI under a common name + :cvar executables: executables this service depends on to function, if executable is + not on the path, service will not be loaded + :cvar dependencies: services that this service depends on for startup, tuple of + service names + :cvar dirs: directories that this service will create within a node + :cvar configs: files that this service will generate, without a full path this file + goes in the node's directory e.g. /tmp/pycore.12345/n1.conf/myfile + :cvar startup: commands used to start this service, any non-zero exit code will + cause a failure + :cvar validate: commands used to validate that a service was started, any non-zero + exit code will cause a failure + :cvar validation_mode: validation mode, used to determine startup success. + NON_BLOCKING - runs startup commands, and validates success with validation commands + BLOCKING - runs startup commands, and validates success with the startup commands themselves + TIMER - runs startup commands, and validates success by waiting for "validation_timer" alone + :cvar validation_timer: time in seconds for a service to wait for validation, before + determining success in TIMER/NON_BLOCKING modes. + :cvar validation_period: period in seconds to wait before retrying validation, + only used in NON_BLOCKING mode + :cvar shutdown: shutdown commands to stop this service + """ + + name: str = "ExampleService" + group: str = "Utility" + executables: Tuple[str, ...] = () + dependencies: Tuple[str, ...] = () + dirs: Tuple[str, ...] = () + configs: Tuple[str, ...] = ("myservice1.sh", "myservice2.sh") + startup: Tuple[str, ...] = tuple(f"sh {x}" for x in configs) + validate: Tuple[str, ...] = () + validation_mode: ServiceMode = ServiceMode.NON_BLOCKING + validation_timer: int = 5 + validation_period: float = 0.5 + shutdown: Tuple[str, ...] = () + + @classmethod + def on_load(cls) -> None: + """ + Provides a way to run some arbitrary logic when the service is loaded, possibly + to help facilitate dynamic settings for the environment. + + :return: nothing + """ + pass + + @classmethod + def get_configs(cls, node: CoreNode) -> Tuple[str, ...]: + """ + Provides a way to dynamically generate the config files from the node a service + will run. Defaults to the class definition and can be left out entirely if not + needed. + + :param node: core node that the service is being ran on + :return: tuple of config files to create + """ + return cls.configs + + @classmethod + def generate_config(cls, node: CoreNode, filename: str) -> str: + """ + Returns a string representation for a file, given the node the service is + starting on the config filename that this information will be used for. This + must be defined, if "configs" are defined. + + :param node: core node that the service is being ran on + :param filename: configuration file to generate + :return: configuration file content + """ + cfg = "#!/bin/sh\n" + if filename == cls.configs[0]: + cfg += "# auto-generated by MyService (sample.py)\n" + for iface in node.get_ifaces(): + cfg += f'echo "Node {node.name} has interface {iface.name}"\n' + elif filename == cls.configs[1]: + cfg += "echo hello" + return cfg + + @classmethod + def get_startup(cls, node: CoreNode) -> Tuple[str, ...]: + """ + Provides a way to dynamically generate the startup commands from the node a + service will run. Defaults to the class definition and can be left out entirely + if not needed. + + :param node: core node that the service is being ran on + :return: tuple of startup commands to run + """ + return cls.startup + + @classmethod + def get_validate(cls, node: CoreNode) -> Tuple[str, ...]: + """ + Provides a way to dynamically generate the validate commands from the node a + service will run. Defaults to the class definition and can be left out entirely + if not needed. + + :param node: core node that the service is being ran on + :return: tuple of commands to validate service startup with + """ + return cls.validate diff --git a/daemon/examples/myservices/sample.py b/daemon/examples/myservices/sample.py deleted file mode 100644 index e0c9a232f..000000000 --- a/daemon/examples/myservices/sample.py +++ /dev/null @@ -1,110 +0,0 @@ -""" -Simple example for a user-defined service. -""" - -from core.services.coreservices import CoreService, ServiceMode - - -class MyService(CoreService): - """ - Custom CORE Service - - :var str name: name used as a unique ID for this service and is required, no spaces - :var str group: allows you to group services within the GUI under a common name - :var tuple executables: executables this service depends on to function, if executable is - not on the path, service will not be loaded - :var tuple dependencies: services that this service depends on for startup, tuple of service names - :var tuple dirs: directories that this service will create within a node - :var tuple configs: files that this service will generate, without a full path this file goes in - the node's directory e.g. /tmp/pycore.12345/n1.conf/myfile - :var tuple startup: commands used to start this service, any non-zero exit code will cause a failure - :var tuple validate: commands used to validate that a service was started, any non-zero exit code - will cause a failure - :var ServiceMode validation_mode: validation mode, used to determine startup success. - NON_BLOCKING - runs startup commands, and validates success with validation commands - BLOCKING - runs startup commands, and validates success with the startup commands themselves - TIMER - runs startup commands, and validates success by waiting for "validation_timer" alone - :var int validation_timer: time in seconds for a service to wait for validation, before determining - success in TIMER/NON_BLOCKING modes. - :var float validation_validation_period: period in seconds to wait before retrying validation, - only used in NON_BLOCKING mode - :var tuple shutdown: shutdown commands to stop this service - """ - - name = "MyService" - group = "Utility" - executables = () - dependencies = () - dirs = () - configs = ("myservice1.sh", "myservice2.sh") - startup = tuple(f"sh {x}" for x in configs) - validate = () - validation_mode = ServiceMode.NON_BLOCKING - validation_timer = 5 - validation_period = 0.5 - shutdown = () - - @classmethod - def on_load(cls): - """ - Provides a way to run some arbitrary logic when the service is loaded, possibly to help facilitate - dynamic settings for the environment. - - :return: nothing - """ - pass - - @classmethod - def get_configs(cls, node): - """ - Provides a way to dynamically generate the config files from the node a service will run. - Defaults to the class definition and can be left out entirely if not needed. - - :param node: core node that the service is being ran on - :return: tuple of config files to create - """ - return cls.configs - - @classmethod - def generate_config(cls, node, filename): - """ - Returns a string representation for a file, given the node the service is starting on the config filename - that this information will be used for. This must be defined, if "configs" are defined. - - :param node: core node that the service is being ran on - :param str filename: configuration file to generate - :return: configuration file content - :rtype: str - """ - cfg = "#!/bin/sh\n" - - if filename == cls.configs[0]: - cfg += "# auto-generated by MyService (sample.py)\n" - for iface in node.get_ifaces(): - cfg += f'echo "Node {node.name} has interface {iface.name}"\n' - elif filename == cls.configs[1]: - cfg += "echo hello" - - return cfg - - @classmethod - def get_startup(cls, node): - """ - Provides a way to dynamically generate the startup commands from the node a service will run. - Defaults to the class definition and can be left out entirely if not needed. - - :param node: core node that the service is being ran on - :return: tuple of startup commands to run - """ - return cls.startup - - @classmethod - def get_validate(cls, node): - """ - Provides a way to dynamically generate the validate commands from the node a service will run. - Defaults to the class definition and can be left out entirely if not needed. - - :param node: core node that the service is being ran on - :return: tuple of commands to validate service startup with - """ - return cls.validate diff --git a/daemon/proto/core/api/grpc/core.proto b/daemon/proto/core/api/grpc/core.proto index 4727bbef7..d168afe0a 100644 --- a/daemon/proto/core/api/grpc/core.proto +++ b/daemon/proto/core/api/grpc/core.proto @@ -777,6 +777,7 @@ message LinkOptions { int64 delay = 8; int32 dup = 9; bool unidirectional = 10; + int32 buffer = 11; } message Interface { diff --git a/daemon/pyproject.toml b/daemon/pyproject.toml index 656b92da5..6916c1970 100644 --- a/daemon/pyproject.toml +++ b/daemon/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "core" -version = "7.2.1" +version = "7.3.0" description = "CORE Common Open Research Emulator" authors = ["Boeing Research and Technology"] license = "BSD-2-Clause" diff --git a/daemon/tests/test_links.py b/daemon/tests/test_links.py index 535ad8372..94c8c6997 100644 --- a/daemon/tests/test_links.py +++ b/daemon/tests/test_links.py @@ -83,6 +83,7 @@ def test_update_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): loss = 25 dup = 25 jitter = 10 + buffer = 100 node1 = session.add_node(CoreNode) node2 = session.add_node(SwitchNode) iface1_data = ip_prefixes.create_iface(node1) @@ -93,10 +94,16 @@ def test_update_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): assert iface1.getparam("loss") != loss assert iface1.getparam("duplicate") != dup assert iface1.getparam("jitter") != jitter + assert iface1.getparam("buffer") != buffer # when options = LinkOptions( - delay=delay, bandwidth=bandwidth, loss=loss, dup=dup, jitter=jitter + delay=delay, + bandwidth=bandwidth, + loss=loss, + dup=dup, + jitter=jitter, + buffer=buffer, ) session.update_link( node1.id, node2.id, iface1_id=iface1_data.id, options=options @@ -108,6 +115,7 @@ def test_update_node_to_net(self, session: Session, ip_prefixes: IpPrefixes): assert iface1.getparam("loss") == loss assert iface1.getparam("duplicate") == dup assert iface1.getparam("jitter") == jitter + assert iface1.getparam("buffer") == buffer def test_update_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -116,6 +124,7 @@ def test_update_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): loss = 25 dup = 25 jitter = 10 + buffer = 100 node1 = session.add_node(SwitchNode) node2 = session.add_node(CoreNode) iface2_data = ip_prefixes.create_iface(node2) @@ -126,10 +135,16 @@ def test_update_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): assert iface2.getparam("loss") != loss assert iface2.getparam("duplicate") != dup assert iface2.getparam("jitter") != jitter + assert iface2.getparam("buffer") != buffer # when options = LinkOptions( - delay=delay, bandwidth=bandwidth, loss=loss, dup=dup, jitter=jitter + delay=delay, + bandwidth=bandwidth, + loss=loss, + dup=dup, + jitter=jitter, + buffer=buffer, ) session.update_link( node1.id, node2.id, iface2_id=iface2_data.id, options=options @@ -141,6 +156,7 @@ def test_update_net_to_node(self, session: Session, ip_prefixes: IpPrefixes): assert iface2.getparam("loss") == loss assert iface2.getparam("duplicate") == dup assert iface2.getparam("jitter") == jitter + assert iface2.getparam("buffer") == buffer def test_update_ptp(self, session: Session, ip_prefixes: IpPrefixes): # given @@ -149,6 +165,7 @@ def test_update_ptp(self, session: Session, ip_prefixes: IpPrefixes): loss = 25 dup = 25 jitter = 10 + buffer = 100 node1 = session.add_node(CoreNode) node2 = session.add_node(CoreNode) iface1_data = ip_prefixes.create_iface(node1) @@ -161,15 +178,22 @@ def test_update_ptp(self, session: Session, ip_prefixes: IpPrefixes): assert iface1.getparam("loss") != loss assert iface1.getparam("duplicate") != dup assert iface1.getparam("jitter") != jitter + assert iface1.getparam("buffer") != buffer assert iface2.getparam("delay") != delay assert iface2.getparam("bw") != bandwidth assert iface2.getparam("loss") != loss assert iface2.getparam("duplicate") != dup assert iface2.getparam("jitter") != jitter + assert iface2.getparam("buffer") != buffer # when options = LinkOptions( - delay=delay, bandwidth=bandwidth, loss=loss, dup=dup, jitter=jitter + delay=delay, + bandwidth=bandwidth, + loss=loss, + dup=dup, + jitter=jitter, + buffer=buffer, ) session.update_link(node1.id, node2.id, iface1_data.id, iface2_data.id, options) @@ -179,11 +203,13 @@ def test_update_ptp(self, session: Session, ip_prefixes: IpPrefixes): assert iface1.getparam("loss") == loss assert iface1.getparam("duplicate") == dup assert iface1.getparam("jitter") == jitter + assert iface1.getparam("buffer") == buffer assert iface2.getparam("delay") == delay assert iface2.getparam("bw") == bandwidth assert iface2.getparam("loss") == loss assert iface2.getparam("duplicate") == dup assert iface2.getparam("jitter") == jitter + assert iface2.getparam("buffer") == buffer def test_delete_ptp(self, session: Session, ip_prefixes: IpPrefixes): # given diff --git a/docs/emane.md b/docs/emane.md index bbd3b0a4a..f589f8341 100644 --- a/docs/emane.md +++ b/docs/emane.md @@ -50,6 +50,26 @@ can also subscribe to EMANE location events and move the nodes on the canvas as they are moved in the EMANE emulation. This would occur when an Emulation Script Generator, for example, is running a mobility script. +## EMANE in CORE + +This section will cover some high level topics and examples for running and +using EMANE in CORE. + +You can find more detailed tutorials and examples at the +[EMANE Tutorial](https://github.com/adjacentlink/emane-tutorial/wiki). + +Every topic below assumes CORE, EMANE, and OSPF MDR have been installed. + +> **WARNING:** demo files will be found within the new `core-pygui` + +|Topic|Model|Description| +|---|---|---| +|[XML Files](emane/files.md)|RF Pipe|Overview of generated XML files used to drive EMANE| +|[GPSD](emane/gpsd.md)|RF Pipe|Overview of running and integrating gpsd with EMANE| +|[Precomputed](emane/precomputed.md)|RF Pipe|Overview of using the precomputed propagation model| +|[EEL](emane/eel.md)|RF Pipe|Overview of using the Emulation Event Log (EEL) Generator| +|[Antenna Profiles](emane/antenna.md)|RF Pipe|Overview of using antenna profiles in EMANE| + ## EMANE Configuration The CORE configuration file **/etc/core/core.conf** has options specific to @@ -96,7 +116,61 @@ placed within the path defined by **emane_models_dir** in the CORE configuration file. This path cannot end in **/emane**. Here is an example model with documentation describing functionality: -[Example Model](../daemon/examples/myemane/examplemodel.py) +```python +""" +Example custom emane model. +""" +from typing import Dict, List, Optional, Set + +from core.config import Configuration +from core.emane import emanemanifest, emanemodel + + +class ExampleModel(emanemodel.EmaneModel): + """ + Custom emane model. + + :cvar name: defines the emane model name that will show up in the GUI + + Mac Definition: + :cvar mac_library: defines that mac library that the model will reference + :cvar mac_xml: defines the mac manifest file that will be parsed to obtain configuration options, + that will be displayed within the GUI + :cvar mac_defaults: allows you to override options that are maintained within the manifest file above + :cvar mac_config: parses the manifest file and converts configurations into core supported formats + + Phy Definition: + NOTE: phy configuration will default to the universal model as seen below and the below section does not + have to be included + :cvar phy_library: defines that phy library that the model will reference, used if you need to + provide a custom phy + :cvar phy_xml: defines the phy manifest file that will be parsed to obtain configuration options, + that will be displayed within the GUI + :cvar phy_defaults: allows you to override options that are maintained within the manifest file above + or for the default universal model + :cvar phy_config: parses the manifest file and converts configurations into core supported formats + + Custom Override Options: + NOTE: these options default to what's seen below and do not have to be included + :cvar config_ignore: allows you to ignore options within phy/mac, used typically if you needed to add + a custom option for display within the gui + """ + + name: str = "emane_example" + mac_library: str = "rfpipemaclayer" + mac_xml: str = "/usr/share/emane/manifest/rfpipemaclayer.xml" + mac_defaults: Dict[str, str] = { + "pcrcurveuri": "/usr/share/emane/xml/models/mac/rfpipe/rfpipepcr.xml" + } + mac_config: List[Configuration] = emanemanifest.parse(mac_xml, mac_defaults) + phy_library: Optional[str] = None + phy_xml: str = "/usr/share/emane/manifest/emanephy.xml" + phy_defaults: Dict[str, str] = { + "subid": "1", "propagationmodel": "2ray", "noisemode": "none" + } + phy_config: List[Configuration] = emanemanifest.parse(phy_xml, phy_defaults) + config_ignore: Set[str] = set() +``` ## Single PC with EMANE diff --git a/docs/emane/antenna.md b/docs/emane/antenna.md new file mode 100644 index 000000000..20c983044 --- /dev/null +++ b/docs/emane/antenna.md @@ -0,0 +1,435 @@ +# EMANE Antenna Profiles +* Table of Contents +{:toc} + +## Overview +Introduction to using the EMANE antenna profile in CORE, based on the example +EMANE Demo linked below. + +[EMANE Demo 6](https://github.com/adjacentlink/emane-tutorial/wiki/Demonstration-6) +for more specifics. + +## Demo Setup +We will need to create some files in advance of starting this session. + +Create directory to place antenna profile files. +```shell +mkdir /tmp/emane +``` + +Create `/tmp/emane/antennaprofile.xml` with the following contents. +```xml + + + + + + + + + + +``` + +Create `/tmp/emane/antenna30dsector.xml` with the following contents. +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +Create `/tmp/emane/blockageaft.xml` with the following contents. +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +## Run Demo +1. Select `Open...` within the GUI +1. Load `emane-demo-antenna.xml` +1. Click ![Start Button](../static/gui/start.gif) +1. After startup completes, double click n1 to bring up the nodes terminal + +## Example Demo +This demo will cover running an EMANE event service to feed in antenna, +location, and pathloss events to demonstrate how antenna profiles +can be used. + +### EMANE Event Dump +On n1 lets dump EMANE events, so when we later run the EMANE event service +you can monitor when and what is sent. + +```shell +root@n1:/tmp/pycore.44917/n1.conf# emaneevent-dump -i ctrl0 +``` + +### Send EMANE Events +On the host machine create the following to send EMANE events. + +> **WARNING:** make sure to set the `eventservicedevice` to the proper control +> network value + +Create `eventservice.xml` with the following contents. +```xml + + + + + + + +``` + +Create `eelgenerator.xml` with the following contents. +```xml + + + + + + + + + + + +``` + +Create `scenario.eel` with the following contents. +```shell +0.0 nem:1 antennaprofile 1,0.0,0.0 +0.0 nem:4 antennaprofile 2,0.0,0.0 +# +0.0 nem:1 pathloss nem:2,60 nem:3,60 nem:4,60 +0.0 nem:2 pathloss nem:3,60 nem:4,60 +0.0 nem:3 pathloss nem:4,60 +# +0.0 nem:1 location gps 40.025495,-74.315441,3.0 +0.0 nem:2 location gps 40.025495,-74.312501,3.0 +0.0 nem:3 location gps 40.023235,-74.315441,3.0 +0.0 nem:4 location gps 40.023235,-74.312501,3.0 +0.0 nem:4 velocity 180.0,0.0,10.0 +# +30.0 nem:1 velocity 20.0,0.0,10.0 +30.0 nem:1 orientation 0.0,0.0,10.0 +30.0 nem:1 antennaprofile 1,60.0,0.0 +30.0 nem:4 velocity 270.0,0.0,10.0 +# +60.0 nem:1 antennaprofile 1,105.0,0.0 +60.0 nem:4 antennaprofile 2,45.0,0.0 +# +90.0 nem:1 velocity 90.0,0.0,10.0 +90.0 nem:1 orientation 0.0,0.0,0.0 +90.0 nem:1 antennaprofile 1,45.0,0.0 +``` + +Run the EMANE event service, monitor what is output on n1 for events +dumped and see the link changes within the CORE GUI. +```shell +emaneeventservice -l 3 eventservice.xml +``` + +### Stages +The events sent will trigger 4 different states. + +* State 1 + * n2 and n3 see each other + * n4 and n3 are pointing away +* State 2 + * n2 and n3 see each other + * n1 and n2 see each other + * n4 and n3 see each other +* State 3 + * n2 and n3 see each other + * n4 and n3 are pointing at each other but blocked +* State 4 + * n2 and n3 see each other + * n4 and n3 see each other diff --git a/docs/emane/eel.md b/docs/emane/eel.md new file mode 100644 index 000000000..0f41c3578 --- /dev/null +++ b/docs/emane/eel.md @@ -0,0 +1,103 @@ +# EMANE Emulation Event Log (EEL) Generator +* Table of Contents +{:toc} + +## Overview +Introduction to using the EMANE event service and eel files to provide events. + +[EMANE Demo 1](https://github.com/adjacentlink/emane-tutorial/wiki/Demonstration-1) +for more specifics. + +## Run Demo +1. Select `Open...` within the GUI +1. Load `emane-demo-eel.xml` +1. Click ![Start Button](../static/gui/start.gif) +1. After startup completes, double click n1 to bring up the nodes terminal + +## Example Demo +This demo will go over defining an EMANE event service and eel file to drive +an emane event service. + +### Viewing Events +On n1 we will use the EMANE event dump utility to listen to events. +```shell +root@n1:/tmp/pycore.46777/n1.conf# emaneevent-dump -i ctrl0 +``` + +### Sending Events +On the host machine we will create the following files and start the +EMANE event service targeting the control network. + +> **WARNING:** make sure to set the `eventservicedevice` to the proper control +> network value + +Create `eventservice.xml` with the following contents. +```xml + + + + + + + +``` + +Next we will create the `eelgenerator.xml` file. The EEL Generator is actually +a plugin that loads sentence parsing plugins. The sentence parsing plugins know +how to convert certain sentences, in this case commeffect, location, velocity, +orientation, pathloss and antennaprofile sentences, into their corresponding +emane event equivalents. + +* commeffect:eelloadercommeffect:delta +* location,velocity,orientation:eelloaderlocation:delta +* pathloss:eelloaderpathloss:delta +* antennaprofile:eelloaderantennaprofile:delta + +These configuration items tell the EEL Generator which sentences to map to +which plugin and whether to issue delta or full updates. + +Create `eelgenerator.xml` with the following contents. +```xml + + + + + + + + + + + +``` + +Finally, create `scenario.eel` with the following contents. +```shell +0.0 nem:1 pathloss nem:2,90.0 +0.0 nem:2 pathloss nem:1,90.0 +0.0 nem:1 location gps 40.031075,-74.523518,3.000000 +0.0 nem:2 location gps 40.031165,-74.523412,3.000000 +``` + +Start the EMANE event service using the files created above. +```shell +emaneeventservice eventservice.xml -l 3 +``` + +### Sent Events +If we go back to look at our original terminal we will see the events logged +out to the terminal. + +```shell +root@n1:/tmp/pycore.46777/n1.conf# emaneevent-dump -i ctrl0 +[1601858142.917224] nem: 0 event: 100 len: 66 seq: 1 [Location] + UUID: 0af267be-17d3-4103-9f76-6f697e13bcec + (1, {'latitude': 40.031075, 'altitude': 3.0, 'longitude': -74.823518}) + (2, {'latitude': 40.031165, 'altitude': 3.0, 'longitude': -74.523412}) +[1601858142.917466] nem: 1 event: 101 len: 14 seq: 2 [Pathloss] + UUID: 0af267be-17d3-4103-9f76-6f697e13bcec + (2, {'forward': 90.0, 'reverse': 90.0}) +[1601858142.917889] nem: 2 event: 101 len: 14 seq: 3 [Pathloss] + UUID: 0af267be-17d3-4103-9f76-6f697e13bcec + (1, {'forward': 90.0, 'reverse': 90.0}) +``` diff --git a/docs/emane/files.md b/docs/emane/files.md new file mode 100644 index 000000000..62729ac8e --- /dev/null +++ b/docs/emane/files.md @@ -0,0 +1,160 @@ +# EMANE XML Files +* Table of Contents +{:toc} + +## Overview +Introduction to the XML files generated by CORE used to drive EMANE for +a given node. + +[EMANE Demo 0](https://github.com/adjacentlink/emane-tutorial/wiki/Demonstration-0) +may provide more helpful details. + +## Run Demo +1. Select `Open...` within the GUI +1. Load `emane-demo-files.xml` +1. Click ![Start Button](../static/gui/start.gif) +1. After startup completes, double click n1 to bring up the nodes terminal + +## Example Demo +We will take a look at the files generated in the example demo provided. In this +case we are running the RF Pipe model. + +### Generated Files + +|Name|Description| +|---|---| +|\-platform.xml|configuration file for the emulator instances| +|\-nem.xml|configuration for creating a NEM| +|\-mac.xml|configuration for defining a NEMs MAC layer| +|\-phy.xml|configuration for defining a NEMs PHY layer| +|\-trans-virtual.xml|configuration when a virtual transport is being used| +|\-trans.xml|configuration when a raw transport is being used| + +### Listing File +Below are the files within n1 after starting the demo session. + +```shell +root@n1:/tmp/pycore.46777/n1.conf# ls +eth0-mac.xml eth0-trans-virtual.xml n1-platform.xml var.log +eth0-nem.xml ipforward.sh quaggaboot.sh var.run +eth0-phy.xml n1-emane.log usr.local.etc.quagga var.run.quagga +``` + +### Platform XML +The root configuration file used to run EMANE for a node is the platform xml file. +In this demo we are looking at `n1-platform.xml`. + +* lists all configuration values set for the platform +* The unique nem id given for each interface that EMANE will create for this node +* The path to the file(s) used for definition for a given nem + +```shell +root@n1:/tmp/pycore.46777/n1.conf# cat n1-platform.xml + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### NEM XML +The nem definition will contain reference to the transport, mac, and phy xml +definitions being used for a given nem. + +```shell +root@n1:/tmp/pycore.46777/n1.conf# cat eth0-nem.xml + + + + + + + +``` + +### MAC XML +MAC layer configuration settings would be found in this file. CORE will write +out all values, even if the value is a default value. + +```shell +root@n1:/tmp/pycore.46777/n1.conf# cat eth0-mac.xml + + + + + + + + + + + + + + +``` + +### PHY XML +PHY layer configuration settings would be found in this file. CORE will write +out all values, even if the value is a default value. + +```shell +root@n1:/tmp/pycore.46777/n1.conf# cat eth0-phy.xml + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### Transport XML +```shell +root@n1:/tmp/pycore.46777/n1.conf# cat eth0-trans-virtual.xml + + + + + + +``` diff --git a/docs/emane/gpsd.md b/docs/emane/gpsd.md new file mode 100644 index 000000000..06c441988 --- /dev/null +++ b/docs/emane/gpsd.md @@ -0,0 +1,103 @@ +# EMANE GPSD Integration +* Table of Contents +{:toc} + +## Overview +Introduction to integrating gpsd in CORE with EMANE. + +[EMANE Demo 0](https://github.com/adjacentlink/emane-tutorial/wiki/Demonstration-0) +may provide more helpful details. + +> **WARNING:** requires installation of [gpsd](https://gpsd.gitlab.io/gpsd/index.html) + +## Run Demo +1. Select `Open...` within the GUI +1. Load `emane-demo-gpsd.xml` +1. Click ![Start Button](../static/gui/start.gif) +1. After startup completes, double click n1 to bring up the nodes terminal + +## Example Demo +This section will cover how to run a gpsd location agent within EMANE, that will +write out locations to a pseudo terminal file. That file can be read in by the +gpsd server and make EMANE location events available to gpsd clients. + +### EMANE GPSD Event Daemon +First create an `eventdaemon.xml` file on n1 with the following contents. +```xml + + + + + + + +``` + +Then create the `gpsdlocationagent.xml` file on n1 with the following contents. +```xml + + + + + +``` + +Start the EMANE event agent. This will facilitate feeding location events +out to a pseudo terminal file defined above. +```shell +emaneeventd eventdaemon.xml -r -d -l 3 -f emaneeventd.log +``` + +Start gpsd, reading in the pseudo terminal file. +```shell +gpsd -G -n -b $(cat gps.pty) +``` + +### EMANE EEL Event Daemon + +EEL Events will be played out from the actual host machine over the designated +control network interface. Create the following files in the same directory +somewhere on your host. + +> **NOTE:** make sure the below eventservicedevice matches the control network +> device being used on the host for EMANE + +Create `eventservice.xml` on the host machine with the following contents. +```xml + + + + + + + +``` + +Create `eelgenerator.xml` on the host machine with the following contents. +```xml + + + + + + + + + + + +``` + +Create `scenario.eel` file with the following contents. +```shell +0.0 nem:1 location gps 40.031075,-74.523518,3.000000 +0.0 nem:2 location gps 40.031165,-74.523412,3.000000 +``` + +Start the EEL event service, which will send the events defined in the file above +over the control network to all EMANE nodes. These location events will be received +and provided to gpsd. This allow gpsd client to connect to and get gps locations. +```shell +emaneeventservice eventservice.xml -l 3 +``` + diff --git a/docs/emane/precomputed.md b/docs/emane/precomputed.md new file mode 100644 index 000000000..f8064c970 --- /dev/null +++ b/docs/emane/precomputed.md @@ -0,0 +1,80 @@ +# EMANE Procomputed +* Table of Contents +{:toc} + +## Overview +Introduction to using the precomputed propagation model. + +[EMANE Demo 1](https://github.com/adjacentlink/emane-tutorial/wiki/Demonstration-1) +for more specifics. + +## Run Demo +1. Select `Open...` within the GUI +1. Load `emane-demo-precomputed.xml` +1. Click ![Start Button](../static/gui/start.gif) +1. After startup completes, double click n1 to bring up the nodes terminal + +## Example Demo +This demo is uing the RF Pipe model witht he propagation model set to +precomputed. + +### Failed Pings +Due to using precomputed and having not sent any pathloss events, the nodes +cannot ping eachother yet. + +Open a terminal on n1. +```shell +root@n1:/tmp/pycore.46777/n1.conf# ping 10.0.0.2 +connect: Network is unreachable +``` + +### EMANE Shell +You can leverage `emanesh` to investigate why packets are being dropped. +```shell +root@n1:/tmp/pycore.46777/n1.conf# emanesh localhost get table nems phy BroadcastPacketDropTable0 UnicastPacketDropTable0 +nem 1 phy BroadcastPacketDropTable0 +| NEM | Out-of-Band | Rx Sensitivity | Propagation Model | Gain Location | Gain Horizon | Gain Profile | Not FOI | Spectrum Clamp | Fade Location | Fade Algorithm | Fade Select | +| 2 | 0 | 0 | 169 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | + +nem 1 phy UnicastPacketDropTable0 +| NEM | Out-of-Band | Rx Sensitivity | Propagation Model | Gain Location | Gain Horizon | Gain Profile | Not FOI | Spectrum Clamp | Fade Location | Fade Algorithm | Fade Select | +``` + +In the example above we can see that the reason packets are being dropped is due to +the propogation model and that is because we have not issued any pathloss events. +You can run another command to validate if you have received any pathloss events. +```shell +root@n1:/tmp/pycore.46777/n1.conf# emanesh localhost get table nems phy PathlossEventInfoTable +nem 1 phy PathlossEventInfoTable +| NEM | Forward Pathloss | Reverse Pathloss | +``` + +### Pathloss Events +On the host we will send pathloss events from all nems to all other nems. + +> **NOTE:** make sure properly specify the right control network device + +```shell +emaneevent-pathloss 1:2 90 -i +``` + +Now if we check for pathloss events on n2 we will see what was just sent above. +```shell +root@n1:/tmp/pycore.46777/n1.conf# emanesh localhost get table nems phy PathlossEventInfoTable +nem 1 phy PathlossEventInfoTable +| NEM | Forward Pathloss | Reverse Pathloss | +| 2 | 90.0 | 90.0 +``` + +You should also now be able to ping n1 from n2. +```shell +root@n1:/tmp/pycore.46777/n1.conf# ping -c 3 10.0.0.2 +PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data. +64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=3.06 ms +64 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=2.12 ms +64 bytes from 10.0.0.2: icmp_seq=3 ttl=64 time=1.99 ms + +--- 10.0.0.2 ping statistics --- +3 packets transmitted, 3 received, 0% packet loss, time 2001ms +rtt min/avg/max/mdev = 1.991/2.393/3.062/0.479 ms +``` diff --git a/docs/install.md b/docs/install.md index 26203b39c..7c5ebb84f 100644 --- a/docs/install.md +++ b/docs/install.md @@ -9,6 +9,37 @@ or build and install a python wheel. > **WARNING:** if Docker is installed, the default iptable rules will block CORE traffic +### Requirements +Any computer capable of running Linux should be able to run CORE. Since the physical machine will be hosting numerous +containers, as a general rule you should select a machine having as much RAM and CPU resources as possible. + +* Linux Kernel v3.3+ +* iproute2 4.5+ is a requirement for bridge related commands +* ebtables not backed by nftables + +### Supported Linux Distributions +Plan is to support recent Ubuntu and CentOS LTS releases. + +Verified: +* Ubuntu - 18.04, 20.04 +* CentOS - 7.8, 8.0* + +> **NOTE:** Ubuntu 20.04 requires installing legacy ebtables for WLAN +> functionality + +> **NOTE:** CentOS 8 does not provide legacy ebtables support, WLAN will not +> function properly + +> **NOTE:** CentOS 8 does not have the netem kernel mod available by default + +CentOS 8 Enabled netem: +```shell +sudo yum update +# restart into updated kernel +sudo yum install -y kernel-modules-extra +sudo modprobe sch_netem +``` + ### Tools Used The following tools will be leveraged during installation: @@ -25,25 +56,25 @@ The following is a list of files that would be installed after running the autom > **NOTE:** the default install prefix is /usr/local, but can be changed as noted below * executable files - * /bin/{core-daemon, core-gui, vcmd, vnoded, etc} + * `/bin/{core-daemon, core-gui, vcmd, vnoded, etc}` * tcl/tk gui files - * /lib/core - * /share/core/icons + * `/lib/core` + * `/share/core/icons` * example imn files - * /share/core/examples + * `/share/core/examples` * python files * poetry virtual env * `cd /daemon && poetry env info` - * ~/.cache/pypoetry/virtualenvs/ + * `~/.cache/pypoetry/virtualenvs/` * local python install * default install path for python3 installation of a wheel * `python3 -c "import core; print(core.__file__)"` * configuration files - * /etc/core/{core.conf, logging.conf} + * `/etc/core/{core.conf, logging.conf}` * ospf mdr repository files - * /../ospf-mdr + * `/../ospf-mdr` * emane repository files - * /../emane + * `/../emane` ### Installed Executables After the installation complete it will have installed the following scripts. @@ -62,39 +93,6 @@ After the installation complete it will have installed the following scripts. | core-service-update | tool to update automate modifying a legacy service to match current naming | | coresendmsg | tool to send TLV API commands from command line | -### Required Hardware -Any computer capable of running Linux should be able to run CORE. Since the physical machine will be hosting numerous -containers, as a general rule you should select a machine having as much RAM and CPU resources as possible. - -### Supported Linux Distributions -Plan is to support recent Ubuntu and CentOS LTS releases. - -Verified: -* Ubuntu - 18.04, 20.04 -* CentOS - 7.8, 8.0* - -> **NOTE:** Ubuntu 20.04 requires installing legacy ebtables for WLAN -> functionality - -> **NOTE:** CentOS 8 does not provide legacy ebtables support, WLAN will not -> function properly - -> **NOTE:** CentOS 8 does not have the netem kernel mod available by default - -CentOS 8 Enabled netem: -```shell -sudo yum update -# restart into updated kernel -sudo yum install -y kernel-modules-extra -sudo modprobe sch_netem -``` - -### Utility Requirements -The following are known dependencies that will result in errors when not met. - -* iproute2 4.5+ is a requirement for bridge related commands -* ebtables not backed by nftables - ## Upgrading from Older Release Please make sure to uninstall any previous installations of CORE cleanly before proceeding to install. diff --git a/docs/services.md b/docs/services.md index 2ce52e997..ccd8e9a5a 100644 --- a/docs/services.md +++ b/docs/services.md @@ -180,68 +180,77 @@ consider contributing it to the CORE project. Below is the skeleton for a custom service with some documentation. Most people would likely only setup the required class variables **(name/group)**. Then define the **configs** (files they want to generate) and implement the -**generate_confifs** function to dynamically create the files wanted. Finally +**generate_config** function to dynamically create the files wanted. Finally the **startup** commands would be supplied, which typically tends to be running the shell files generated. ```python +""" +Simple example custom service, used to drive shell commands on a node. +""" +from typing import Tuple + +from core.nodes.base import CoreNode from core.services.coreservices import CoreService, ServiceMode -class MyService(CoreService): +class ExampleService(CoreService): """ - Custom CORE Service + Example Custom CORE Service - :var str name: name used as a unique ID for this service and is required, no spaces - :var str group: allows you to group services within the GUI under a common name - :var tuple executables: executables this service depends on to function, if executable is + :cvar name: name used as a unique ID for this service and is required, no spaces + :cvar group: allows you to group services within the GUI under a common name + :cvar executables: executables this service depends on to function, if executable is not on the path, service will not be loaded - :var tuple dependencies: services that this service depends on for startup, tuple of service names - :var tuple dirs: directories that this service will create within a node - :var tuple configs: files that this service will generate, without a full path this file goes in - the node's directory e.g. /tmp/pycore.12345/n1.conf/myfile - :var tuple startup: commands used to start this service, any non-zero exit code will cause a failure - :var tuple validate: commands used to validate that a service was started, any non-zero exit code - will cause a failure - :var ServiceMode validation_mode: validation mode, used to determine startup success. + :cvar dependencies: services that this service depends on for startup, tuple of + service names + :cvar dirs: directories that this service will create within a node + :cvar configs: files that this service will generate, without a full path this file + goes in the node's directory e.g. /tmp/pycore.12345/n1.conf/myfile + :cvar startup: commands used to start this service, any non-zero exit code will + cause a failure + :cvar validate: commands used to validate that a service was started, any non-zero + exit code will cause a failure + :cvar validation_mode: validation mode, used to determine startup success. NON_BLOCKING - runs startup commands, and validates success with validation commands BLOCKING - runs startup commands, and validates success with the startup commands themselves TIMER - runs startup commands, and validates success by waiting for "validation_timer" alone - :var int validation_timer: time in seconds for a service to wait for validation, before determining - success in TIMER/NON_BLOCKING modes. - :var float validation_validation_period: period in seconds to wait before retrying validation, + :cvar validation_timer: time in seconds for a service to wait for validation, before + determining success in TIMER/NON_BLOCKING modes. + :cvar validation_period: period in seconds to wait before retrying validation, only used in NON_BLOCKING mode - :var tuple shutdown: shutdown commands to stop this service + :cvar shutdown: shutdown commands to stop this service """ - name = "MyService" - group = "Utility" - executables = () - dependencies = () - dirs = () - configs = ("myservice1.sh", "myservice2.sh") - startup = tuple(f"sh {x}" for x in configs) - validate = () - validation_mode = ServiceMode.NON_BLOCKING - validation_timer = 5 - validation_period = 0.5 - shutdown = () + name: str = "ExampleService" + group: str = "Utility" + executables: Tuple[str, ...] = () + dependencies: Tuple[str, ...] = () + dirs: Tuple[str, ...] = () + configs: Tuple[str, ...] = ("myservice1.sh", "myservice2.sh") + startup: Tuple[str, ...] = tuple(f"sh {x}" for x in configs) + validate: Tuple[str, ...] = () + validation_mode: ServiceMode = ServiceMode.NON_BLOCKING + validation_timer: int = 5 + validation_period: float = 0.5 + shutdown: Tuple[str, ...] = () @classmethod - def on_load(cls): + def on_load(cls) -> None: """ - Provides a way to run some arbitrary logic when the service is loaded, possibly to help facilitate - dynamic settings for the environment. + Provides a way to run some arbitrary logic when the service is loaded, possibly + to help facilitate dynamic settings for the environment. :return: nothing """ pass @classmethod - def get_configs(cls, node): + def get_configs(cls, node: CoreNode) -> Tuple[str, ...]: """ - Provides a way to dynamically generate the config files from the node a service will run. - Defaults to the class definition and can be left out entirely if not needed. + Provides a way to dynamically generate the config files from the node a service + will run. Defaults to the class definition and can be left out entirely if not + needed. :param node: core node that the service is being ran on :return: tuple of config files to create @@ -249,32 +258,31 @@ class MyService(CoreService): return cls.configs @classmethod - def generate_config(cls, node, filename): + def generate_config(cls, node: CoreNode, filename: str) -> str: """ - Returns a string representation for a file, given the node the service is starting on the config filename - that this information will be used for. This must be defined, if "configs" are defined. + Returns a string representation for a file, given the node the service is + starting on the config filename that this information will be used for. This + must be defined, if "configs" are defined. :param node: core node that the service is being ran on - :param str filename: configuration file to generate + :param filename: configuration file to generate :return: configuration file content - :rtype: str """ cfg = "#!/bin/sh\n" - if filename == cls.configs[0]: cfg += "# auto-generated by MyService (sample.py)\n" - for ifc in node.get_ifaces(): - cfg += f'echo "Node {node.name} has interface {ifc.name}"\n' + for iface in node.get_ifaces(): + cfg += f'echo "Node {node.name} has interface {iface.name}"\n' elif filename == cls.configs[1]: cfg += "echo hello" - return cfg @classmethod - def get_startup(cls, node): + def get_startup(cls, node: CoreNode) -> Tuple[str, ...]: """ - Provides a way to dynamically generate the startup commands from the node a service will run. - Defaults to the class definition and can be left out entirely if not needed. + Provides a way to dynamically generate the startup commands from the node a + service will run. Defaults to the class definition and can be left out entirely + if not needed. :param node: core node that the service is being ran on :return: tuple of startup commands to run @@ -282,10 +290,11 @@ class MyService(CoreService): return cls.startup @classmethod - def get_validate(cls, node): + def get_validate(cls, node: CoreNode) -> Tuple[str, ...]: """ - Provides a way to dynamically generate the validate commands from the node a service will run. - Defaults to the class definition and can be left out entirely if not needed. + Provides a way to dynamically generate the validate commands from the node a + service will run. Defaults to the class definition and can be left out entirely + if not needed. :param node: core node that the service is being ran on :return: tuple of commands to validate service startup with