diff --git a/CHANGELOG.md b/CHANGELOG.md index 30b5c711c..f02b0eece 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +## 2021-01-11 CORE 7.4.0 + +* Installation + * fixed issue for automated install assuming ID_LIKE is always present in /etc/os-release +* gRPC API + * fixed issue stopping session and not properly going to data collect state + * fixed issue to have start session properly create a directory before configuration state +* core-pygui + * fixed issue handling deletion of wired link to a switch + * avoid saving edge metadata to xml when values are default + * fixed issue editing node mac addresses + * added support for configuring interface names + * fixed issue with potential node names to allow hyphens and remove under bars + * \#531 - fixed issue changing distributed nodes back to local +* core-daemon + * fixed issue to properly handle deleting links from a network to network node + * updated xml to support writing and reading link buffer configurations + * reverted change and removed mac learning from wlan, due to promiscuous like behavior + * fixed issue creating control interfaces when starting services + * fixed deadlock issue when clearing a session using sdt + * \#116 - fixed issue for wlans handling multiple mobility scripts at once + * \#539 - fixed issue in udp tlv api + ## 2020-12-02 CORE 7.3.0 * core-daemon diff --git a/configure.ac b/configure.ac index 7b91b304c..d8e6a34c3 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.3.0) +AC_INIT(core, 7.4.0) # autoconf and automake initialization AC_CONFIG_SRCDIR([netns/version.h.in]) diff --git a/daemon/core/api/grpc/server.py b/daemon/core/api/grpc/server.py index 3159776a6..73fa2fa67 100644 --- a/daemon/core/api/grpc/server.py +++ b/daemon/core/api/grpc/server.py @@ -221,9 +221,9 @@ def StartSession( # clear previous state and setup for creation session.clear() - session.set_state(EventTypes.CONFIGURATION_STATE) if not os.path.exists(session.session_dir): os.mkdir(session.session_dir) + session.set_state(EventTypes.CONFIGURATION_STATE) # location if request.HasField("location"): @@ -315,6 +315,7 @@ def StopSession( """ logging.debug("stop session: %s", request) session = self.get_session(request.session_id, context) + session.data_collect() session.shutdown() return core_pb2.StopSessionResponse(result=True) diff --git a/daemon/core/api/tlv/corehandlers.py b/daemon/core/api/tlv/corehandlers.py index b99b86308..65abed8c4 100644 --- a/daemon/core/api/tlv/corehandlers.py +++ b/daemon/core/api/tlv/corehandlers.py @@ -2029,7 +2029,7 @@ def handle(self): for session_id in sessions: session = self.server.mainserver.coreemu.sessions.get(session_id) if session: - logging.debug("session handling message: %s", session.session_id) + logging.debug("session handling message: %s", session.id) self.session = session self.handle_message(message) self.broadcast(message) diff --git a/daemon/core/emane/emanemodel.py b/daemon/core/emane/emanemodel.py index 0ee9aa404..755f07aa9 100644 --- a/daemon/core/emane/emanemodel.py +++ b/daemon/core/emane/emanemodel.py @@ -12,7 +12,6 @@ from core.emulator.enumerations import ConfigDataTypes from core.errors import CoreError from core.location.mobility import WirelessModel -from core.nodes.base import CoreNode from core.nodes.interface import CoreInterface from core.xml import emanexml @@ -119,13 +118,12 @@ def post_startup(self) -> None: """ logging.debug("emane model(%s) has no post setup tasks", self.name) - def update(self, moved: List[CoreNode], moved_ifaces: List[CoreInterface]) -> None: + def update(self, moved_ifaces: List[CoreInterface]) -> None: """ Invoked from MobilityModel when nodes are moved; this causes emane location events to be generated for the nodes in the moved list, making EmaneModels compatible with Ns2ScriptedMobility. - :param moved: moved nodes :param moved_ifaces: interfaces that were moved :return: nothing """ diff --git a/daemon/core/emulator/coreemu.py b/daemon/core/emulator/coreemu.py index c07d8c953..885fb4318 100644 --- a/daemon/core/emulator/coreemu.py +++ b/daemon/core/emulator/coreemu.py @@ -144,6 +144,7 @@ def delete_session(self, _id: int) -> bool: result = False if session: logging.info("shutting session down: %s", _id) + session.data_collect() session.shutdown() result = True else: diff --git a/daemon/core/emulator/enumerations.py b/daemon/core/emulator/enumerations.py index 5cad0aa57..83e7bffd8 100644 --- a/daemon/core/emulator/enumerations.py +++ b/daemon/core/emulator/enumerations.py @@ -106,6 +106,9 @@ class EventTypes(Enum): def should_start(self) -> bool: return self.value > self.DEFINITION_STATE.value + def already_collected(self) -> bool: + return self.value >= self.DATACOLLECT_STATE.value + class ExceptionLevels(Enum): """ diff --git a/daemon/core/emulator/session.py b/daemon/core/emulator/session.py index 6d583860b..9264ce84b 100644 --- a/daemon/core/emulator/session.py +++ b/daemon/core/emulator/session.py @@ -13,7 +13,7 @@ import threading import time from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, TypeVar +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, TypeVar, Union from core import constants, utils from core.configservice.manager import ConfigServiceManager @@ -369,6 +369,19 @@ def delete_link( node1.delete_iface(iface1_id) elif isinstance(node2, CoreNodeBase) and isinstance(node1, CoreNetworkBase): node2.delete_iface(iface2_id) + elif isinstance(node1, CoreNetworkBase) and isinstance( + node2, CoreNetworkBase + ): + for iface in node1.get_ifaces(control=False): + if iface.othernet == node2: + node1.detach(iface) + iface.shutdown() + break + for iface in node2.get_ifaces(control=False): + if iface.othernet == node1: + node2.detach(iface) + iface.shutdown() + break self.sdt.delete_link(node1_id, node2_id) def update_link( @@ -558,11 +571,11 @@ def add_node( if isinstance(node, WlanNode): self.mobility.set_model_config(_id, BasicRangeModel.name) - # boot nodes after runtime, CoreNodes, Physical, and RJ45 are all nodes - is_boot_node = isinstance(node, CoreNodeBase) and not isinstance(node, Rj45Node) + # boot nodes after runtime CoreNodes and PhysicalNodes + is_boot_node = isinstance(node, (CoreNode, PhysicalNode)) if self.state == EventTypes.RUNTIME_STATE and is_boot_node: self.write_nodes() - self.add_remove_control_iface(node=node, remove=False) + self.add_remove_control_iface(node, remove=False) self.services.boot_services(node) self.sdt.add_node(node) @@ -757,10 +770,11 @@ def shutdown(self) -> None: """ Shutdown all session nodes and remove the session directory. """ + if self.state == EventTypes.SHUTDOWN_STATE: + logging.info("session(%s) state(%s) already shutdown", self.id, self.state) + return logging.info("session(%s) state(%s) shutting down", self.id, self.state) - if self.state != EventTypes.SHUTDOWN_STATE: - self.set_state(EventTypes.DATACOLLECT_STATE, send_event=True) - self.set_state(EventTypes.SHUTDOWN_STATE, send_event=True) + self.set_state(EventTypes.SHUTDOWN_STATE, send_event=True) # clear out current core session self.clear() # shutdown sdt @@ -1115,13 +1129,16 @@ def delete_nodes(self) -> None: """ Clear the nodes dictionary, and call shutdown for each node. """ + nodes_ids = [] with self.nodes_lock: funcs = [] while self.nodes: _, node = self.nodes.popitem() - self.sdt.delete_node(node.id) + nodes_ids.append(node.id) funcs.append((node.shutdown, [], {})) utils.threadpool(funcs) + for node_id in nodes_ids: + self.sdt.delete_node(node_id) def write_nodes(self) -> None: """ @@ -1245,6 +1262,14 @@ def data_collect(self) -> None: :return: nothing """ + if self.state.already_collected(): + logging.info( + "session(%s) state(%s) already data collected", self.id, self.state + ) + return + logging.info("session(%s) state(%s) data collection", self.id, self.state) + self.set_state(EventTypes.DATACOLLECT_STATE, send_event=True) + # stop event loop self.event_loop.stop() @@ -1266,10 +1291,8 @@ def data_collect(self) -> None: self.update_control_iface_hosts(remove=True) # remove all four possible control networks - self.add_remove_control_net(0, remove=True) - self.add_remove_control_net(1, remove=True) - self.add_remove_control_net(2, remove=True) - self.add_remove_control_net(3, remove=True) + for i in range(4): + self.add_remove_control_net(i, remove=True) def short_session_id(self) -> str: """ @@ -1290,7 +1313,6 @@ def boot_node(self, node: CoreNode) -> None: :return: nothing """ logging.info("booting node(%s): %s", node.name, [x.name for x in node.services]) - self.add_remove_control_iface(node=node, remove=False) self.services.boot_services(node) node.start_config_services() @@ -1305,11 +1327,10 @@ def boot_nodes(self) -> List[Exception]: with self.nodes_lock: funcs = [] start = time.monotonic() - for _id in self.nodes: - node = self.nodes[_id] - if isinstance(node, CoreNodeBase) and not isinstance(node, Rj45Node): - args = (node,) - funcs.append((self.boot_node, args, {})) + for node in self.nodes.values(): + if isinstance(node, (CoreNode, PhysicalNode)): + self.add_remove_control_iface(node, remove=False) + funcs.append((self.boot_node, (node,), {})) results, exceptions = utils.threadpool(funcs) total = time.monotonic() - start logging.debug("boot run time: %s", total) @@ -1457,7 +1478,7 @@ def add_remove_control_net( def add_remove_control_iface( self, - node: CoreNode, + node: Union[CoreNode, PhysicalNode], net_index: int = 0, remove: bool = False, conf_required: bool = True, diff --git a/daemon/core/gui/coreclient.py b/daemon/core/gui/coreclient.py index 114aa5b29..01225c6b6 100644 --- a/daemon/core/gui/coreclient.py +++ b/daemon/core/gui/coreclient.py @@ -582,6 +582,8 @@ def set_metadata(self) -> None: # create edges config edges_config = [] for edge in self.links.values(): + if not edge.is_customized(): + continue edge_config = dict(token=edge.token, width=edge.width, color=edge.color) edges_config.append(edge_config) edges_config = json.dumps(edges_config) diff --git a/daemon/core/gui/dialogs/nodeconfig.py b/daemon/core/gui/dialogs/nodeconfig.py index 9ceafa351..de5916315 100644 --- a/daemon/core/gui/dialogs/nodeconfig.py +++ b/daemon/core/gui/dialogs/nodeconfig.py @@ -7,7 +7,7 @@ import netaddr from PIL.ImageTk import PhotoImage -from core.api.grpc.wrappers import Node +from core.api.grpc.wrappers import Interface, Node from core.gui import nodeutils, validation from core.gui.appconfig import ICONS_PATH from core.gui.dialogs.dialog import Dialog @@ -21,8 +21,11 @@ from core.gui.app import Application from core.gui.graph.node import CanvasNode +IFACE_NAME_LEN: int = 15 +DEFAULT_SERVER: str = "localhost" -def check_ip6(parent, name: str, value: str) -> bool: + +def check_ip6(parent: tk.BaseWidget, name: str, value: str) -> bool: if not value: return True title = f"IP6 Error for {name}" @@ -47,7 +50,7 @@ def check_ip6(parent, name: str, value: str) -> bool: return True -def check_ip4(parent, name: str, value: str) -> bool: +def check_ip4(parent: tk.BaseWidget, name: str, value: str) -> bool: if not value: return True title = f"IP4 Error for {name}" @@ -84,16 +87,88 @@ def mac_auto(is_auto: tk.BooleanVar, entry: ttk.Entry, mac: tk.StringVar) -> Non class InterfaceData: def __init__( self, + name: tk.StringVar, is_auto: tk.BooleanVar, mac: tk.StringVar, ip4: tk.StringVar, ip6: tk.StringVar, ) -> None: + self.name: tk.StringVar = name self.is_auto: tk.BooleanVar = is_auto self.mac: tk.StringVar = mac self.ip4: tk.StringVar = ip4 self.ip6: tk.StringVar = ip6 + def validate(self, parent: tk.BaseWidget, iface: Interface) -> bool: + valid_name = self._validate_name(parent, iface) + valid_ip4 = self._validate_ip4(parent, iface) + valid_ip6 = self._validate_ip6(parent, iface) + valid_mac = self._validate_mac(parent, iface) + return all([valid_name, valid_ip4, valid_ip6, valid_mac]) + + def _validate_name(self, parent: tk.BaseWidget, iface: Interface) -> bool: + name = self.name.get() + title = f"Interface Name Error for {iface.name}" + if not name: + messagebox.showerror(title, "Name cannot be empty", parent=parent) + return False + if len(name) > IFACE_NAME_LEN: + messagebox.showerror( + title, + f"Name cannot be greater than {IFACE_NAME_LEN} chars", + parent=parent, + ) + return False + for x in name: + if x.isspace() or x == "/": + messagebox.showerror( + title, "Name cannot contain space or /", parent=parent + ) + return False + iface.name = name + return True + + def _validate_ip4(self, parent: tk.BaseWidget, iface: Interface) -> bool: + ip4_net = self.ip4.get() + if not check_ip4(parent, iface.name, ip4_net): + return False + if ip4_net: + ip4, ip4_mask = ip4_net.split("/") + ip4_mask = int(ip4_mask) + else: + ip4, ip4_mask = "", 0 + iface.ip4 = ip4 + iface.ip4_mask = ip4_mask + return True + + def _validate_ip6(self, parent: tk.BaseWidget, iface: Interface) -> bool: + ip6_net = self.ip6.get() + if not check_ip6(parent, iface.name, ip6_net): + return False + if ip6_net: + ip6, ip6_mask = ip6_net.split("/") + ip6_mask = int(ip6_mask) + else: + ip6, ip6_mask = "", 0 + iface.ip6 = ip6 + iface.ip6_mask = ip6_mask + return True + + def _validate_mac(self, parent: tk.BaseWidget, iface: Interface) -> bool: + mac = self.mac.get() + auto_mac = self.is_auto.get() + if auto_mac: + iface.mac = None + else: + if not netaddr.valid_mac(mac): + title = f"MAC Error for {iface.name}" + messagebox.showerror(title, "Invalid MAC Address", parent=parent) + return False + else: + mac = netaddr.EUI(mac, dialect=netaddr.mac_unix_expanded) + iface.mac = str(mac) + return True + class NodeConfigDialog(Dialog): def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None: @@ -109,7 +184,7 @@ def __init__(self, app: "Application", canvas_node: "CanvasNode") -> None: self.name: tk.StringVar = tk.StringVar(value=self.node.name) self.type: tk.StringVar = tk.StringVar(value=self.node.model) self.container_image: tk.StringVar = tk.StringVar(value=self.node.image) - server = "localhost" + server = DEFAULT_SERVER if self.node.server: server = self.node.server self.server: tk.StringVar = tk.StringVar(value=server) @@ -176,7 +251,7 @@ def draw(self) -> None: frame.columnconfigure(1, weight=1) label = ttk.Label(frame, text="Server") label.grid(row=row, column=0, sticky=tk.EW, padx=PADX, pady=PADY) - servers = ["localhost"] + servers = [DEFAULT_SERVER] servers.extend(list(sorted(self.app.core.servers.keys()))) combobox = ttk.Combobox( frame, textvariable=self.server, values=servers, state=combo_state @@ -229,6 +304,14 @@ def draw_ifaces(self) -> None: button.grid(row=row, sticky=tk.EW, columnspan=3, pady=PADY) row += 1 + label = ttk.Label(tab, text="Name") + label.grid(row=row, column=0, padx=PADX, pady=PADY) + name = tk.StringVar(value=iface.name) + entry = ttk.Entry(tab, textvariable=name, state=state) + entry.var = name + entry.grid(row=row, column=1, columnspan=2, sticky=tk.EW) + row += 1 + label = ttk.Label(tab, text="MAC") label.grid(row=row, column=0, padx=PADX, pady=PADY) auto_set = not iface.mac @@ -267,7 +350,7 @@ def draw_ifaces(self) -> None: entry = ttk.Entry(tab, textvariable=ip6, state=state) entry.grid(row=row, column=1, columnspan=2, sticky=tk.EW) - self.ifaces[iface.id] = InterfaceData(is_auto, mac, ip4, ip6) + self.ifaces[iface.id] = InterfaceData(name, is_auto, mac, ip4, ip6) def draw_buttons(self) -> None: frame = ttk.Frame(self.top) @@ -300,8 +383,11 @@ def click_apply(self) -> None: if NodeUtils.is_image_node(self.node.type): self.node.image = self.container_image.get() server = self.server.get() - if NodeUtils.is_container_node(self.node.type) and server != "localhost": - self.node.server = server + if NodeUtils.is_container_node(self.node.type): + if server == DEFAULT_SERVER: + self.node.server = None + else: + self.node.server = server # set custom icon if self.image_file: @@ -313,43 +399,9 @@ def click_apply(self) -> None: # update node interface data for iface in self.canvas_node.ifaces.values(): data = self.ifaces[iface.id] - - # validate ip4 - ip4_net = data.ip4.get() - if not check_ip4(self, iface.name, ip4_net): - error = True + error = not data.validate(self, iface) + if error: break - if ip4_net: - ip4, ip4_mask = ip4_net.split("/") - ip4_mask = int(ip4_mask) - else: - ip4, ip4_mask = "", 0 - iface.ip4 = ip4 - iface.ip4_mask = ip4_mask - - # validate ip6 - ip6_net = data.ip6.get() - if not check_ip6(self, iface.name, ip6_net): - error = True - break - if ip6_net: - ip6, ip6_mask = ip6_net.split("/") - ip6_mask = int(ip6_mask) - else: - ip6, ip6_mask = "", 0 - iface.ip6 = ip6 - iface.ip6_mask = ip6_mask - - mac = data.mac.get() - auto_mac = data.is_auto.get() - if not auto_mac and not netaddr.valid_mac(mac): - title = f"MAC Error for {iface.name}" - messagebox.showerror(title, "Invalid MAC Address") - error = True - break - elif not auto_mac: - mac = netaddr.EUI(mac, dialect=netaddr.mac_unix_expanded) - iface.mac = str(mac) # redraw if not error: diff --git a/daemon/core/gui/graph/edges.py b/daemon/core/gui/graph/edges.py index 9829cbd49..216fc7f26 100644 --- a/daemon/core/gui/graph/edges.py +++ b/daemon/core/gui/graph/edges.py @@ -28,8 +28,8 @@ def create_wireless_token(src: int, dst: int, network: int) -> str: 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 + iface1_id = link.iface1.id if link.iface1 else 0 + iface2_id = link.iface2.id if link.iface2 else 0 return f"{link.node1_id}-{iface1_id}-{link.node2_id}-{iface2_id}" @@ -297,6 +297,9 @@ def __init__( self.context: tk.Menu = tk.Menu(self.canvas) self.create_context() + def is_customized(self) -> bool: + return self.width != EDGE_WIDTH or self.color != EDGE_COLOR + def create_context(self) -> None: themes.style_menu(self.context) self.context.add_command(label="Configure", command=self.click_configure) diff --git a/daemon/core/gui/validation.py b/daemon/core/gui/validation.py index 22f12bb8b..2360ab0b0 100644 --- a/daemon/core/gui/validation.py +++ b/daemon/core/gui/validation.py @@ -107,7 +107,7 @@ def is_valid(self, s: str) -> bool: if len(s) == 0: return True for x in s: - if not x.isalnum() and x != "_": + if not x.isalnum() and x != "-": return False return True diff --git a/daemon/core/location/mobility.py b/daemon/core/location/mobility.py index a548433cb..95516ce86 100644 --- a/daemon/core/location/mobility.py +++ b/daemon/core/location/mobility.py @@ -31,6 +31,9 @@ if TYPE_CHECKING: from core.emulator.session import Session +LEARNING_DISABLED: int = 0 +LEARNING_ENABLED: int = 30000 + def get_mobility_node(session: "Session", node_id: int) -> Union[WlanNode, EmaneNet]: try: @@ -172,26 +175,6 @@ def sendevent(self, model: "WayPointMobility") -> None: ) self.session.broadcast_event(event_data) - def update_nets( - self, moved: List[CoreNode], moved_ifaces: List[CoreInterface] - ) -> None: - """ - A mobility script has caused nodes in the 'moved' list to move. - Update every mobility network. This saves range calculations if the model - were to recalculate for each individual node movement. - - :param moved: moved nodes - :param moved_ifaces: moved network interfaces - :return: nothing - """ - for node_id in self.nodes(): - try: - node = get_mobility_node(self.session, node_id) - if node.model: - node.model.update(moved, moved_ifaces) - except CoreError: - logging.exception("error updating mobility node") - class WirelessModel(ConfigurableOptions): """ @@ -223,11 +206,10 @@ def links(self, flags: MessageFlags = MessageFlags.NONE) -> List[LinkData]: """ return [] - def update(self, moved: List[CoreNode], moved_ifaces: List[CoreInterface]) -> None: + def update(self, moved_ifaces: List[CoreInterface]) -> None: """ Update this wireless model. - :param moved: moved nodes :param moved_ifaces: moved network interfaces :return: nothing """ @@ -280,6 +262,12 @@ class BasicRangeModel(WirelessModel): Configuration( _id="error", _type=ConfigDataTypes.STRING, default="0", label="loss (%)" ), + Configuration( + _id="promiscuous", + _type=ConfigDataTypes.BOOL, + default="0", + label="promiscuous mode", + ), ] @classmethod @@ -303,6 +291,7 @@ def __init__(self, session: "Session", _id: int) -> None: self.delay: Optional[int] = None self.loss: Optional[float] = None self.jitter: Optional[int] = None + self.promiscuous: bool = False def _get_config(self, current_value: int, config: Dict[str, str], name: str) -> int: """ @@ -369,14 +358,13 @@ def set_position(self, iface: CoreInterface) -> None: position_callback = set_position - def update(self, moved: List[CoreNode], moved_ifaces: List[CoreInterface]) -> None: + def update(self, moved_ifaces: List[CoreInterface]) -> None: """ Node positions have changed without recalc. Update positions from node.position, then re-calculate links for those that have moved. Assumes bidirectional links, with one calculation per node pair, where one of the nodes has moved. - :param moved: moved nodes :param moved_ifaces: moved network interfaces :return: nothing """ @@ -420,7 +408,6 @@ def calclink(self, iface: CoreInterface, iface2: CoreInterface) -> None: with self.wlan._linked_lock: linked = self.wlan.linked(a, b) - if d > self.range: if linked: logging.debug("was linked, unlinking") @@ -467,6 +454,12 @@ def update_config(self, config: Dict[str, str]) -> None: self.delay = self._get_config(self.delay, config, "delay") self.loss = self._get_config(self.loss, config, "error") self.jitter = self._get_config(self.jitter, config, "jitter") + promiscuous = config["promiscuous"] == "1" + if self.promiscuous and not promiscuous: + self.wlan.net_client.set_mac_learning(self.wlan.brname, LEARNING_ENABLED) + elif not self.promiscuous and promiscuous: + self.wlan.net_client.set_mac_learning(self.wlan.brname, LEARNING_DISABLED) + self.promiscuous = promiscuous self.setlinkparams() def create_link_data( @@ -638,17 +631,14 @@ def runround(self) -> None: return return self.run() - # only move interfaces attached to self.wlan, or all nodenum in script? - moved = [] moved_ifaces = [] for iface in self.net.get_ifaces(): node = iface.node if self.movenode(node, dt): - moved.append(node) moved_ifaces.append(iface) # calculate all ranges after moving nodes; this saves calculations - self.session.mobility.update_nets(moved, moved_ifaces) + self.net.model.update(moved_ifaces) # TODO: check session state self.session.event_loop.add_event(0.001 * self.refresh_ms, self.runround) @@ -659,7 +649,6 @@ def run(self) -> None: :return: nothing """ - logging.info("running mobility scenario") self.timezero = time.monotonic() self.lasttime = self.timezero - (0.001 * self.refresh_ms) self.movenodesinitial() @@ -719,7 +708,6 @@ def movenodesinitial(self) -> None: :return: nothing """ - moved = [] moved_ifaces = [] for iface in self.net.get_ifaces(): node = iface.node @@ -727,9 +715,8 @@ def movenodesinitial(self) -> None: continue x, y, z = self.initial[node.id].coords self.setnodeposition(node, x, y, z) - moved.append(node) moved_ifaces.append(iface) - self.session.mobility.update_nets(moved, moved_ifaces) + self.net.model.update(moved_ifaces) def addwaypoint( self, @@ -1114,7 +1101,7 @@ def start(self) -> None: :return: nothing """ - logging.info("starting script") + logging.info("starting script: %s", self.file) laststate = self.state super().start() if laststate == self.STATE_PAUSED: @@ -1135,6 +1122,7 @@ def pause(self) -> None: :return: nothing """ + logging.info("pausing script: %s", self.file) super().pause() self.statescript("pause") @@ -1146,6 +1134,7 @@ def stop(self, move_initial: bool = True) -> None: position :return: nothing """ + logging.info("stopping script: %s", self.file) super().stop(move_initial=move_initial) self.statescript("stop") diff --git a/daemon/core/nodes/base.py b/daemon/core/nodes/base.py index 9069b14c3..9a301432a 100644 --- a/daemon/core/nodes/base.py +++ b/daemon/core/nodes/base.py @@ -837,7 +837,12 @@ def new_iface( if net.has_custom_iface: return net.custom_iface(self, iface_data) else: - iface_id = self.newveth(iface_data.id, iface_data.name) + iface_id = iface_data.id + if iface_id is not None and iface_id in self.ifaces: + raise CoreError( + f"node({self.name}) already has interface({iface_id})" + ) + iface_id = self.newveth(iface_id, iface_data.name) self.attachnet(iface_id, net) if iface_data.mac: self.set_mac(iface_id, iface_data.mac) diff --git a/daemon/core/nodes/interface.py b/daemon/core/nodes/interface.py index 28f7f925c..99f4fb8d3 100644 --- a/daemon/core/nodes/interface.py +++ b/daemon/core/nodes/interface.py @@ -241,12 +241,16 @@ def get_link_options(self, unidirectional: int) -> LinkOptions: jitter = self.getparam("jitter") if jitter is not None: jitter = int(jitter) + buffer = self.getparam("buffer") + if buffer is not None: + buffer = int(buffer) return LinkOptions( delay=delay, bandwidth=bandwidth, dup=dup, jitter=jitter, loss=self.getparam("loss"), + buffer=buffer, unidirectional=unidirectional, ) diff --git a/daemon/core/nodes/netclient.py b/daemon/core/nodes/netclient.py index 68fbef985..729550b6f 100644 --- a/daemon/core/nodes/netclient.py +++ b/daemon/core/nodes/netclient.py @@ -286,14 +286,15 @@ def existing_bridges(self, _id: int) -> bool: return True return False - def disable_mac_learning(self, name: str) -> None: + def set_mac_learning(self, name: str, value: int) -> None: """ - Disable mac learning for a Linux bridge. + Set mac learning for a Linux bridge. :param name: bridge name + :param value: ageing time value :return: nothing """ - self.run(f"{IP} link set {name} type bridge ageing_time 0") + self.run(f"{IP} link set {name} type bridge ageing_time {value}") class OvsNetClient(LinuxNetClient): diff --git a/daemon/core/nodes/network.py b/daemon/core/nodes/network.py index aef4b04b7..cb3aca792 100644 --- a/daemon/core/nodes/network.py +++ b/daemon/core/nodes/network.py @@ -32,7 +32,8 @@ WirelessModelType = Type[WirelessModel] -ebtables_lock = threading.Lock() +LEARNING_DISABLED: int = 0 +ebtables_lock: threading.Lock = threading.Lock() class EbtablesQueue: @@ -946,7 +947,7 @@ def startup(self) -> None: :return: nothing """ super().startup() - self.net_client.disable_mac_learning(self.brname) + self.net_client.set_mac_learning(self.brname, LEARNING_DISABLED) class WlanNode(CoreNetwork): @@ -989,7 +990,6 @@ def startup(self) -> None: :return: nothing """ super().startup() - self.net_client.disable_mac_learning(self.brname) ebq.ebchange(self) def attach(self, iface: CoreInterface) -> None: diff --git a/daemon/core/nodes/physical.py b/daemon/core/nodes/physical.py index e69985ef3..4e8c94644 100644 --- a/daemon/core/nodes/physical.py +++ b/daemon/core/nodes/physical.py @@ -266,7 +266,9 @@ def __init__( will run on, default is None for localhost """ super().__init__(session, _id, name, server) - self.iface = CoreInterface(session, self, name, name, mtu, server) + self.iface: CoreInterface = CoreInterface( + session, self, name, name, mtu, server + ) self.iface.transport_type = TransportType.RAW self.lock: threading.RLock = threading.RLock() self.iface_id: Optional[int] = None @@ -335,11 +337,12 @@ def new_iface( if iface_id is None: iface_id = 0 if self.iface.net is not None: - raise CoreError("RJ45 nodes support at most 1 network interface") + raise CoreError( + f"RJ45({self.name}) nodes support at most 1 network interface" + ) self.ifaces[iface_id] = self.iface self.iface_id = iface_id - if net is not None: - self.iface.attachnet(net) + self.iface.attachnet(net) for ip in iface_data.get_ips(): self.add_ip(ip) return self.iface @@ -353,6 +356,12 @@ def delete_iface(self, iface_id: int) -> None: """ self.get_iface(iface_id) self.ifaces.pop(iface_id) + if self.iface.net is None: + raise CoreError( + f"RJ45({self.name}) is not currently connected to a network" + ) + self.iface.detachnet() + self.iface.net = None self.shutdown() def get_iface(self, iface_id: int) -> CoreInterface: diff --git a/daemon/core/plugins/sdt.py b/daemon/core/plugins/sdt.py index 27e54ff33..4d56f1a96 100644 --- a/daemon/core/plugins/sdt.py +++ b/daemon/core/plugins/sdt.py @@ -4,7 +4,6 @@ import logging import socket -import threading from typing import IO, TYPE_CHECKING, Dict, Optional, Set, Tuple from urllib.parse import urlparse @@ -65,7 +64,6 @@ def __init__(self, session: "Session") -> None: :param session: session this manager is tied to """ self.session: "Session" = session - self.lock: threading.Lock = threading.Lock() self.sock: Optional[IO] = None self.connected: bool = False self.url: str = self.DEFAULT_SDT_URL diff --git a/daemon/core/xml/corexml.py b/daemon/core/xml/corexml.py index 79d3209d6..667ebae88 100644 --- a/daemon/core/xml/corexml.py +++ b/daemon/core/xml/corexml.py @@ -570,6 +570,7 @@ def create_link_element(self, link_data: LinkData) -> etree.Element: add_attribute(options, "unidirectional", options_data.unidirectional) add_attribute(options, "network_id", link_data.network_id) add_attribute(options, "key", options_data.key) + add_attribute(options, "buffer", options_data.buffer) if options.items(): link_element.append(options) @@ -976,6 +977,7 @@ def read_links(self) -> None: if options.loss is None: options.loss = get_float(options_element, "per") options.unidirectional = get_int(options_element, "unidirectional") + options.buffer = get_int(options_element, "buffer") if options.unidirectional == 1 and node_set in node_sets: logging.info("updating link node1(%s) node2(%s)", node1_id, node2_id) diff --git a/daemon/examples/grpc/distributed_switch.py b/daemon/examples/grpc/distributed_switch.py index 0d781c19f..e8ddfb4c7 100644 --- a/daemon/examples/grpc/distributed_switch.py +++ b/daemon/examples/grpc/distributed_switch.py @@ -74,10 +74,14 @@ def main(args): parser.add_argument( "-a", "--address", + required=True, help="local address that distributed servers will use for gre tunneling", ) parser.add_argument( - "-s", "--server", help="distributed server to use for creating nodes" + "-s", + "--server", + required=True, + help="distributed server to use for creating nodes", ) args = parser.parse_args() main(args) diff --git a/daemon/pyproject.toml b/daemon/pyproject.toml index 6916c1970..44d4b2279 100644 --- a/daemon/pyproject.toml +++ b/daemon/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "core" -version = "7.3.0" +version = "7.4.0" description = "CORE Common Open Research Emulator" authors = ["Boeing Research and Technology"] license = "BSD-2-Clause" diff --git a/daemon/tests/test_xml.py b/daemon/tests/test_xml.py index fb8bc4d9d..8a6e465d1 100644 --- a/daemon/tests/test_xml.py +++ b/daemon/tests/test_xml.py @@ -309,6 +309,7 @@ def test_link_options( options.jitter = 10 options.delay = 30 options.dup = 5 + options.buffer = 100 session.add_link(node1.id, switch.id, iface1_data, options=options) # instantiate session @@ -352,6 +353,7 @@ def test_link_options( assert options.jitter == link.options.jitter assert options.delay == link.options.delay assert options.dup == link.options.dup + assert options.buffer == link.options.buffer def test_link_options_ptp( self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes @@ -376,6 +378,7 @@ def test_link_options_ptp( options.jitter = 10 options.delay = 30 options.dup = 5 + options.buffer = 100 session.add_link(node1.id, node2.id, iface1_data, iface2_data, options) # instantiate session @@ -419,6 +422,7 @@ def test_link_options_ptp( assert options.jitter == link.options.jitter assert options.delay == link.options.delay assert options.dup == link.options.dup + assert options.buffer == link.options.buffer def test_link_options_bidirectional( self, session: Session, tmpdir: TemporaryFile, ip_prefixes: IpPrefixes @@ -444,6 +448,7 @@ def test_link_options_bidirectional( options1.loss = 10.5 options1.dup = 5 options1.jitter = 5 + options1.buffer = 50 session.add_link(node1.id, node2.id, iface1_data, iface2_data, options1) options2 = LinkOptions() options2.unidirectional = 1 @@ -452,6 +457,7 @@ def test_link_options_bidirectional( options2.loss = 10 options2.dup = 10 options2.jitter = 10 + options2.buffer = 100 session.update_link( node2.id, node1.id, iface2_data.id, iface1_data.id, options2 ) @@ -499,8 +505,10 @@ def test_link_options_bidirectional( assert options1.loss == link1.options.loss assert options1.dup == link1.options.dup assert options1.jitter == link1.options.jitter + assert options1.buffer == link1.options.buffer assert options2.bandwidth == link2.options.bandwidth assert options2.delay == link2.options.delay assert options2.loss == link2.options.loss assert options2.dup == link2.options.dup assert options2.jitter == link2.options.jitter + assert options2.buffer == link2.options.buffer diff --git a/tasks.py b/tasks.py index 075119a27..bdebffc11 100644 --- a/tasks.py +++ b/tasks.py @@ -100,6 +100,7 @@ def get(cls, name: str, like: List[str], version: Optional[str]) -> "OsInfo": if not os_like: like = " ".join(like) print(f"unsupported os install type({like})") + print("trying using the -i option to specify an install type") sys.exit(1) if version: try: @@ -141,7 +142,7 @@ def get_os(install_type: Optional[str]) -> OsInfo: key, value = line.split("=") d[key] = value.strip("\"") name_value = d["ID"] - like_value = d["ID_LIKE"] + like_value = d.get("ID_LIKE", "") version_value = d["VERSION_ID"] return OsInfo.get(name_value, like_value.split(), version_value)